Custom facts overview

You can add custom facts by writing snippets of Ruby code on the primary Puppet server. Puppet then uses plug-ins in modules to distribute the facts to the client.

For information on how to add custom facts to modules, see Module plug-in types.

Adding custom facts to Facter

Sometimes you need to be able to write conditional expressions based on site-specific data that just isn’t available via Facter, or perhaps you’d like to include it in a template.

Because you can’t include arbitrary Ruby code in your manifests, the best solution is to add a new fact to Facter. These additional facts can then be distributed to Puppet clients and are available for use in manifests and templates, just like any other fact is.

Note: Facter 3.0 removed the Ruby implementations of some features and replaced them with a custom facts API. Any custom fact that requires one of the Ruby files previously stored in lib/facter/util fails with an error.

Structured and flat facts

A typical fact extracts a piece of information about a system and returns it as either as a simple value (“flat” fact) or data organized as a hash or array (“structured” fact). There are several types of facts classified by how they collect information, including:

  • Core facts, which are built into Facter and are common to almost all systems.

  • Custom facts, which run Ruby code to produce a value.

  • External facts, which return values from pre-defined static data, or the result of an executable script or program.

All fact types can produce flat or structured values.

Loading custom facts

Facter offers multiple methods of loading facts.

These include:

  • $LOAD\_PATH, or the Ruby library load path.

  • The --custom-dir command line option.

  • The environment variable FACTERLIB.

You can use these methods to do things like test files locally before distributing them, or you can arrange to have a specific set of facts available on certain machines.

Using the Ruby load path

Facter searches all directories in the Ruby $LOAD_PATH variable for subdirectories named Facter, and loads all Ruby files in those directories. If you had a directory in your $LOAD_PATH like ~/lib/ruby, set up like this:
#~/lib/ruby
└── facter
    ├── rackspace.rb
    ├── system_load.rb
    └── users.rb

Facter loads facter/system_load.rb, facter/users.rb, and facter/rackspace.rb.

Using the --custom-dir command line option

Facter can take multiple --custom-dir options on the command line that specifies a single directory to search for custom facts. Facter attempts to load all Ruby files in the specified directories. This allows you to do something like this:
$ ls my_facts
system_load.rb
$ ls my_other_facts
users.rb
$ facter --custom-dir=./my_facts --custom-dir=./my_other_facts system_load users
system_load => 0.25
users => thomas,pat

Using the FACTERLIB environment variable

Facter also checks the environment variable FACTERLIB for a delimited (semicolon for Windows and colon for all other platforms) set of directories, and tries to load all Ruby files in those directories. This allows you to do something like this:
$ ls my_facts
system_load.rb
$ ls my_other_facts
users.rb
$ export FACTERLIB="./my_facts:./my_other_facts"
$ facter system_load users
system_load => 0.25
users => thomas,pat

Two parts of every fact

Most facts have at least two elements.

  1. A call to Facter.add('fact_name'), which determines the name of the fact.

  2. A setcode statement for simple resolutions, which is evaluated to determine the fact’s value.

Facts can get a lot more complicated than that, but those two together are the most common implementation of a custom fact.

Executing shell commands in facts

Puppet gets information about a system from Facter, and the most common way for Facter to get that information is by executing shell commands.

You can then parse and manipulate the output from those commands using standard Ruby code. The Facter API gives you a few ways to execute shell commands:
  • To run a command and use the output verbatim, as your fact’s value, you can pass the command into setcode directly. For example: setcode 'uname --hardware-platform'

  • If your fact is more complicated than that, you can call Facter::Core::Execution.execute('uname --hardware-platform') from within the setcode do ... end block. Whatever the setcode statement returns is used as the fact’s value.

  • Your shell command is also a Ruby string, so you need to escape special characters if you want to pass them through.

Note: Not everything that works in the terminal works in a fact. You can use the pipe (|) and similar operators as you normally would, but Bash-specific syntax like if statements do not work. The best way to handle this limitation is to write your conditional logic in Ruby.

Example

To get the output of uname --hardware-platform to single out a specific type of workstation, you create a custom fact.
  1. Start by giving the fact a name, in this case, hardware_platform.

  2. Create the fact in a file called hardware_platform.rb on the primary Puppet server:
    # hardware_platform.rb
    
    Facter.add('hardware_platform') do
      setcode do
        Facter::Core::Execution.execute('/bin/uname --hardware-platform')
      end
    end
  3. Use the instructions in the Plug-ins in modules docs to copy the new fact to a module and distribute it. During your next Puppet run, the value of the new fact is available to use in your manifests and templates.

Using other facts

You can write a fact that uses other facts by accessing Facter.value('somefact'). If the fact fails to resolve or is not present, Facter returns nil.

