Facts are the most comfortable kind of variables to work with:
Out-of-the-box, depending on Facter version and the underlying OS, they give us:
architecture bios_* board* memory* processor virtual
)kernel* osfamily operatingsystem* lsb* sp_* selinux ssh* timezone uptime*
)hostname domain fqdn interfaces ipaddress_* macaddress_* network*
)blockdevice_* filesystems swap*
)facterversion puppetversion ruby*
)We already use some of these facts in modules to manage the right resources to apply for our operating systems, and we already classify nodes according to their hostname.
There's a lot more that we can do with them, we can create custom facts useful to:
dc env region role node_number
and so on)php_version mysql_cluster glassfish_registered
and so on)apache_error_rate
, network_connections
)Just think about any possible command we can run on our servers and how its output might be useful in our manifests to model the resources we provide to our servers.
We can write custom facts in two ways:
pluginsync
in our modules/etc/facter/facts.d
directory where we can place plain text, JSON, YAML files, or executable scriptsWe can create a custom fact editing, in one of our modules, files called lib/facter/<fact_name>.rb
, for example, a fact named role
should be placed in a file called lib/facter/role.rb
and may have content as follows:
require 'facter'
We require the facter
class and then we use the add
method for our custom fact called role
:
Facter.add("role") do
We can restrict the execution of this fact only to specific hosts, according to any other facts' values. This is done using the confine
statement, here based on the kernel
fact:
confine :kernel => [ 'Linux' , 'SunOS' , 'FreeBSD' , 'Darwin' ]
We can also set a maximum number, in seconds, to wait for its execution:
timeout = 10
We can even have different facts with the same name and give a weight to each of them; facts with higher weight value are executed first and facts with lower weight are executed only if no value is already returned:
has_weight = 40
We use the setcode
method of the Facter
class to define what our fact does; what is returned from the block of code contained here (between do
and end
) is the value of our fact:
setcode do
We can have access to other facts values with Facter.value
, in our case, the role value is a simple extrapolation of the hostname
(basically the hostname without numbers). If we have different naming schemes for our nodes and we can deduce their role from their name, we can easily use other Ruby string functions to extrapolate it:
host = Facter.value(:hostname) host.gsub(/d|W/,"") end end
Often, the output of a fact is simply the execution of a command; for this case, there is a specific wrapper method called Facter::Util::Resolution.exec
, which expects the command to execute as a parameter.
The following fact, called last_run
, simply contains the output of the command date:
require 'facter' Facter.add("last_run") do confine :kernel => [ 'Linux' , 'SunOS' , 'FreeBSD' , 'Darwin' ] setcode do Facter::Util::Resolution.exec('date') end end
We can have different facts, with different names, on the same Ruby file, and we can also provide dynamic facts, for example, the following code creates one different fact, returning the installed version for each package. Here, the confinement according to the osfamily
is done outside the fact definition:
if Facter.osfamily == 'RedHat' IO.popen('yum list installed').readlines.collect do |line| array = line.split Facter.add("#{array[0]}_version") do setcode do "#{array[1]}" end end end end if Facter.osfamily == 'Debian' IO.popen('dpkg -l').readlines.collect do |line| array = line.split Facter.add("#{array[1]}_version") do setcode do "#{array[2]}" end end end end
Remember that to have access, from the shell, to the facts we distribute via pluginsync
, we need to use the --puppet(-p)
argument:
facter -p
Otherwise, we need to set the FACTERLIB
environment variable pointing to the lib
directory of the module that provides it, for example:
export FACTERLIB=/opt/puppetlabs/puppet/cache/lib/facter
Puppet Labs's stdlib
module provides a very powerful addendum to the facts, a feature called external facts, which has proven to be so useful to deserve inclusion directly into core Facter since version 1.7.
We can define new facts without even the need to write Ruby code, just by placing files in the /opt/puppetlabs/facter/facts.d/
or /etc/facter/facts.d/
directory (/etc/puppetlabs/facter/facts.d
with Puppet Enterprise and C:ProgramDataPuppetLabsfacterfacts.d
on Windows).
These files can be simple .txt
files such as /etc/facter/facts.d/node.txt
, with facts declared with the following syntax:
role=webserver env=prod
YAML files such as /etc/facter/facts.d/node.yaml
, which for the same sample facts would appear like this:
--- role: webserver env: prod
Also, JSON files files such as /etc/facter/facts.d/node.json
with:
{ 'role': 'webserver', 'env': 'prod' }
We can also place plain commands in any language; on Unix, any executable file present in /etc/facter/facts.d/
is run and is expected to return the facts' values with an output like this:
role=webserver env=prod
On Windows, we can place files with .com
, .exe
, .bat
, .cmd
, or .ps1
extensions.
Since Puppet 3.4.0, external facts can also be automatically propagated to clients with pluginsync
; in this case, the directory synced is <modulename>/facts.d
(note that this is at the same level of the lib
directory).
Alternatively, we can use other methods to place our external facts, for example, in post installation scripts during the initial provisioning of a server, or using directly Puppet's file resources, or having them generated by custom scripts or cron jobs.