Chapter 10. Extending Facter and Puppet

Among the most powerful features of Puppet are its flexibility and extensibility. In addition to the existing facts, resource types, providers, and functions, you can quickly and easily add custom code specific to your environment or to meet a particular need.

In the first part of this chapter we're going to examine how to add your own custom facts. Adding custom facts is highly useful for gathering and making use of information specific to your environment. Indeed, we've used Facter extensively in this book to provide information about our hosts, applications and services, and you've seen the array of facts available across many platforms. You may have noted, though, that Facter isn't comprehensive; many facts about your hosts and environments are not available as Facter facts.

In the second part of the chapter, we're going to examine how to add your own custom types, providers and functions to Puppet and how to have Puppet distribute these, and we'll discuss how to make use of them. These are among Puppet's most powerful features, and are at the heart of its flexibility and extensibility. Being able to add your own enhancements in addition to the existing resources types, providers and functions, you can quickly and easily add custom code specific to your environment or to meet a particular need.

Writing and Distributing Custom Facts

Creating your own custom facts to Puppet is a very simple process. Indeed, it only requires a basic understanding of Ruby. Luckily for you, Ruby is incredibly easy to pick up and there are lots of resources available to help (refer to the "Resources" section at the end of the chapter for some helpful links).

In the following sections, you'll see how to successfully extend Facter. We first configure Puppet so we can write custom facts, then we test our new facts to confirm they are working properly.

Note

If the idea of learning any Ruby is at all daunting, a fast alternative way to add a fact without writing any Ruby code is via Facter's support of environmental variables. Any environmental variables set by the user Facter is running as (usually the root user) that are prefixed with FACTER_ will be added to Facter as facts. So, if you were to set an environmental variable of FACTER_datacenter with a value of Chicago, then this would become a fact called datacenter with the value of Chicago.

Configuring Puppet for Custom Facts

The best way to distribute custom facts is to include them in modules, using a Puppet concept called "plug-ins in modules." This concept allows you to place your custom code inside an existing or new Puppet module and then use that module in your Puppet environment. Custom facts, custom types, providers, and functions are then distributed to any host that includes a particular module.

Modules that distribute facts are no different from other modules, and there are two popular approaches to doing so. Some people distribute facts related to a particular function in the module that they use to configure that function. For example, a fact with some Bind data in it might be distributed with the module you use to configure Bind. This clusters facts specific to a function together and allows a greater portability. Other sites include all custom facts (and other items) in a single, central module, such as a module called facts or plugins. This centralizes facts in one location for ease of management and maintenance.

Each approach has pros and cons and you should select one that suits your organization and its workflow. We personally prefer the former approach because it limits custom facts and other items to only those clients that require them, rather than all hosts. For some environments, this may be a neater approach. We're going to use this approach in this section when demonstrating managing custom facts.

So where in our modules do facts go? Let's create a simple module called bind as an example:

bind/
bind/manifests
bind/manifests/init.pp
bind/files
bind/templates
bind/lib/facter

Here we've created our standard module directory structure, but we've added another directory, lib. The lib directory contains any "plug-ins" or additional facts, types or functions we want to add to Puppet. We're going to focus on adding facts; these are stored in the lib/facter directory.

In addition to adding the lib/facter directory to modules that will distribute facts, you need to enable "plug-ins in modules" in your Puppet configuration. To do this, enable options in the [main] section of the Puppet master's puppet.conf configuration file, as you can see on the next line:

[main]
pluginsync = true

When set to true, the pluginsync setting turns on the "plug-ins in modules" capability. Now, when clients connect to the master, each client will check its modules for facts and other custom items. Puppet will take these facts and other custom items and sync them to the relevant clients, so they can then be used on these clients.

Warning

The sync of facts and other items occurs during the Puppet run. In some cases, the custom items synchronized may not be available in that initial Puppet run. For example, if you sync a fact during a Puppet run and rely on the value of that fact in configuration you are using in the SAME run, then that configuration may fail. This is because Puppet has yet to re-run Facter and assign a value for the new custom fact you've provided. On subsequent runs, the new fact's value will be populated and available to Puppet.

Writing Custom Facts