For example:
Facter.add('osfamily') do
  setcode do
    distid = Facter.value('lsbdistid')
    case distid
    when /RedHatEnterprise|CentOS|Fedora/
      'redhat'
    when 'ubuntu'
      'debian'
    else
      distid
    end
  end
end

Configuring facts

Facts have properties that you can use to customize how they are evaluated.

Confining facts

One of the more commonly used properties is the confine statement, which restricts the fact to run only on systems that match another given fact.

For example:
Facter.add('powerstates') do
  confine kernel: 'Linux'
  setcode do
    Facter::Core::Execution.execute('cat /sys/power/states')
  end
end
This fact uses sysfs on Linux to get a list of the power states that are available on the given system. Because this is available only on Linux systems, we use the confine statement to ensure that this fact isn’t needlessly run on systems that don’t support this type of enumeration.
To confine structured facts like ['os']['family'], you can use Facter.value: You can also use a Ruby block:
confine 'os' do |os|
  os['family'] == 'RedHat'
end

Fact precedence

A single fact can have multiple resolutions, each of which is a different way of determining the value of the fact. It’s common to have different resolutions for different operating systems, for example. To add a new resolution to a fact, you add the fact again with a different setcode statement.

When a fact has more than one resolution, the first resolution that returns a value other than nil sets the fact’s value. The way that Facter decides the issue of resolution precedence is the weight property. After Facter rules out any resolutions that are excluded because of confine statements, the resolution with the highest weight is evaluated first. If that resolution returns nil, Facter moves on to the next resolution (by descending weight) until it gets a value for the fact.

By default, the weight of a resolution is the number of confine statements it has, so that more specific resolutions take priority over less specific resolutions. External facts have a weight of 1000 — to override them, set a weight above 1000.
# Check to see if this server has been marked as a postgres server
Facter.add('role') do
  has_weight 100
  setcode do
    if File.exist? '/etc/postgres_server'
      'postgres_server'
    end
  end
end

# Guess if this is a server by the presence of the pg_create binary
Facter.add('role') do
  has_weight 50
  setcode do
    if File.exist? '/usr/sbin/pg_create'
      'postgres_server'
    end
  end
end

# If this server doesn't look like a server, it must be a desktop
Facter.add('role') do
  setcode do
    'desktop'
  end
end

Execution timeouts

Although this version of Facter does not support overall timeouts on resolutions, you can pass a timeout to Facter::Core::Execution#execute:
Facter.add('sleep') do
  setcode do
    begin
      Facter::Core::Execution.execute('sleep 10', options = {:timeout => 5})
      'did not timeout!'
    rescue Facter::Core::Execution::ExecutionFailure
      Facter.warn("Sleep fact timed out!")
    end
  end
end
When Facter runs as standalone, using Facter.warn ensures that the message is printed to STDERR. When Facter is called as part of a catalog application, using Facter.warn prints the message to Puppet’s log. If an exception is not caught, Facter automatically logs it as an error.

Structured facts

Structured facts take the form of either a hash or an array.

To create a structured fact, return a hash or an array from the setcode statement.

You can see some relevant examples in the Writing structured facts section of the Custom facts overview.

Aggregate resolutions

If your fact combines the output of multiple commands, use aggregate resolutions. An aggregate resolution is split into chunks, each one responsible for resolving one piece of the fact. After all of the chunks have been resolved separately, they’re combined into a single flat or structured fact and returned.

Aggregate resolutions have several key differences compared to simple resolutions, beginning with the fact declaration. To introduce an aggregate resolution, add the :type => :aggregate parameter:
Facter.add('fact_name', :type => :aggregate) do
    #chunks go here
    #aggregate block goes here
end
Each step in the resolution then gets its own named chunk statement:
chunk('one') do
    'Chunk one returns this. '
end

chunk('two') do
    'Chunk two returns this.'
end
Aggregate resolutions never have a setcode statement. Instead, they have an optional aggregate block that combines the chunks. Whatever value the aggregate block returns is the fact’s value. Here’s an example that just combines the strings from the two chunks above:
aggregate do |chunks|
  result = ''

  chunks.each_value do |str|
    result += str
  end

  # Result: "Chunk one returns this. Chunk two returns this."
  result
end
If the chunk blocks all return arrays or hashes, you can omit the aggregate block. If you do, Facter merges all of your data into one array or hash and uses that as the fact’s value.

For more examples of aggregate resolutions, see the Aggregate resolutions section of the Custom facts overview page.

Viewing fact values

If your Puppet primary servers are configured to use PuppetDB, you can view and search all of the facts for any node, including custom facts.

See the PuppetDB docs for more info.