Summarizing systems with Facter

Configuration management is quite a dynamic problem. In other words, the systems that need configuration are mostly moving targets. In some situations, system administrators or operators get lucky and work with large quantities of 100 percent uniform hardware and software. In most cases, however, the landscape of servers and other computing nodes is rather heterogeneous, at least in subtle ways. Even in unified networks, there are likely multiple generations of machines or operating systems, with small or larger differences required for their respective configurations.

For example, a common task for Puppet is to handle the configuration of system monitoring. Your business logic will likely dictate warning thresholds for gauges such as the system load value. However, those thresholds can rarely be static. On a two-processor virtual machine, a system load of 10 represents a crippling overload, while the same value can be absolutely acceptable for a busy DBMS server that has cutting edge hardware of the largest dimensions.

Another important factor can be software platforms. Your infrastructure might span multiple distributions of Linux or alternate operating systems such as BSD, Solaris, or Windows, each with different ways of handling certain scenarios. Imagine, for example, that you want Puppet to manage some content from the fstab file. On your rare Solaris system, you would have to make sure that Puppet targets the /etc/vfstab file instead of /etc/fstab.

Note

It is usually not a good idea to interact directly with the fstab file in your manifest. This example will be rounded off in the section concerning providers.

Puppet strives to present you with a unified way of managing all your infrastructure. It therefore needs a means to allow your manifests to adapt to different kinds of circumstances on the agent machines. This includes their operating system, hardware layout, and many other details. Keep in mind that, generally, the manifests have to be compiled on the master machine.

There are several conceivable ways to implement a solution for this particular problem. A direct approach would be to use a language construct that allows the master to send a piece of shell script (or other code) to the agent and receive its output in return.

Note

The following is pseudocode, however, there are no back tick expressions in the Puppet DSL:

if `grep -c ^processor /proc/cpuinfo` > 2 {
  $load_warning = 4
}
else {
  $load_warning = 2
}

This solution would be powerful but expensive. The master would need to call back to the agent whenever the compilation process encountered such an expression. Writing manifests that were able to cope with such a command returning an error code would be strenuous, and Puppet would likely end up resembling a quirky scripting engine.

Tip

When using puppet apply instead of the master, such a feature would pose less of a problem, and it is indeed available in the form of the generate function, which works just like the back ticks in the pseudocode mentioned previously. The commands are always run on the compiling node though, so this is less useful with an agent/master than apply.

Puppet uses a different approach. It relies on a secondary system called Facter, which has the sole purpose of examining the machine on which it is run. It serves a list of well-known variable names and values, all according to the system on which it runs. For example, an actual Puppet manifest that needs to form a condition upon the number of processors on the agent will use this expression:

if $::processorcount > 4 { … }

Facter's variables are called facts, and processorcount is such a fact. The fact values are gathered by the agent and sent to the master who will use these facts to compile a catalog. All fact names are available in the manifests as variables.

Note

Facts are available to manifests that are used with puppet apply too, of course. You can test this very simply:

puppet apply -e 'notify { "I am ${::fqdn} and have ${::processorcount} CPUs": }'

Accessing and using fact values

You have already seen the use of the processorcount fact in an example. In the manifest, each fact value is available as a global variable value. That is why you can just use the ::processorcount expression where you need it.

Note

You will often see conventional uses such as $::processorcount or $::ipaddress. Prefixing the fact name with double colons is highly recommended. The official Style Guide at https://docs.puppetlabs.com/guides/style_guide.html#namespacing-variables recommends this. The prefix indicates that you are referring to a variable delivered from Facter. Facter variables are put into the puppet master's top scope.

Some helpful facts have already been mentioned. The processorcount fact might play a role for your configuration. When configuring some services, you will want to use the machine's ipaddress value in a configuration file or as an argument value:

file { '/etc/mysql/conf.d/bind-address':
  ensure  => 'file',
  mode    => '0644',
  content => "[mysqld]\nbind-address=${::ipaddress}\n",
}

Besides the hostname, your manifest can also make use of the fully qualified domain name (FQDN) of the agent machine.

