If we had to name a single feature that defines Puppet, it would probably be its approach to the management of systems resources.
The abstraction layer that types and providers provide saves us from worrying about implementations on different operating systems of the resources we want on them.
This is a strong and powerful competitive edge of Puppet, and the thing that makes it even more interesting is the possibility of easily creating custom types and providers and seamlessly distributing them to clients.
Types and providers are the components of Puppet's Resource Abstracton Layer; even if strongly coupled, they do different things:
For each type, there must be at least one provider and each provider may be tied to one and only one type.
Custom types can be placed inside a module in files such as lib/puppet/type/<type_name>.rb
, and providers are placed in lib/puppet/provider/<type_name>/<provider_name>.rb
.
Before analyzing a sample piece of code, we will recapitulate what types are about:
namevar
; if not set explicitly its value is taken from the titleLet's see a sample custom native type, what follows manages the execution of psql
commands and is from the Puppet Labs' postgresql
module (https://github.com/puppetlabs/puppetlabs-postgresql); we find it in lib/puppet/type/postgresql_psql.rb
:
Puppet::Type.newtype(:postgresql_psql) do
A type is created by calling the newtype
method of the Puppet::Type
class. We pass the type name, as a symbol, and a block of code with the type's content.
Here, we just have to define the parameters and properties of the type, exactly the ones our users will deal with.
Parameters are set with the newparam
method, here the name
parameter is defined with a brief description and is marked as namevar
with the isnamevar
method:
newparam(:name) do desc "An arbitrary tag for your own reference; the name of the message." end
Every type must have at least one mandatory parameter, the namevar
, the parameter that will identify each resource among the ones of its type. Each type must have exactly one namevar
. There are three options to set the namevar
:
name
is a special case as most types use it as the namevar
; it automatically becomes the namevar
. In the previous example, the parameter would be namevar
.:namevar => true
argument to the newparam
call:newparam(:path, :namevar => true) do ... end
isnamevar
method inside the newparam
block:newparam(:path) do isnamevar end
Types may have parameters, which are instances of the Puppet::Parameter
class, and properties, instances of Puppet::Property
, which inherits Puppet::Parameter
and all its methods.
The main difference between a property and a parameter is that a property model is a part of the state of the managed resource (it define a characteristic), whereas a parameter gives information that the provider will use to manage the properties of the resource.
We should be able to discover the status of a resource's property and modify it.
In the type, we define them. In the providers, we query their status and change them.
For example, the built-in type service has different arguments: ensure
and enable
are properties, all the others are parameters.
The file type has these properties: content
, ctime
, group
, mode
, mtime
, owner
, seluser
, selrole
, seltype
, selrange
, target
, and type
; they represent characteristics of the file resource on the system.
On the other side, its parameters are: path
, backup
, recurse
, recurselimit
, replace
, force
, ignore
, links
, purge
, sourceselect
, show_diff
, source
, source_permissions
, checksum
, and selinux_ignore_defaults
, which allow us to manage the file in various ways, but which are not direct expressions of the characteristics of the file on the system.
A property is set with the newproperty
method, here is how the postgresql_psql
type sets the command
property, which is the SQL query we have to do:
newproperty(:command) do desc 'The SQL command to execute via psql.'
A default value can be defined here. In this case, it is the resource name:
defaultto { @resource[:name] }
In this specific case, the sync
method of Puppet::Property
is redefined to manage this particular case:
def sync(refreshing = false) if ([email protected]? || refreshing) super() else nil end end end
Other parameters have the same structure:
newparam(:db) do desc "The name of the database to execute the SQL command against." end newparam(:search_path) do desc "The schema search path to use when executing the SQL command" end newparam(:psql_user) do desc "The system user account under which the psql command should be executed." defaultto("postgres") end […] end
The postgresql_psql
type continues with the definition of other parameters, their description and, where possible, the default values.
If a parameter or a property is required, we set this with the isrequired
method, we can also validate the input values if we need to force specific data types or values, and normalize them with the munge
method.
A type can also be made ensurable, that is, have an ensure
property that can be set to present
or absent
, the property is automatically added just by calling the ensurable
method.
We can also set automatic dependencies for a type; for example, the exec
native type has an automatic dependency for a user resource that creates the user who is supposed to run the command as, if this is set by its name and not its uid
, this is how it is done in lib/puppet/type/exec.rb
:
autorequire(:user) do # Autorequire users if they are specified by name if user = self[:user] and user !~ /^d+$/ user end end
We can use such a type in our manifests, here is, for example, how it's used in the postgresql::server::grant
define of the Puppet Labs' postgresql
module:
$grant_cmd = "GRANT ${_privilege} ON ${_object_type} "${objectname}" TO "${role}"" postgresql_psql { $grant_cmd: db => $on_db, port => $port, psql_user => $psql_user, psql_group => $group, psql_path => $psql_path, unless => "SELECT 1 WHERE ${unless_function}('${role}', '${object_name}', '${unless_privilege}')", require => Class['postgresql::server'] }
For each type, there must be at least one provider. When the implementation of the resource defined by the type is different according to factors such as the operating system, we may have different providers for a given type.
A provider must be able to query the current state of a resource and eventually configure it according to the desired state, as defined by the parameters we've provided to the type.
We define a provider by calling the provide
method of Puppet::Type.type()
; the block passed to it is the content of our provider.
We can restrict a provider to a specific platform with the confine
method and, in case of alternatives, use the defaultfor
method to make it the default one.
For example, the portage provider of the package type has something like:
Puppet::Type.type(:package).provide :portage, :parent => Puppet::Provider::Package do desc "Provides packaging support for Gentoo's portage system." has_feature :versionable confine :operatingsystem => :gentoo defaultfor :operatingsystem => :gentoo […]
In the preceding example, confine
has matched a fact value, but it can also be used to check for a file existence, a system's feature, or any piece of code:
confine :exists => '/usr/sbin/portage' confine :feature => :selinux confine :true => begin [Any block of code that enables the provider if returns true] end
Note also the desc
method, used to set a description of the provider, and the has_feature
method, used to define the supported features of the relevant type.
The provider has to execute commands on the system. These are defined via the command
or optional_command
methods; the latter defines a command, which might not exist on the system and is not required by the provider.
For example, the useradd
provider of the user
type has the following commands defined:
Puppet::Type.type(:user).provide :useradd, :parent => Puppet::Provider::NameService::ObjectAdd do commands :add => "useradd", :delete => "userdel", :modify => "usermod", :password => "change" optional_commands :localadd => "luseradd"
When we define a command, a new method is created; we can use it where needed, passing eventual arguments via an array. The defined command is searched in the path, unless specified with an absolute path.
All the types' property and parameter values are accessible via the []
method of the resource
object, which are resource[:uid]
and resource[:groups]
.
When a type is ensurable
, its providers must support the create
, exists?
and destroy
methods, which are used, respectively, to create the resource type, check whether it exists, and remove it.
The exists?
method, in particular, is at the basis of Puppet idempotence, since it verifies that the resource is in the desired state or needs to be synced.
For example, the zfs
provider of the zfs
zone implements these methods running the (previously defined) zfs
command:
def create zfs *([:create] + add_properties + [@resource[:name]]) end def destroy zfs(:destroy, @resource[:name]) end def exists? if zfs(:list).split(" ").detect { |line| line.split("s")[0] == @resource[:name] } true else false end end
For every property of a type, the provider must have methods to read (getter) and modify (setter) its status. These methods have exactly the same name of the property, with the setter ending with an equal symbol (=
).
For example, the ruby
provider of the postgresql_psql
type we have seen before has these methods to manage the command to execute (here we have removed the implementation code):
Puppet::Type.type(:postgresql_psql).provide(:ruby) do def command() [ Code to check if sql command has to be executed ] end def command=(val) [ Code that executes the sql command ] end
If a property is out of sync, the setter
method is invoked to configure the system as desired.