Authors: Jeffrey McCune James Turnbull
reports=store,log,summary
After we restarted the Puppet master and performed a Puppet run, the new report would be generated. In our case, the final report is contained in a file calledsummary.txt
and looks something like this:
Changes:
Total: 1
Events:
Success: 1
Total: 1
Resources:
Changed: 1
Out of sync: 1
Total: 8
Time:
Config retrieval: 0.19
File: 0.05
Filebucket: 0.00
Schedule: 0.00
Tip
You can see other examples of how to use and extract reporting data from the code of the existing reports, at
https://github.com/puppetlabs/puppet/tree/master/lib/puppet/reports
In this chapter, we've demonstrated the basics of Puppet reporting, including how to configure reporting and some details on each report type and its configuration.
We've also shown you how to create custom reports of your own, making use of the report data in its YAML form or via processing with a custom report processor.
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.
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 theroot
user) that are prefixed withFACTER_
will be added to Facter as facts. So, if you were to set an environmental variable ofFACTER_datacenter
with a value ofChicago
, then this would become a fact calleddatacenter
with the value ofChicago
.
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 calledfacts
orplugins
. 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 calledbind
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 thelib/facter
directory.
In addition to adding thelib/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'spuppet.conf
configuration file, as you can see on the next line:
[main]
pluginsync = true
When set to true, thepluginsync
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.
Caution
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
.
Listing 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 theHOME
environmental value as a fact calledhome
, which in turn would be available in our manifests as the variable$home
.
TheFacter.add
method allows us to specify the name of our new fact. We then use thesetcode
block to specify the contents of our new fact, in our case using Ruby's built-inENV
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.
Listing 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 aconfine
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 theoperatingsystem
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 previousconfine
is commonly used to limit the use of a particular fact to nodes with Linux-based kernels.
Second, we've used thereadlines
File method to read in the contents of the/etc/timezone
file. The contents are returned as the facttimezone
, 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 theoperatingsystemrelease
fact shown in
Listing 10-3
.
Listing 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, theoperatingsystem
fact returns the current kernel,Facter.value(:kernel)
, as the value ofoperatingsystem
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
.
Listing 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
}