While managing a complex environment, facts can be used to bring order out of chaos. If your manifests have large case
statements or nested if
statements, a custom fact might help in reducing the complexity or allow you to change your logic.
When you work in a large organization, keeping the number of facts to a minimum is important, as several groups may be working on the same system and thus interaction between the users may adversely affect one another's work or they may find it difficult to understand how everything fits together.
As we have already seen in the previous chapter, if our facts are simple text values that are node specific, we can just use the facts.d
directory of stdlib
to create static facts that are node specific.
This facts.d
mechanism is included, by default, on Facter versions 1.7 and higher and is referred to as external fact.
We will be creating some custom facts; therefore, we will create our Ruby files in the module_name/lib/facter
directory. While designing your facts, choose names that are specific to your organization. Unless you plan on releasing your modules on the Forge, avoid calling your fact something similar to a predefined fact or using a name that another developer might use. The names should be meaningful and specific—a fact named foo
is probably not a good idea. Facts should be placed in the specific module that requires them. Keeping the fact name related to the module name will make it easier to determine where the fact is being set later.
For our example.com
organization, we'll create a module named example_facts
and place our first fact in there. As the first example, we'll create a fact that returns 1
(true) if the node is running the latest installed kernel or 0
(false) if not. As we don't expect this fact to become widely adopted, we'll call it example_latestkernel
. The idea here is that we can apply modules to nodes that are not running the latest installed kernel, such as locking them down or logging them more closely.
To begin writing the fact, we'll start writing a Ruby script; you can also work in IRB while you're developing your fact. Interactive Ruby (IRB) is like a shell to write the Ruby code, where you can test your code instantly. Version 4 of Puppet installs its own Ruby, so our fact will need to use the Ruby installed by Puppet (/opt/puppetlabs/puppet/bin/ruby
). Our fact will use a function from Puppet, so we will require puppet
and facter
. The fact scripts are run from within Facter so that the require
lines are removed once we are done with our development work. The script is written, as follows:
#!/opt/puppetlabs/puppet/bin/ruby require 'puppet' require 'facter' # drop alpha numeric endings def sanitize_version (version) temp = version.gsub(/.(el5|el6|el7|fc19|fc20)/,'') return temp.gsub(/.(x86_64|i686|i586|i386)/,'') end
We define a function to remove textual endings on kernel versions and architectures. Textual endings, such as el5
and el6
will make our version comparison return incorrect results. For example, 2.6.32-431.3.1.el6
is less than 2.6.32-431.el6
because the e
in el6
is higher in ASCII than 3
. Our script will get simplified greatly, if we simply remove known endings. We then obtain a list of installed kernel packages; the easiest way to do so is with rpm, as shown here:
kernels = %x( rpm -q kernel --qf '%{version}-%{release} ' ) kernels = sanitize_version(kernels) latest = ''
We will then set the latest
variable to empty and we'll loop through the installed kernels by comparing them to latest
. If their values are greater than latest
, then we convert latest
such that it is equal to the value of the kernels. At the end of the loop, we will have the latest
(largest version number) kernel in the variable. For kernel in kernels, we will use the following commands:
for kernel in kernels.split(' ') kernel=kernel.chomp() if latest == '' latest = kernel end if Puppet::Util::Package.versioncmp(kernel,latest) > 0 latest = kernel end end
We use versioncmp
from puppet::util::package
to compare the versions. I've included a debugging statement in the following code that we will remove later. At the end of this loop, the latest
variable contains the largest version number and the latest installed kernel:
kernelrelease = Facter.value('kernelrelease') kernelrelease = sanitize_version(kernelrelease)
Now, we will ask Facter for the value of kernelrelease
. We don't need to run uname
or a similar tool, as we'll rely on Facter to get the value using the Facter.value('kernelrelease')
command. Here, Facter.value()
returns the value of a known fact. We will also run the result of Facter.value()
through our sanitize_version
function to remove textual endings. We will then compare the value of kernelrelease
with latest
and update the kernellatest
variable accordingly:
if Puppet::Util::Package.versioncmp(kernelrelease,latest) == 0 kernellatest = 1 else kernellatest = 0 end
At this point, kernellatest
will contain the value 1
if the system is running the installed kernel with latest
and 0
if not. We will then print some debugging information to confirm whether our script is doing the right thing, as shown here:
print "running kernel = %s " % kernelrelease print "latest installed kernel = %s " % latest print "kernellatest = %s " % kernellatest
We'll now run the script on node1
and compare the results with the output of rpm -q kernel
to check whether our fact is calculating the correct value:
[samdev@standfacter]$ rpm -q kernel kernel-3.10.0-229.11.1.el7.x86_64 kernel-3.10.0-229.14.1.el7.x86_64 [samdev@standfacter]$ ./latestkernel.rb 3.10.0-229.11.1.el7 3.10.0-229.14.1.el7 running kernel = 3.10.0-229.11.1.el7 latest installed kernel = 3.10.0-229.11.1.el7 3.10.0-229.14.1.el7 kernellatest = 0
Now that we've verified that our fact is doing the right thing, we need to call Facter.add()
to add a fact to Facter. The reason behind this will become clear in a moment, but we will place all our code within the Facter.add
section, as shown in the following code:
Facter.add("example_latestkernel") do kernels = %x( rpm -q kernel --qf '%{version}-%{release} ' ) ... end Facter.add("example_latestkernelinstalled") do setcode do latest end end
This will add two new facts to Facter. We now need to go back and remove our require
lines and print
statements. The complete fact should look similar to the following script:
# drop alpha numeric endings def sanitize_version (version) temp = version.gsub(/.(el5|el6|el7|fc19|fc20)/,'') return temp.gsub(/.(x86_64|i686|i586|i386)/,'') end Facter.add("example_latestkernel") do kernels = %x( rpm -q kernel --qf '%{version}-%{release} ' ) kernels = sanitize_version(kernels) latest = '' for kernel in kernels do kernel=kernel.chomp() if latest == '' latest = kernel end if Puppet::Util::Package.versioncmp(kernel,latest) > 0 latest = kernel end end kernelrelease = Facter.value('kernelrelease') kernelrelease = sanitize_version(kernelrelease) if Puppet::Util::Package.versioncmp(kernelrelease,latest) == 0 kernellatest = 1 else kernellatest = 0 end setcode do kernellatest end Facter.add("example_latestkernelinstalled") do setcode do latest end end end
Now, we need to create a module of our Git repository on stand
and have that checked out by client to see the fact in action. Switch back to the samdev
account to add the fact to Git as follows:
[Thomas@stand ~]$ sudo –iu samdev [samdev@stand]$ cd control/dist [samdev@stand]$ mkdir -p example_facts/lib/facter [samdev@stand]$ cd example_facts/lib/facter [samdev@stand]$ cp ~/latestkernel.rbexample_latestkernel.rb [samdev@stand]$ git add example_latestkernel.rb [samdev@stand]$ git commit -m "adding first fact to example_facts" [masterd42bc22] adding first fact to example_facts 1 files changed, 33 insertions(+), 0 deletions(-) create mode 100755 dist/example_facts/lib/facter/example_latestkernel.rb [samdev@stand]$ git push origin … To /var/lib/git/control.git/ fc4f2e5..55305d8 production -> production
Now, we will go back to client, run Puppet agent, and see that example_latestkernel.rb
is placed in /opt/puppetlabs/puppet/cache/lib/facter/example_latestkernel.rb
so that Facter can now use the new fact.
This fact will be in the /dist
folder of the environment. In the previous chapter, we added /etc/puppet/environments/$environment/dist
to modulepath
in puppet.conf
; if you haven't done this already, do so now:
[root@client ~]# puppet agent -t … Notice: /File[/opt/puppetlabs/puppet/cache/lib/facter/example_latestkernel.rb]/ensure: defined content as '{md5}579a2f06068d4a9f40d1dadcd2159527'… Notice: Finished catalog run in 1.18 seconds [root@client ~]# facter -p |grep ^example example_latestkernel => 1 example_latestkernelinstalled => 3.10.0-123
Now, this fact works fine for systems that use rpm for package management; it will not work on an apt system. To ensure that our fact doesn't fail on these systems, we can use a confine
statement to confine the fact calculation to systems where it will succeed. We can assume that our script will work on all systems that report RedHat
for the osfamily
fact, so we will confine ourselves to that fact.
For instance, if we run Puppet on a Debian-based node to apply our custom fact, it fails when we run Facter, as shown here:
# cat /etc/debian_version wheezy/sid # facter -p example_latestkernelinstalled sh: 1: rpm: not found Could not retrieve example_latestkernelinstalled: undefined local variable or method `latest' for #<Facter::Util::Resolution:0xb6bd386c>
Now, if we add a confine
statement to confine the fact to nodes in which osfamily
is RedHat
, it doesn't happen, as shown here:
Facter.add("example_latestkernel") do confine :osfamily => 'RedHat' … end Facter.add("example_latestkernelinstalled") do confine :osfamily => 'RedHat' setcode do latest end end
When we run Facter on the Debian node again, we will see that the fact is simply not defined, as shown here:
# facter -p example_latestkernelinstalled ##
This simple example creates two facts that can be used in modules. Based on this fact you can, for instance, add a warning to motd
to say that the node needs to reboot.
While implementing a custom fact such as this, every effort should be made to ensure that the fact doesn't break Facter compilation on any OSes within your organization. Using confine
statements is one way to ensure your facts stay where you designed them.
So, why not just use the external fact (/etc/facter/facts.d
) mechanism all the time? We could have easily written the previous fact script in bash and put the executable script in /etc/facter/facts.d
. Indeed, there is no problem in doing it that way. The problem with using the external fact mechanism is timing and precedence. The fact files placed in lib/facter
are synced to nodes when pluginsync
is set to true
, so the custom fact is available for use during the initial catalog compilation. If you use the external fact mechanism, you have to send your script or text file to the node during the agent run so that the fact isn't available until after the file has been placed there (after the first run, any logic built around that fact will be broken until the next Puppet run). The second problem is preference. External facts are given a very high weight by default. Weight in the Facter world is used to determine when a fact is calculated and facts with low weight are calculated first and cannot be overridden by facts with higher weight.
One great use case for external facts is having a system task (something that runs out of cron perhaps) that generates the text file in /etc/facter/facts.d
. Initial runs of Puppet agent won't see the fact until after cron runs the script, so you can use this to trigger further configuration by having your manifests key off the new fact. As a concrete example, you can have your node installed as a web server for a load-balancing cluster as a part of the modules that run a script from cron to ensure that your web server is up and functioning and ready to take a part of the load. The cron script will then define a load_balancer_ready=true
fact. It will then be possible to have the next Puppet agent run and add the node to the load balancer configuration.
The most useful custom facts are those that return a calculated value that you can use to organize your nodes. Such facts allow you to group your nodes into smaller groups or create groups with similar functionality or locality. These facts allow you to separate the data component of your modules from the logic or code components. This is a common theme that will be addressed again in Chapter 9, Roles and Profiles. This can be used in your hiera.yaml
file to add a level to the hierarchy. One aspect of the system that can be used to determine information about the node is the IP address. Assuming that you do not reuse the IP addresses within your organization, the IP address can be used to determine where or in which part a node resides on a network, specifically, the zone. In this example, we will define three zones in which the machines reside: production, development, and sandbox. The IP addresses in each zone are on different subnets. We'll start by building a script to calculate the zone and then turn it into a fact similar to our last example. Our script will need to calculate IP ranges using netmasks, so we'll import the ipaddr
library and use the IPAddr
objects to calculate ranges:
require('ipaddr') require('facter') require('puppet')
Next, we'll define a function that takes an IP address as the argument and returns the zone to which that IP address belongs:
def zone(ip) zones = { 'production' => [IPAddr.new('10.0.2.0/24'),IPAddr.new('192.168.124.0/23')], 'development' => [IPAddr.new('192.168.123.0/24'),IPAddr.new('192.168.126.0/23')], 'sandbox' => [IPAddr.new('192.168.128.0/22')] } for zone in zones.keys do for subnet in zones[zone] do ifsubnet.include?(ip) return zone end end end return 'undef' end
This function will loop through the zones looking for a match on the IP address. If no match is found, the value of undef
is returned. We then obtain an IP address for the machine that is using the IP address fact from Facter:
ip = IPAddr.new(Facter.value('ipaddress'))
Then, we will call the zone
function with this IP address to obtain the zone:
print zone(ip)," "
Now, we can make this script executable and test it:
[root@client ~]# facter ipaddress 10.0.2.15 [root@client ~]# ./example_zone.rb production
Now, all we have to do is replace print zone(ip),"
"
with the following code to define the fact:
Facter.add('example_zone') do setcode do zone(ip) end end
Now, when we insert this code into our example_facts
module and run Puppet on our nodes, the custom fact is available:
[root@client ~]# facter -p example_zone production
Now that we can define a zone based on a custom fact, we can go back to our hiera.yaml
file and add %{::example_zone}
to the hierarchy. The hiera.yaml
hierarchy will now contain the following:
--- :hierarchy: - "zones/%{::example_zone}" - "hosts/%{::hostname}" - "roles/%{::role}" - "%{::kernel}/%{::osfamily}/%{::lsbmajdistrelease}" - "is_virtual/%{::is_virtual}" - common
After restarting puppetserver
to have the hiera.yaml
file reread, we create a zones
directory in hieradata
and add production.yaml
with the following content:
--- welcome: "example_zone - production"
Now when we run Puppet on our node1
, we will see motd
updated with the new welcome message, as follows:
[root@client ~]# cat /etc/motd example_zone - production Managed Node: client Managed by Puppet version 4.2.2
Creating a few key facts that can be used to build up your hierarchy can greatly reduce the complexity of your modules. There are several workflows available, in addition to the custom fact we described earlier. You can use the /etc/facter/facts.d
(or /etc/puppetlabs/facter/facts.d
) directory with static files or scripts, or you can have tasks run from other tools that dump files into that directory to create custom facts.
While writing Ruby scripts, you can use any other fact by calling Facter.value('factname')
. If you write your script in Ruby, you can access any Ruby library using require
. Your custom fact can query the system using lspci
or lsusb
to determine which hardware is specifically installed on that node. As an example, you can use lspci
to determine the make and model of graphics card on the machine and return that as a fact, such as videocard
.
18.119.160.181