After configuring Puppet to deliver our custom facts, you should actually create some new facts! Each fact is a snippet of Ruby code wrapped in a Facter method to add the result of our Ruby code as a fact. Let's look at a simple example in Listing 10-1.

Example 10-1. Our first custom fact

Facter.add("home") do
       setcode do
           ENV['HOME']
       end
end

In this example, our custom fact returns the value of the HOME environmental value as a fact called home, which in turn would be available in our manifests as the variable $home.

The Facter.add method allows us to specify the name of our new fact. We then use the setcode block to specify the contents of our new fact, in our case using Ruby's built-in ENV variable to access an environmental variable. Facter will set the value of our new fact using the result of the code executed inside this block.

In Listing 10-2, you can see a custom fact that reads a file to return the value of the fact.

Example 10-2. Another custom fact

Facter.add("timezone") do
       confine :operatingsystem => :debian
       setcode do
            File.readlines("/etc/timezone").to_a.last
       end
end

Here, we're returning the timezone of a Debian host. We've also done two interesting things. First, we've specified a confine statement. This statement restricts the execution of the fact if a particular criteria is not met. This restriction is commonly implemented by taking advantage of the values of other facts. In this case, we've specified that the value of the operatingsystem fact should be Debian for the fact to be executed. We can also use the values of other facts, for example:

confine :kernel => :linux

The previous confine is commonly used to limit the use of a particular fact to nodes with Linux-based kernels.

Second, we've used the readlines File method to read in the contents of the /etc/timezone file. The contents are returned as the fact timezone, which in turn would be available as the variable $timezone.

timezone => Australia/Melbourne

We've established how to confine the execution of a fact but we can also use other fact values to influence our fact determination, for example:

Facter.add("timezone") do
       setcode do
         if Facter.value(:operatingsystem) =~ /Debian|Ubuntu/
File.readlines("/etc/timezone").to_a.last
         else
            tz = Time.new.zone
         end
       end
end

Here, if the operating system is Debian or Ubuntu, it will return a time zone value by returning the value from the /etc/timezone file. Otherwise, the fact will use Ruby's in-built time handling to return a time zone.

You could also use a case statement to select different fact values, for example as used in the operatingsystemrelease fact shown in Listing 10-3.

Example 10-3. Using a case statement to select fact values

Facter.add(:operatingsystemrelease) do
    confine :operatingsystem => %w{CentOS Fedora oel ovs RedHat MeeGo}
    setcode do
        case Facter.value(:operatingsystem)
        when "CentOS", "RedHat"
            releasefile = "/etc/redhat-release"
        when "Fedora"
            releasefile = "/etc/fedora-release"
        when "MeeGo"
            releasefile = "/etc/meego-release"
        when "OEL", "oel"
            releasefile = "/etc/enterprise-release"
        when "OVS", "ovs"
            releasefile = "/etc/ovs-release"
        end
        File::open(releasefile, "r") do |f|
            line = f.readline.chomp
            if line =~ /(Rawhide)$/
                "Rawhide"
            elsif line =~ /release (d[d.]*)/
                $1
            end
        end
    end
end

You can use other fact values for any purpose you like, not just for determining how to retrieve a fact. Some facts return another fact value if they cannot find a way to determine the correct value. For example, the operatingsystem fact returns the current kernel, Facter.value(:kernel), as the value of operatingsystem if Facter cannot determine the operating system it is being run on.

You can create more complex facts and even return more than one fact in your Ruby snippets, as you can see in Listing 10-4.

Example 10-4. A more complex fact

netname = nil
       netaddr = nil
       test = {}
       File.open("/etc/networks").each do |line|
            netname = $1 and netaddr = $2 if line 
A more complex fact
=~ /^(w+.?w+)s+([0-9]+.[0-9]+.[0-9]+.[0-9]+)/ if netname != nil && netaddr != nil test["network_" + netname] = netaddr netname = nil netaddr = nil end end test.each{|name,fact| Facter.add(name) do setcode do fact end end }

This fact actually creates a series of facts, each fact taken from information collected from the /etc/networks file. This file associates network names with networks. Our snippet parses this file and adds a series of facts, one per each network in the file. So, if our file looked like:

default    0.0.0.0
loopback    127.0.0.0
link-local    169.254.0.0

Then three facts would be returned:

