Puppet ParsedFile types and providers

February 13th, 2010 by kartar Leave a reply »

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

Let’s start with our :

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 ::.newtype(:) do that creates a new , which we’ve called . Inside the block we’ve got a @doc string. This is the documentation for the . 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. types use the ensure property to determine the state of a configuration item. In our previous example, ensurable resulted in three methods in the : create, destroy, and exists?. In a 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/ file. We’ve also used another piece of automagic, isnamevar, to make this parameter the “name” variable for this . In -speak, the value of this parameter is used as the name of the resource.

Lastly in our we’ve specified an optional parameter, target, that allows us to override the default location of the file, usually /etc/.

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 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 .

The for our 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 is stored in a file called parsed.rb in a directory named for the in the directory, for example:

/usr/lib/ruby/site_ruby/1.8///.rb
/usr/lib/ruby/site_ruby/1.8////parsed.rb

The file needs to be named parsed.rb to allow to load the support.

We first include the code at the top of our , require '//' and set a variable called to the location of the /etc/ file. We’re going to use this variable a bit later.

Then we tell that this is a called . We specify a :parent value that tells that this should inherit the and make its functions available. We then specify the :default_target value to the variable we’ve just created. This tells the , that unless it is overridden by the target attribute, that the file to act upon is /etc/.

Then we use a desc method that allows us to add some documentation to our .

The next lines are the core of the . They tell the how to manipulate the target file to add or remove the required shell. The first two lines, both text_lines, tell how to match comments and blank lines respectively.

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

We specify these to let know to ignore these lines as unimportant.

The next line performs the actual parsing of the relevant line in the /etc/ 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 would use the to add the /bin/newshell by parsing each line of the /etc/ file and checking if the newshell is present. If it is, then will do nothing. If not, then will add newshell to the file. If we changed the ensure attribute to absent then would go through the file and remove the newshell if it is present.

It is important to remember that 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/ files.

You can see the complete code for this and its providers at my Puppet repository on GitHub. Quite a lot of the existing types and providers use providers (the cron 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.

Leave a Reply