Custom facts

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.

Creating custom facts

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
##

Note

In the previous command, the prompt is returned without an error, and the confine statements prevent the fact from being defined, so there is no error to return.

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.

Note

If you want to become really popular at work, have the node turn off SSH until it's running the latest kernel in the name of security.

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.

Note

Weights are often used when a fact can be determined by one of the several methods. The preferred method is given the lowest weight. If the preferred method is unavailable (due to a confine), then the next higher weight fact is tried.

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.

Creating a custom fact for use in Hiera

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.

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

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