network_default => 0.0.0.0
network_loopback => 127.0.0.0
network_link-local => 169.254.0.0

You can take a similar approach to commands, or files, or a variety of other sources.

Testing the Facts

There is a simple process for testing your facts: Import them into Facter and use it to test them before using them in Puppet. To do this, you need to set up a testing environment. Create a directory structure to hold our test facts—we'll call ours lib/ruby/facter. Situate this structure beneath the root user's home directory. Then create an environmental variable, $RUBYLIB, that references this directory and will allow Facter to find our test facts:

# mkdir -p ~/lib/ruby/facter
# export RUBYLIB=~/lib/ruby

Then copy your fact snippets into this new directory:

# cp /var/puppet/facts/home.rb $RUBYLIB/facter

After this, you can call Facter with the name of the fact you've just created. If the required output appears, your fact is working correctly. On the following lines, we've tested our home fact and discovered it has returned the correct value:

# facter home
/root

If your fact is not working correctly, an error message you can debug will be generated.

Facts just scratch the surface of Puppet's extensibility, and adding to types, providers, and functions adds even more capability. We're going to demonstrate that in the next section.

Developing Custom Types, Providers and Functions

When developing custom types, providers and functions it is important to remember that Puppet and Facter are open-source tools developed by both Puppet Labs and a wide community of contributors. Sharing custom facts and resource types helps everyone in the community, and it means you can also get input from the community on your work. Extending Puppet or Facter is also an excellent way to give back to that community. You can share your custom code via the Puppet mailing list or on the Puppet Wiki, by logging a Redmine ticket, or by setting up your own source repository for Puppet code on the Puppet forge (http://forge.puppetlabs.com).

Lastly, don't underestimate the usefulness of code people before you have already developed that you can use and adapt for your environment. Explore existing Puppet modules, plug-ins, facts and other code via Google and on resources like GitHub. Like all systems administrators, we know that imitation is the ultimate form of flattery.

In the following sections, we demonstrate how to configure Puppet to distribute your own custom code. You'll also see how to write a variety of custom types and providers, and finally how to write your own Puppet functions.

Configuring Puppet for Types, Providers and Functions

The best way to distribute custom types, providers and functions is to include them in modules, using "plug-ins in modules," the same concept we introduced earlier this chapter to distribute custom facts. Just like custom facts, you again place your custom code into a Puppet module and use that module in your configuration. Puppet will take care of distributing your code to your Puppet masters and agents.

Again, just like custom facts, you can take two approaches to managing custom code: placing it in function-specific modules or centralizing it into a single module. We're going to demonstrate adding custom code in a single, function-specific module.

So, where in our modules does custom code go? Let's create a simple module called apache as an example:

apache/
apache/manifests
apache/manifests/init.pp
apache/files
apache/templates
apache/lib/facter
apache/lib/puppet/type
apache/lib/puppet/provider
apache/lib/puppet/parser/functions

Here we've created our standard module directory structure, but we've added another directory, lib. We saw the lib directory earlier in the chapter when we placed custom facts into its Facter subdirectory. The lib directory also contains other "plug-ins" like types, providers and functions, which we want to add to Puppet. The lib/puppet/type and lib/puppet/provider directories hold custom types and providers respectively. The last directory, lib/puppet/parser/functions, holds custom functions.

Like we did when we configured Puppet for custom facts, you need to enable "plug-ins in modules" in your Puppet configuration. To do this, enable the pluginsync option in the [main] section of the Puppet master's puppet.conf configuration file, as follows:

[main]
pluginsync = true

The pluginsync setting, when set to true, turns on the "plug-ins in modules" capability. Now, when agents connect to the master, each agent will check its modules for custom code. Puppet will take this custom code and sync it to the relevant agents. It can then be used on these agents. The only exception to this is custom functions. Functions run on the Puppet master rather than the Puppet agents, so they won't be synched down to an agent. They will only be synched if the Puppet agent is run on the Puppet master, i.e., if you are managing Puppet with Puppet.

Note

In earlier releases of Puppet, "plug-ins in modules" required some additional configuration. You can read about that configuration on the Puppet Labs Documentation site at http://docs.puppetlabs.com/guides/plugins_in_modules.html.

Writing a Puppet Type and Provider

Puppet types are used to manage individual configuration items. Puppet has a package type, a service type, a user type, and all the other types available. Each type has one or more providers. Each provider handles the management of that configuration on a different platform or tool: for example, the package type has aptitude, yum, RPM, and DMG providers (among 22 others).

We're going to show you a simple example of how to create an additional type and provider, one that manages version control systems (VCS), which we're going to call repo. In this case we're going to create the type and two providers, one for Git and one for SVN. Our type is going to allow you to create, manage and delete VCS repositories.

A Puppet type contains the characteristics of the configuration item we're describing, for example in the case of VCS management type:

  • The name of the repository being managed

  • The source of the repository

Correspondingly, the Puppet providers specify the actions required to manage the state of the configuration item. Obviously, each provider has a set of similar actions that tell it how to:

  • Create the resource

  • Delete the resource

  • Check for the resource's existence or state

  • Make changes to the resource's content

Creating Our Type

Let's start by creating our type. We're going to create a module called custom to store it in:

custom/
custom/manifests/init.pp
custom/lib/puppet/type
custom/lib/puppet/provider

Inside the lib/puppet/type directory, we're going to create a file called repo.rb to store our type definition:

custom/lib/puppet/type/repo.rb

You can see that file in Listing 10-5.

Example 10-5. The repo type

Puppet::Type.newtype(:repo) do
    @doc = "Manage repos"
    ensurable

    newparam(:source) do
        desc "The repo source"

        validate do |value|
            if value =~ /^git/
                resource[:provider] = :git
            else
                resource[:provider] = :svn
            end
        end

        isnamevar
    end

    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
end

In this example, we start our type with the Puppet::Type.newtype block and specify the name of type to be created, repo. We can also see a @doc string which is where we specify the documentation for your type. We recommend you provide clear documentation including examples of how to use the type, for a good example have a look at the documentation provided for the Cron type at https://github.com/puppetlabs/puppet/blob/master/lib/puppet/type/cron.rb.

The next statement is ensurable. The ensurable statement is a useful shortcut that tells Puppet to create an ensure property for this type. The ensure property determines the state of the configuration item, for example:

service { "sshd":
    ensure => present,
}

The ensurable statement tells Puppet to expect three methods: create, destroy and exists? in our provider (You'll see the code for this in Listing 10-6). These methods are, respectively:

  • A command to create the resource

  • A command to delete the resource

  • A command to check for the existence of the resource

All we then need to do is specify these methods and their contents and Puppet creates the supporting infrastructure around them. Types have two kinds of values - properties and parameters. Properties "do things." They tell us how the provider works. We've only defined one property, ensure, by using the ensurable statement. Puppet expects that properties will generally have corresponding methods in the provider that we'll see later in this chapter. Parameters are variables and contain information relevant to configuring the resource the type manages, rather than "doing things."

Next, we've defined a parameter, called source:

newparam(:source) do
  desc "The repo source"

  validate do |value|
    if value =~ /^git/
      resource[:provider] = :git
   else
     resource[:provider] = :svn
  end
 end
   isnamevar
end

The source parameter will tell the repo type where to go to retrieve, clone, or check out our source repository.

In the source parameter we're also using a hook called validate. It's normally used to check the parameter value for appropriateness; here, we're using it to take a guess at what provider to use.

Note

In addition to the validate hook, Puppet also has the munge hook. You can use the munge hook to adjust the value of the parameter rather than validating it before passing it to the provider.

Our validate code specifies that if the source parameter starts with git, then use the Git provider; if not, then default to the Subversion provider. This is fairly crude as a default, and you can override this by defining the provider attribute in your resource, like so:

repo { "puppet":
  source => "git://github.com/puppetlabs/puppet.git",
  path => "/home/puppet",
  provider => git,
  ensure => present,
}

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.

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

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

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

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

A More Complex Type and Provider

In this section we're going to show you a slightly more complex type and provider used to manage HTTP authentication password files. It's a similarly ensureable type and provider, but with some more sophisticated components.

The httpauth Type

Let's start by looking at the httpauth type shown in Listing 10-10.

Example 10-10. The httpauth type

Puppet::Type.newtype(:httpauth) do
    @doc = "Manage HTTP Basic or Digest password files." +
           "    httpauth { 'user':                     " +
           "      file => '/path/to/password/file',    " +
           "      password => 'password',              " +
           "      mechanism => basic,                  " +
           "      ensure => present,                   " +
           "    }                                      "

    ensurable do
       newvalue(:present) do
           provider.create
       end

       newvalue(:absent) do
           provider.destroy
       end

       defaultto :present
    end

    newparam(:name) do
       desc "The name of the user to be managed."

       isnamevar
    end

    newparam(:file) do
       desc "The HTTP password file to be managed. If it doesn't exist it is created."
    end

    newparam(:password) do
       desc "The password in plaintext."

    end

    newparam(:realm) do
       desc "The realm - defaults to nil and mainly used for Digest authentication."

       defaultto "nil"
    end

    newparam(:mechanism) do
       desc "The authentication mechanism to use - either basic or digest. Default to basic."
newvalues(:basic, :digest)

       defaultto :basic
    end

    # Ensure a password is always specified
    validate do
       raise Puppet::Error, "You must specify a password for the user." unless @parameters.include?(:password)
    end
end

In the httpauth type we're managing a number of attributes, principally the user, password and password file. We also provide some associated information, like the realm (A HTTP Digest Authentication value) and the mechanism we're going to use, Basic or Digest Authentication.

First, notice that we've added some code to our ensurable method. In this case, we're telling Puppet some specifics about the operation of our ensure attribute. We're specifying that for each state, present and absent, exactly which method in the provider should be called, here create and destroy, respectively. We're also specifying the default behavior of the ensure attribute. This means that if we omit the ensure attribute that the httpauth resource will assume present as the value. The resource will then check for the presence of the user we want to manage, and if it doesn't exist, then it will create that user.

We've also used some other useful methods. The first is the defaultto method that specifies a default value for a parameter or property. If the resource does not specify this attribute, then Puppet will use to this default value to populate it. The other is the newvalues method that allows you to specify the values that the parameter or property will accept. In Listing 10-10, you can see the mechanism parameter that the newvalues method specifies will take the values of basic or digest.

Lastly, you can see that we used the validate method to return an error if the httpauth resource is specified without the password attribute.

The httpauth Provider

Now let's look at the provider for the httpauth type, shown in Listing 10-11.

Example 10-11. The httpauth provider

begin
    require 'webrick'
rescue
    Puppet.warning "You need WEBrick installed to manage HTTP Authentication files."
end

Puppet::Type.type(:httpauth).provide(:httpauth) do
    desc "Manage HTTP Basic and Digest authentication files"

    def create
        # Create a user in the file we opened in the mech method
        @htauth.set_passwd(resource[:realm], resource[:name], resource[:password])
        @htauth.flush
end

    def destroy
        # Delete a user in the file we opened in the mech method
        @htauth.delete_passwd(resource[:realm], resource[:name])
        @htauth.flush
    end

    def exists?
        # Check if the file exists at all
        if File.exists?(resource[:file])
            # If it does exist open the file
            mech(resource[:file])

            # Check if the user exists in the file
            cp = @htauth.get_passwd(resource[:realm], resource[:name], false)

            # Check if the current password matches the proposed password
            return check_passwd(resource[:realm], resource[:name], resource[:password], cp)
        else
            # If the file doesn't exist then create it
            File.new(resource[:file], "w")
            mech(resource[:file])
            return false
        end
    end

    # Open the password file
    def mech(file)
        if resource[:mechanism] == :digest
            @htauth = WEBrick::HTTPAuth::Htdigest.new(file)
        elsif resource[:mechanism] == :basic
            @htauth = WEBrick::HTTPAuth::Htpasswd.new(file)
        end
    end

    # Check password matches
    def check_passwd(realm, user, password, cp)
        if resource[:mechanism] == :digest
            WEBrick::HTTPAuth::DigestAuth.make_passwd(realm, user, password) == cp
        elsif resource[:mechanism] == :basic
            # Can't ask webbrick as it uses a random seed
            password.crypt(cp[0,2]) == cp
        end
    end
end

This provider is more complex than what we've seen before. We've still got the methods that handle Puppet's ensurable capabilities: create, destroy and exists?. In addition, though, we've got additional methods that manipulate our password files.

Our provider first checks for the existence of the Webrick library, which it needs in order to manipulate HTTP password files. The provider will fail to run if this library is not present. Fortunately, Webrick is commonly present in most Ruby distributions (and indeed, is used by Puppet as its basic server framework, as we learned in 2).

Tip

As an alternative to requiring the Webrick library, we could use Puppet's feature capability. You can see some examples of this in https://github.com/puppetlabs/puppet/blob/master/lib/puppet/feature/base.rb. This capability allows you to enabled or disable features based on whether certain capabilities are present or not. The obvious limitation is that this approach requires adding a new feature to Puppet's core, rather than simply adding a new type or provider.

Our provider then has the three ensurable methods. The create and destroy methods are relatively simple. They use methods from the Webrick library to either set or delete a password specified in the HTTP password file managed by the resource. The file being referred to here using the resource[:file] value which is controlled by setting the file attribute in the httpauth resource, for example:

httpauth { "bob":
  file => "/etc/apache2/htpasswd.basic",
  password => "password",
  mechanism => basic,
}

Lastly, you'll also see in the create and destroy methods that we call the flush method. This flushes the buffer and writes out our changes.

The exists? method is more complex and calls several helper methods to check whether the user and password already exist, and if they do, whether the current and proposed passwords match.

Testing Types and Providers

Like facts, you can test your types and providers. The best way to do this is add them to a module in your development or testing environment and enable pluginsync to test them there before using them in your production environment, for example let's add our HTTPAuth type to a module called httpauth, first adding the required directories:

$ mkdir -p /etc/puppet/modules/httpauth/(manifests,files,templates,lib}
$ mkdir -p /etc/puppet/modules/httpauth/lib/{type,provider}
$ mkdir -p /etc/puppet/modules/httpauth/lib/provider/httpauth

Then copying in the type and provider to the requisite directories.

# cp type/httpauth.rb /etc/puppet/modules/lib/type/httpauth.rb
# cp provider/httpauth.rb /etc/puppet/modules/lib/provider/httpauth/httpauth.rb

When Puppet is run (and pluginsync enabled) it will find your types and providers in these directories, deploy them and make them available to be used in your Puppet manifests.

Writing Custom Functions

The last type of custom Puppet code we're going to look at is the function. You've seen a number of functions in this book already, for example: include, notice and template are all functions we've used. But you can extend the scope of the available functions by writing your own.

There are two types of functions: statements and rvalues. Statements perform some action, for example the fail function, and rvalues return a value, for example if you pass in a value, the function will process it and return a value. The split function is an example of an rvalue function.

Note

Remember that functions are executed on the Puppet master. They only have access to resources and data that are contained on the master.

We're going to write a simple function and distribute it to our agents. Like plug-ins, we can use plug-in sync to distribute functions to agents; they are stored in:

custom/lib/puppet/parser/functions

The file containing the function must be named after the function it contains; for example, the template function should be contained in the template.rb file.

Let's take a look at a simple function in Listing 10-12.

Example 10-12. The SHA512 function

Puppet::Parser::Functions::newfunction(:sha512, :type => :rvalue, :doc => "Returns a SHA1hash value from a provided string.") do |args|

  require 'sha1'

  Digest::SHA512.hexdigest(args[0])

end

Puppet contains an existing function called sha1 that generates a SHA1 hash value from a provided string. In Listing 10-12, we've updated that function to support SHA512 instead. Let's break that function down. To create the function we call the Puppet::Parser::Functions::newfunction method and pass it some values. First, we name the function, in our case sha512. We then specify the type of function it is, here rvalue, for a function that returns a value. If we don't specify the type at all then Puppet assumes the function is a statement. Lastly, we specify a :doc string to document the function.

The newfunction block takes the incoming argument and we process it, first adding in support for working with SHA hashes by requiring the sha1 library, and then passing the argument to the hexdigest method. As this is an rvalue function, it will return the created hash as the result of the function.

Note

The last value returned by the newfunction block will be returned to Puppet as the rvalue.

We mentioned earlier that functions run on the Puppet master. This means we only have access to the resources and data available on the master, but this does include some quite useful information, most importantly fact data. You can look up and use the value of facts in your functions using the lookupvar function, like so:

lookupvar('fqdn')

Replace fqdn with the name of the fact whose value you wish to look up.

You can see how easy it is to create some very powerful functions in only a few lines of code. We recommend having a look at the existing functions (most of which are very succinct) as a way to get started on your first functions. Some of the common functions include tools to manipulate paths, regular expressions and substitutions, and functions to retrieve data from external sources. There are numerous examples (many on Github or searchable via Google) of functions that you can copy or adapt for your environment.

After you've created your function you should test that it works correctly. There are a couple of ways you can do this. Some basic testing of the function can be performed by executing the function file with Ruby, like so:

$ ruby –rpuppet sha512.rb

This loads the Puppet library (Puppet must be installed on the host) and then runs the file containing the function we created in Listing 10-12. This will allow us to determine whether the file parses without error. It does not tell us if the function performed correctly.

Tip

You can raise an error in your function using raise Puppet::ParseError, "raise this error". Replace "raise this error" with the error text you'd like to raise.

We can also use the Ruby IRB (Interactive Ruby Shell) to confirm our function is properly defined, like so:

$ irb
irb> require 'puppet'
=> true
irb> require '/tmp/sha512.rb'
=> true
irb> Puppet::Parser::Functions.function(:sha512)
=> "function_sha512"

Here we've launched irb and then required Puppet and our new function. We then confirm that Puppet can see the new function and that it parses as a correct function.

The best way to test a function is to use it in a manifest, and the easiest way to do that is to add your functions to Puppet's libdir and run a stand-alone manifest. Assuming Puppet is installed, first find your libdir:

$ sudo puppet –configprint | grep 'libdir'
/var/lib/puppet/lib

Then create a directory to hold our functions:

$ sudo mkdir -p /var/lib/puppet/lib/puppet/parser/functions

Then copy in our function:

$ sudo cp sha512.rb /var/lib/puppet/lib/puppet/parser/functions

Then a manifest to execute our new function:

$ cat /tmp/sha.pp
$hash = sha512("test")
notify { $hash: }

And finally run the function:

$ puppet /tmp/sha.pp
notice:
ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff
notice:
/Stage[main]//Notify[ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff]/message: defined 'message' as'ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff'

We can see that our notify resource returned a 512-bit hash generated by our sha512 function.

Note

You can call a function from another function by prefixing the function to be called with function_, for example function_notice.

Summary

In this chapter, you learned how to extend Puppet and Facter with your own custom types, providers, functions and facts. We demonstrated how to:

  • Configure Puppet to distribute your custom facts in your modules

  • Write your own custom facts

  • Test your new custom facts

  • Utilize two ensure-style types and providers

  • Use a ParsedFile type and provider to edit simple configuration files

  • Write Puppet functions

  • Test Puppet functions

There are also a lot of examples of extensions and additions to Puppet that are available for you to add to your Puppet installation, or which can serve as examples of how to develop particular extensions. A good place to start looking for these is on GitHub (http://www.github.com).

Resources

  • Adding custom facts http://puppetlabs.com/trac/puppet/wiki/AddingFacts

  • Try Ruby (http://tryruby.org/) online tutorial

  • Learn to Program tutorial (http://pine.fm/LearnToProgram/)

  • Programming Ruby (http://ruby-doc.org/docs/ProgrammingRuby/)

  • Beginning Ruby (http://beginningruby.org/).

  • Documentation on how to create custom types:http://docs.puppetlabs.com/guides/custom_types.html

  • A complete example of resource type creation:http://projects.puppetlabs.com/projects/puppet/wiki/Development_Complete_Resource_Example

  • Documentation on detailed provider development:http://projects.puppetlabs.com/projects/puppet/wiki/Development_Provider_Development

  • Practical set of documentation covering type development:http://projects.puppetlabs.com/projects/puppet/wiki/Development_Practical_Types

  • Writing your own functions:http://projects.puppetlabs.com/projects/1/wiki/Writing_Your_Own_Functions

  • Writing tests for Puppet:http://projects.puppetlabs.com/projects/puppet/wiki/Development_Writing_Tests

  • Try Ruby (http://tryruby.org/) online tutorial

  • Learn to Program tutorial(http://pine.fm/LearnToProgram/)

  • Programming Ruby (http://ruby-doc.org/docs/ProgrammingRuby/)

  • Beginning Ruby (http://beginningruby.org/).

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

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