Custom types and providers

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:

  • Types abstract a physical resource and specify the interface to its management exposing parameters and properties that allow users to model the resource as desired.
  • Providers implement on the system the types' specifications, adapting to different operating systems. They need to be able to query the current status of a resource and to configure it to reflect the expected state.

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:

  • We know that they abstract the resources of a system
  • They expose parameters to shape them in the desired state
  • They have a title, which must be unique across the catalog
  • One of their parameters is namevar; if not set explicitly its value is taken from the title

Let'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:

  • Parameter 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.
  • Providing the :namevar => true argument to the newparam call:
    newparam(:path, :namevar => true) do
    ...
    end
  • Calling the 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset