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.