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

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