Pro Puppet (54 page)

Read Pro Puppet Online

Authors: Jeffrey McCune James Turnbull

BOOK: Pro Puppet
9.6Mb size Format: txt, pdf, ePub

We've also used another piece of Puppet auto-magic, the
isnamevar
method, to make this parameter the “name” variable for this type so that the value of this parameter is used as the name of the resource.

Finally, we've defined another parameter,
path
:

newparam(:path) do
  desc "Destination path"
  validate do |value|
    unless value =~ /^\/[a-z0-9]+/
      raise ArgumentError, "%s is not a valid file path" % value
    end
  end
end

This is a parameter value that specifies where the repo type should put the cloned/checked-out repository. In this parameter we've again used the
validate
hook to create a block that checks the value for appropriateness. In this case we're just checking, very crudely, to make sure it looks like the destination path is a valid, fully-qualified file path. We could also use this validation for the
source
parameter to confirm that a valid source URL/location is being provided.

Creating the Subversion Provider

Next, we need to create a Subversion provider for our type. We create the provider and put it into:

custom/lib/puppet/provider/repo/svn.rb

You can see the Subversion provider in
Listing 10-6
.

Listing 10-6.
The Subversion provider

require 'fileutils'
Puppet::Type.type(:repo).provide(:svn) do
  desc "Provides Subversion support for the repo type"
  commands :svncmd => "svn"
  commands :svnadmin => "svnadmin"
  def create         
    svncmd "checkout", resource[:name], resource[:path]
  end
  def destroy        
    FileUtils.rm_rf resource[:path]
  end      
  def exists?
    File.directory? resource[:path]
  end
end

In the provider code, we first required the
fileutils
library, which we're going to use some methods from. Next, we defined the provider block itself:

Puppet::Type.type(:repo).provide(:svn) do

We specified that the provider is called
svn
and is a provider for the type called
repo
.

Then we used the
desc
method, which allows us to add some documentation to our provider.

Next, we defined the commands that this provider will use, the
svn
and
svnadmin
binaries, to manipulate our resource's configuration:

commands :svncmd => "svn"
commands :svnadmin => "svnadmin"

Puppet uses these commands to determine if the provider is appropriate to use on an agent. If Puppet can't find these commands in the local path, then it will disable the provider. Any resources that use this provider will fail and Puppet will report an error.

Next, we defined three methods -
create
,
destroy
and
exists?
. These are the methods that the
ensurable
statement expects to find in the provider.

The
create
method ensures our resource is created. It uses the
svn
command to check out a repository specified by
resource[:name]
. This references the value of the name parameter of the type. In our case, the
source
parameter in our type is also the name variable of the type, so we could also specify
resource[:source]
. We also specified the destination for the checkout using the
resource[:path]
hash.

The
delete
method ensures the deletion of the resource. In this case, it deletes the directory and files specified by the
resource[:path]
parameter.

Lastly, the
exists?
method checks to see if the resource exists. Its operation is pretty simple and closely linked with the value of the ensure attribute in the resource:

  • If
    exists?
    is false and
    ensure
    is set to
    present
    , then the
    create
    method will be called.
  • If
    exists?
    is true and
    ensure
    is set to
    absent
    , then the
    destroy
    method will be called.

In the case of our method, the
exists?
method works by checking if there is already a directory at the location specified in the
resource[:path]
parameter.

We can also add another provider, this one for Git, in:

custom/lib/puppet/provider/repo/git.rb

We can see this provider in
Listing 10-7
.

Listing 10-7.
The Git provider

require 'fileutils'
Puppet::Type.type(:repo).provide(:git) do
  desc "Provides Git support for the repo provider"
  commands :gitcmd => "git"
  def create
    gitcmd "clone", resource[:name], resource[:path]
  end
  def destroy
    FileUtils.rm_rf resource[:path]
  end
  def exists?
    File.directory? resource[:path]
  end
end

You can see that this provider is nearly identical to the Subversion provider we saw in
Listing 10-3
. We used the
git
command and its
clone
function rather than the Subversion equivalents, but you can see that the
destroy
and
exists?
methods are identical.

Using Your New Type

