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.
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.
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
.
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.
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.
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.
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 =~ /^(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.
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.
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.
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.
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:
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.
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.
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.
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, }
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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
$ 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.
You can call a function from another function by prefixing the function to be called with function_
, for example function_notice
.
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
).
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/
).
13.59.96.247