Note

The agent will use the value of its fqdn fact as the name of its certificate (clientcert) by default. The master receives both these values. Note that the agent can override the fqdn value to any name, whereas the clientcert value is tied to the signed certificate that the agent uses. Sometimes, you will want the master to pass sensitive information to individual nodes. The manifest must identify the agent by its clientcert fact and never use fqdn or hostname instead, for the reason mentioned. An example is shown in the following code:

file { '/etc/my-secret':
  ensure => 'file',
  mode   => '0600',
  owner  => 'root',
  source => "puppet:///modules/secrets/${::clientcert}/key",
}

There is a whole group of facts to describe the operating system. Each fact is useful in different situations. The operatingsystem fact takes values such as Debian or CentOS:

if $::operatingsystem != 'Ubuntu' {
  package { 'avahi-daemon':
    ensure => absent
  }
}

If your manifest will behave identically on RHEL, CentOS, and Fedora (but not on Debian and Ubuntu), you should make use of the osfamily fact instead:

if $::osfamily == 'RedHat' {
  $kernel_package = 'kernel'
}

The operatingsystemrelease fact allows you to tailor your manifests to differences between the versions of your OS:

if $::operatingsystem == 'Debian' {
  if versioncmp($::operatingsystemrelease, '7.0') >= 0 {
    $ssh_ecdsa_support = true
  }
}

Facts such as mac address, the different SSH host keys, fingerprints, and others make it easy to use Puppet for keeping inventory of your hardware. There are a slew of other useful facts. Of course, the collection will not suit every possible need of every user out there. That is why Facter comes readily extendible.

Extending Facter with custom facts

Technically, nothing is stopping you from adding your own fact code right next to the core facts, either by maintaining your own Facter package, or even by deploying the Ruby code files to your agents directly through Puppet management. However, Puppet offers a much more convenient alternative in the form of custom facts.

We have still not covered Puppet modules yet. They will be thoroughly introduced in Chapter 5, Extending Your Puppet Infrastructure with Modules. For now, just create a Ruby file at /etc/puppetlabs/code/environments/production/modules/hello_world/lib/facter/hello.rb on the master machine. Puppet will recognize this as a custom fact of the name, hello. (For Puppet 3 or older versions, the path should be /etc/puppet/modules/hello_world/lib/facter/hello.rb.)

The inner workings of Facter are very straightforward and goal oriented. There is one block of Ruby code for each fact, and the return value of the block becomes the fact value. Many facts are self-sufficient, but others will rely on the values of one or more basic facts. For example, the method to determine the IP address(es) of the local machine is highly dependent upon the operating system.

The hello fact is very simple though:

Facter.add(:hello) do
  setcode { "Hello, world!" }
end

The return value of the setcode block is the string, Hello, world!, and you can use this fact as $::hello in a Puppet manifest.

Note

Before Facter Version 2.0, each fact had a string value. If a code block returns another value, such as an array or hash, Facter 1.x will convert it to a string. The result is not useful in many cases. For this historic reason, there are facts such as ipaddress_eth0 and ipaddress_lo, instead of (or in addition to) a proper hash structure with interface names and addresses.

It is important for the pluginsync option to be enabled on the agent side. This has been the default for a long time and should not require any customization. The agent will synchronize all custom facts whenever checking in to the master. They are permanently available on the agent machine after that. You can then retrieve the hello fact from the command line using the following line:

# puppet facts | grep hello

By just invoking the following command without an argument, you request a list of all fact names and values.

# puppet facts

There is also a facter command. It does roughly the same as puppet facts, but will only show built in facts, not your custom facts.

Tip

In Puppet 3 and earlier, there was no puppet facts subcommand. You have to rely on the facter CLI (from Facter version 2.x or older) and call facter -p, to include custom facts. Facter 3.0 removed this parameter, although it has been announced that newer versions will support it again.

Extending Facter with custom facts