Once you've got your type and providers in place, you can run Puppet and distribute them to the agents you wish to use the
repo
type in and create resources that use this type, for example:

repo { "wordpress":
  source => "http://core.svn.wordpress.org/trunk/",
  path => "/var/www/wp",
  provider => svn,
  ensure => present,
}

Note
You can find a far more sophisticated version of the
repo
type, and with additional providers, at
https://github.com/puppetlabs/puppet-vcsrepo

Writing a Parsed File Type and Provider

You've just seen a very simple type and provider that uses commands to create, delete and check for the status of a resource. In addition to these kinds of types and providers, Puppet also comes with a helper that allows you to parse and edit simple configuration files. This helper is called ParsedFile.

Unfortunately, you can only manage simple files with ParsedFile, generally files with single lines of configuration like the
/etc/hosts
file or the example we're going to examine. This is a type that manages the
/etc/shells
file rather than multi-line configuration files.

To use a ParsedFile type and provider, we need to include its capabilities. Let's start with our
/etc/shells
management type which we're going to call
shells
. This file will be located in:

custom/lib/puppet/type/shells.rb.
The Shells Type

Let's start with our type in
Listing 10-8
.

Listing 10-8.
The shells 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

In our type, we've created a block,
Puppet::Type.newtype(:shells)
, that creates a new type, which we've called
shells
. Inside the block we've got a
@doc
string. As we've already seen, this should contain the documentation for the type; in this case, we've included an example of the
shells
resource in action.

We've also used the
ensurable
statement to create the basic create, delete and exists ensure structure we saw in our previous type.

We then defined a new parameter, called
shell,
that will contain the name of the shell we want to manage:

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

We also used another piece of Puppet automagic that we saw earlier,
isnamevar
, to make this parameter the name variable for this type.

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

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, which we will set in the provider.

The Shells Provider

Let's look at the shells provider now, in
Listing 10-9
.

Listing 10-9.
The shells provider

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

Unlike other providers, ParsedFile providers are stored in a file called
parsed.rb
located in the provider's directory, here:

custom/lib/puppet/provider/shells/parsed.rb

The file needs to be named
parsed.rb
to allow Puppet to load the appropriate ParsedFile support (unlike other providers, which need to be named for the provider itself).

In our provider, we first need to include the ParsedFile provider code at the top of our provider using a Ruby
require
statement:

require 'puppet/provider/parsedfile'

We then set a variable called
shells
to the location of the
/etc/shells
file. We're going to use this variable shortly.

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
variable to the
shells
variable we just created. This tells the provider, that unless it is overridden by the
target
attribute in a resource, that the file to act upon is
/etc/shells
.

We then use a
desc
method that allows us to add some documentation to our provider.

The next lines in the provider are the core of a ParsedFile provider. They tell the Puppet how to manipulate the target file to add or remove the required shell. The first two lines, both called
text_line,
tell Puppet how to match comments and blank lines, respectively, in the configuration file. You should specify these for any file that might have blank lines or comments:

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

We specify these to let Puppet know to ignore these lines as unimportant. The
text_line
lines are constructed by specifying the type of line to match, a comment or a blank, then specifying a regular expression that specifies the actual content to be matched.

The next line performs the actual parsing of the relevant line of configuration 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/anothershell":
     ensure => present,
}

Puppet would then use the provider to add the
/bin/anothershell
by parsing each line of the
/etc/shells
file and checking if the
/bin/anothershell
shell is present. If it is, then Puppet will do nothing. If not, then Puppet will add
anothershell
to the file.

If we changed the
ensure
attribute to
absent,
then Puppet would go through the file and remove the
anothershell
shell if it is present.

This is quite a simple example of a ParsedFile provider. There are a number of others that ship with Puppet, for example the
cron
type, that can demonstrate the sophisticated things you can do with the ParsedFile provider helper.

Other books

Toro! Toro! by Michael Morpurgo
Ready to Bear by Ivy Sinclair
Constant Cravings by Tracey H. Kitts
Everything But by Jade C. Jamison
The Lobster Kings by Alexi Zentner
Blue Belle by Andrew Vachss
Dweller by Strand, Jeff
Stepbrother: Clubbed by Ling, Sybil