Puppet ParsedFile Types And Providers

In a recent post I talked about how easy it is to generate Puppet types and providers. In that post I used the example of a very simple Subversion and Git repository type, called repo. I’d like to show another example of a type and provider, this one used to manage the contents of the /etc/shells file. This type and provider makes use of some built-in Puppet functionality that allows the simple parsing of files and the management of their contents. To do this Puppet has a provider called ParsedFile that can be included into your own providers to provide this functionality. Let’s start with our type:

Puppet::Type.newtype(:shells) do
    @doc = "Manage the contents of /etc/shells

            shells { "/bin/newshell":
                ensure => present,
            }"

    ensurable

    newparam(:shell) do
        desc "The shell to manage"

        isnamevar

    end

    newproperty(:target) do
        desc "Location of the shells file"

        defaultto {
            if
                @resource.class.defaultprovider.ancestors.include? (Puppet::Provider::ParsedFile)
                @resource.class.defaultprovider.default_target
            else
                nil
            end
        }
    end
end

So - pretty simple. We create a block Puppet::Type.newtype(:shells) do that creates a new type, which we’ve called shells. Inside the block we’ve got a @doc string. This is the documentation for the type. Add whatever level of detail and examples in here that is required. We’ve also got the ensurable statement. Ensurable provides some “automagic” that creates a basic ensure property. Puppet types use the ensure property to determine the state of a configuration item. In our previous example, ensurable resulted in three methods in the provider: create, destroy, and exists?. In a ParsedFile provider we don’t use these methods at all as we’ll see shortly but rather specify how to handle each record in the file. We’ve defined a new parameter - this one called shell.

newparam(:shell) do
        desc "The shell to manage"

        isnamevar
end

The shell parameter is the shell we’re going to manage in the /etc/shells file. We’ve also used another piece of Puppet automagic, isnamevar, to make this parameter the “name” variable for this type. In Puppet-speak, the value of this parameter is used as the name of the resource. Lastly in our type we’ve specified an optional parameter, target, that allows us to override the default location of the shells file, usually /etc/shells.

newproperty(:target) do
        desc "Location of the shells file"

        defaultto {
            if
                @resource.class.defaultprovider.ancestors.include? (Puppet::Provider::ParsedFile)
                @resource.class.defaultprovider.default_target
            else
                nil
            end
        }
end

The target parameter is optional and would only be specified if the shells file wasn’t located in the /etc/ directory. It uses the defaultto structure to specify that the default value for the parameter is the value of default_target variable in the provider. The provider for our type is also very simple:

require 'puppet/provider/parsedfile'
shells = "/etc/shells"

Puppet::Type.type(:shells).provide(:parsed, :parent => Puppet::Provider::ParsedFile, :default_target => shells, :filetype => :flat) do

    desc "The shells provider that uses the ParsedFile class"

    text_line :comment, :match => /^#/;
    text_line :blank, :match => /^\s*$/;

    record_line :parsed, :fields => %w{name}
end

The shells provider is stored in a file called parsed.rb in a directory named for the provider in the provider directory, for example:

/usr/lib/ruby/site_ruby/1.8/puppet/type/shells.rb
/usr/lib/ruby/site_ruby/1.8/puppet/provider/shells/parsed.rb

The file needs to be named parsed.rb to allow Puppet to load the ParsedFile support. We first include the ParsedFile provider code at the top of our provider, require 'puppet/provider/parsedfile' and set a variable called shells to the location of the /etc/shells file. We’re going to use this variable a bit later. Then we tell Puppet that this is a provider called shells. We specify a :parent value that tells Puppet that this provider should inherit the ParsedFile provider and make its functions available. We then specify the :default_target value to the shells variable we’ve just created. This tells the provider, that unless it is overridden by the target attribute, that the file to act upon is /etc/shells. Then we use a desc method that allows us to add some documentation to our provider. The next lines are the core of the provider. They tell the Puppet how to manipulate the target file to add or remove the required shell. The first two lines, both text_lines, tell Puppet how to match comments and blank lines respectively.

    text_line :comment, :match => /^#/;
    text_line :blank, :match => /^\s*$/;

We specify these to let Puppet know to ignore these lines as unimportant. The next line performs the actual parsing of the relevant line in the /etc/shells file:

    record_line :parsed, :fields => %w{name}

The record_line parses each line and divides it into fields, in our case we only have one field: name. The name in this case is the shell we want to manage. So if we specify:

shells { "/bin/newshell":
    ensure => present,

Then Puppet would use the provider to add the /bin/newshell by parsing each line of the /etc/shells file and checking if the newshell is present. If it is, then Puppet will do nothing. If not, then Puppet will add newshell to the file. If we changed the ensure attribute to absent then Puppet would go through the file and remove the newshell if it is present. It is important to remember that ParsedFile providers do have some limitations, they aren’t good at managing complex files such as configuration files with multi-line options, they are best for simple files that contain single line lists of entries such as the cron file entries or the /etc/hosts and /etc/shells files. You can see the complete code for this type and its providers at my Puppet repository on GitHub. Quite a lot of the existing Puppet types and providers use ParsedFile providers (the cron type for example) and you can use these as examples of how to create your own providers. You can also find further documentation (in a lot more detail!) on creating your own types and providers at the Puppet wiki.

comments powered by Disqus