This book will not cover all aspects of Facter's API, but there is one facility that is quite essential. Many of your custom facts will only be useful on Unix-like systems, and others will only be useful on your Windows boxen. You can retrieve such values using a construct like the following:

if Facter.value(:kernel) != "windows"
  nil
else
  # actual fact code here
end

This would be quite tedious and repetitive though. Instead, you can invoke the confine method within the Facter.add(name) { … } block:

Facter.add(:msvs_version) do
  confine :kernel => :windows
  setcode do
    # …
  end
end

You can confine a fact to several alternative values as well:

confine :kernel => [ :linux, :sunos ]

Finally, if a fact does make sense in different circumstances, but requires drastically different code in each respective case, you can add the same fact several times, each with a different set of confine values. Core facts such as ipaddress use this often:

Facter.add(:ipaddress) do
  confine :kernel => :linux
  …
end
Facter.add(:ipaddress) do
  confine :kernel => %w{FreeBSD OpenBSD Darwin DragonFly}
  …
end
…

You can confine facts based on any combination of other facts, not just kernel. It is a very popular choice though. The operatingsystem or osfamily facts can be more appropriate in certain situations. Technically, you can even confine some of your facts to certain processorcount values and so forth.

Simplifying things using external facts

If writing and maintaining Ruby code is not desirable in your team for any reason, you might prefer to use an alternative that allows shell scripts, or really, any kind of programming language, or even static data with no programming involved at all. Facter allows this in the form of external facts.

Creating an external fact is similar to the process for regular custom facts, with the following distinctions:

  • Facts are produced by standalone executables or files with static data, which the agent must find in /etc/puppetlabs/facter/facts.d/
  • The data is not just a string value, but an arbitrary number of key=value pairs instead

The data need not use the ini file notation style—the key/value pairs can also be in the YAML or JSON format. The following external facts hold the same data:

# site-facts.txt
workgroup=CT4Site2
domain_psk=nm56DxLp%

The facts can be written in the YAML format in the following way:

# site-facts.yaml
workgroup: CT4Site2
domain_psk: nm56DxLp%

In the JSON format, facts can be written as follows:

# site-facts.json
{ 'workgroup': 'CT4Site2', 'domain_psk': 'nm56DxLp%' }

The deployment of the external facts works simply through file resources in your Puppet manifest:

file { '/etc/puppetlabs/facter/facts.d/site-facts.yaml':
  ensure => 'file',
  source => 'puppet:///…',
}

Note

With newer versions of Puppet and Facter, external facts will be automatically synchronized, just like custom facts if they are found in facts.d/* in any module (for example, /etc/puppetlabs/code/environments/production/modules/hello_world/facts.d/hello.sh). This is not only more convenient, but has a large benefit; when Puppet must fetch an external fact through a file resource instead, its fact value(s) are not available while the catalog is being compiled. The pluginsync mechanism, on the other hand, makes sure that all synced facts are available before manifest compilation starts.

When facts are not static and cannot be placed in a txt or YAML file, you can make the file executable and add a shebang instead. It will usually be a shell script, but the implementation is of no consequence; it is just important that properly formatted data is written to the standard output. You can simplify the hello fact this way, in /etc/puppetlabs/code/environments/production/modules/hello_world/facts.d/hello.sh:

#!/bin/sh

echo hello=Hello, world\!

For executable facts, the ini styled key=value format is the only supported one. YAML or JSON are not eligible in this context.

Note

Facter 2 introduced structured facts. Structured facts return an array or a hash. In older Puppet versions (prior to 3.4), structured facts have to be enabled in puppet.conf by setting stringify_facts to false. This setting is default for Puppet 4.0 and later versions.

Goals of Facter

The whole structure and philosophy of Facter serves the goal of allowing for platform-agnostic usage and development. The same collection of facts (roughly) is available on all supported platforms. This allows Puppet users to keep a coherent development style through manifests for all those different systems.

Facter forms a layer of abstraction over the characteristics of both hardware and software. It is an important piece of Puppet's platform-independent architecture. Another piece that was mentioned before is the type and provider subsystem. Types and providers are explored in greater detail in the following sections.