This is the first in a series of three posts covering the concepts and best practices for extending Puppet using custom facts, custom functions and custom types and providers.
Part 1 (this post) explains custom facts and how a node can provide information to the Puppet Server.
Part 2 will focus on custom functions,detailing how to develop data processing or execution functions.
Part 3 will cover custom types and providers, extending Puppet DSL functionality.
Custom Facts:
There are several reasons to develop custom facts:
- Whenever a VM naming model is used, we recommend providing the naming schema as a custom fact.
- It is sometimes necessary to determine the state of certain configurations or whether specific software is installed.
- The datacenter department may need an overview of physical hardware, hardware vendor details, and end-of-life (EOL) support dates. Additionally, the finance department may require information on the number of commercial licenses in use and the versions of installed software.
Whenever information from a node is needed, Puppet offers the option to deploy custom facts.
Content:
- Fact Development
- Facts in Modules
- General API
- Confining
- Helpers
- Accessing Other Facts
- Return Values
- Windows Facts
- Additional Concepts
- Coding Strategies and Structured Data
- Summary
Fact Development
You can develop custom facts locally on a workstation or on the system where the fact is needed. Before adding the untested fact to your Puppet code, you can specify the fact load path in two ways.
The following directory structure is used:
ls ~/new_fact/
betadots_application_version.rb
To execute the fact, use one of the following commands:
FACTERLIB=~/new_fact facter betadots_application_version
facter --custom-dir ~/new_fact betadots_application_version
Facts in Modules
Puppet provides an API to develop custom facts. To ensure the Puppet agent finds them, custom facts must be placed in a specific directory within a module.
All files in the lib
directory of modules are synchronized to all agents. It’s not possible to restrict which files are synchronized.
Custom facts should be placed in the lib/facter
directory of a module. Although you can choose any filename, it's recommended to use the fact name as the filename for easy identification. The filename must end with .rb
.
We also recommend prefixing the custom fact name with a company or department identifier.
General API
Facter provides an API to create new custom facts.
Example:
# ~/new_fact/betadots_application_version.rb
Facter.add(:betadots_application_version) do
setcode do
# Code here
end
end
The setcode do ... end
block contains all the Ruby code for the fact. The result of the last command or variable is used as the fact’s return value.
Confining
Since all custom facts are distributed to all Puppet agents, it’s essential to ensure that OS- or application-specific facts are only executed where necessary.
This is achieved by confining facts.
Confinement can be applied to simple or structured facts or any other Ruby code, such as File.exist?
.
Simple fact confinement:
# ~/new_fact/betadots_application_version.rb
Facter.add(:betadots_application_version) do
confine kernel: 'Linux'
setcode do
# Code here
end
end
Confinement using Ruby block and facts:
# ~/new_fact/betadots_application_version.rb
Facter.add(:betadots_application_version) do
confine 'os' do |os|
os['family'] == 'RedHat'
end
setcode do
# Code here
end
end
Confinement using Ruby block and Ruby code:
# ~/new_fact/betadots_application_version.rb
Facter.add(:betadots_application_version) do
confine do
File.exist?('/etc/sysconfig/application')
end
setcode do
# Code here
end
end
Helpers
Facter provides several helper methods that can be used without requiring the facter
class:
Helper | Description |
---|---|
Facter::Core::Execution::execute |
Runs an executable on the system with a timeout option to prevent hanging facter runs. |
Facter::Core::Execution::which |
Checks if an executable is available. Works on any supported OS, searching the default paths ENV['PATH'] + ['/sbin', '/usr/sbin']
|
Facter.value |
Provides access to the value of any other fact. Be careful to avoid loops! |
More information is available in the API documentation.
Please note that Facter::Core::Execution::exec
has been deprecated in favor of Facter::Core::Execution::execute
. This is important when migrating from older versions of Facter.
The timeout
options hash can be set in two ways:
Valid for the whole fact:
Facter.add('<name>', {timeout: 5}) do
...
end
Per execute method
Facter::Core::Execution::execute('<cmd>', options = {:timeout => 5})
For example, if an application returns its configuration via /opt/app/bin/app config
, you can set a timeout of 5 seconds:
# ~/new_fact/betadots_application_version.rb
Facter.add(:betadots_application_version) do
confine do
File.exist?('/etc/sysconfig/application')
end
setcode do
Facter::Core::Execution.execute('/opt/app/bin/app config', {:timeout => 5})
end
end
The result of the last command or a variable with content is used as fact result.
# ~/new_fact/betadots_application_version.rb
Facter.add(:betadots_application_version) do
confine do
File.exist?('/etc/sysconfig/application')
end
setcode do
config_list = Facter::Core::Execution.execute('/opt/app/bin/app config', {:timeout => 5})
config_list
end
end
Accessing Other Facts
It’s possible for a custom fact to leverage the value of another fact by using Facter.value
.However, this should be done cautiously to avoid introducing cyclic dependencies between facts.
In our example the application has to different config paths for different OS.
- RedHat - /etc/sysconfig/application
- Debian - /etc/default/application
Example with access to another fact:
# ~/new_fact/betadots_application_version.rb
Facter.add(:betadots_application_version) do
confine do
app_cfg_file = case Facter.value('os')['family']
when 'RedHat'
'/etc/sysconfig/application'
when 'Debian'
'/etc/default/application'
end
File.exist?(app_cfg_file)
end
setcode do
Facter::Core::Execution.execute('/opt/app/bin/app config')
end
end
The example retreives the os
fact and uses the family
key to compare the OS, then applies logic to determine which application config path to check for.
Return values
Puppet expects custom facts to return a valid value type, specifically strings, integers, booleans, or structured data (such as arrays and hashes). If the fact cannot determine a valid value, it should return nil
or undef
to indicate that no fact value is available.
Windows Facts
Windows-based systems support custom facts just like Linux or other Unix-like systems.
The main difference is that many Windows-specific tasks (such as checking installed software or reading from the registry)may require platform-specific Ruby code.
Within the Facter::Core::Execution.execute
usually powershell commands is used.
Here's an example of a Windows custom fact that retrieves the version of Internet Explorer:
# ~/new_fact/betadots_internet_explorer_version.rb
Facter.add(:betadots_internet_explorer_version) do
confine kernel: 'windows'
setcode do
Facter::Core::Execution.execute('reg query "HKLM\Software\Microsoft\Internet Explorer" /v svcVersion')
end
end
In this example, we use the reg query
command to check the Internet Explorer version in the Windows registry.
Please note that the Win32 API calls are deprecated since ruby 1.9.
Please consider using Fiddle or other Ruby-based libraries for interacting with the system.
Additional Concepts
Logging
It's often useful to include logging within custom facts to help with troubleshooting and development. You can log messages using Puppet's built-in logging mechanism:
Facter.add(:betadots_application_version) do
setcode do
Facter.debug("Custom fact 'betadots_application_version' running")
...
end
end
Logging levels include debug
, info
, warn
, error
, and fatal
.
Aggregates
Another option to create structured facts is the usage of chunks.
Each chunk must have a name and returns a data structure which must be of type array or hash.
After reading all chunks, the chunks will be aggregated. The default is to merge arrays or hashes.
It is possible to write a different aggregate definition.
We assume that our app can directly read config keys: /opt/app/bin/app config <key>
returns the value.
# ~/new_fact/betadots_application_version.rb
Facter.add(:betadots_application_version, :type => :aggregate) do
confine do
app_cfg_file = case Facter.value('os')['family']
when 'RedHat'
'/etc/sysconfig/application'
when 'Debian'
'/etc/default/application'
end
File.exist?(app_cfg_file)
end
chunk(:config_version) do
{:version => Facter::Core::Execution.execute('/opt/app/bin/app config version')}
end
chunk(:config_cache_size) do
{:cache_size => Facter::Core::Execution.execute('/opt/app/bin/app config cache_size')}
end
chunk(:config_log_level) do
{:log_level => Facter::Core::Execution.execute('/opt/app/bin/app config log_level')}
end
end
Result:
betadots_application_version => {
version => '1.2.4',
cache_size => '400M',
log_level => 'info',
}
Weight
Facter offers the option to select a value based on priority of a fact.
Let's assume that an application is either started form systemd or in any other way (so it has a pid file).
Note: external facts have a built in weight value of 1000. Overriding external facts is possible by creating a fact with the same name and specifying a weight value over 1000.
More details can be found in the Fact precedence section of the Custom facts overview page.
Facter.add('application_running') do
has_weight 100
setcode do
Facter::Core::Execution.execute('systemctl status app')
end
end
Facter.add('application_running') do
has_weight 50
setcode do
File.exist?('/var/run/application.pid')
end
end
Blocking and Caching
By default, facts are recalculated each time they are queried. In certain scenarios, this might be inefficient, especially for computationally expensive facts or facts that rarely change. Facter offers a caching mechanism that allows you to persist fact values between Puppet runs.
Besides this some facts might be of no interest, but take long time to read.
In this case Facters blocklist can be used.
Configuration takes place in /etc/puppetlabs/facter/facter.conf
on *nix systems or C:\ProgramData\PuppetLabs\facter\etc\facter.conf
on Windows.
Please note that this configuratoin must be done on the Puppet agents!
# /etc/puppetlabs/facter/facter.conf
facts : {
blocklist : [ "file system", "EC2", "processors.isa" ]
ttls : [
{ "timezone": 30 days },
{ "my-fact-group": 30 days },
]
}
fact-groups : {
my-fact-group : [ "os", "ssh.version"]
}
As you can see: within the blocklist
we can mention the facts itself, a sub fact, or a fact group.
Existing facter groups can be read on the command line:
Facter block groups
facter --list-block-groups
EC2
- ec2_metadata
- ec2_userdata
file system
- mountpoints
- filesystems
- partitions
hypervisors
- hypervisors
Facter cache groups
facter --list-cache-groups
EC2
- ec2_metadata
- ec2_userdata
GCE
- gce
...
Besides this we can add our own fact groups using the facts-group
setting.
Please note that in the past people were using Dylan Ratcliffe's facter_cache Module. This is no longer needed as of Puppet 6, when the facter.conf settings were introduced.
The Puppet website provides more information on facter.conf
settings.
Coding Strategies and Structured Data
- Limit the total number of facts
- Use data hashes
- Generic code and Ruby modules
In more advanced cases, facts can return structured data, such as arrays or hashes. Structured facts allow for more complex data to be passed back to the Puppet server.
It is absolutely required to limit the total number of facts for a node. Main reason is the performance of PuppetDB.
Hashes or arrays can be built directly in Ruby code or via Facter aggregates.
e.g. We need the state of several files.
Solution 1: Hash in Ruby:
Facter.add(:betadots_file_check) do
setcode do
file_list = case Facter.value(:kernel)
when 'windows'
[
'c:/Program Data/Application/bin/app',
'c:/backup/state.txt',
]
when 'Linux'
[
'/opt/app/bin/app',
'/etc/backup/state.txt',
]
end
result = {}
file_list.each |name| do
result[name] = { 'size' => File.size(name), 'world_writable' => File.world_writable?(name) } if File.exist?(name)
end
result
end
end
Will return:
{
/opt/app/bin/app => {
size => 64328,
world_writable => null,
},
/etc/backup/state.txt => {
size => 0,
world_writable => 438,
}
}
Solution 2: Hashes using aggregate:
Facter.add(:betadots_file_check, :type => :aggregate) do
file_list = case Facter.value(:kernel)
when 'windows'
[
'c:/Program Data/Application/bin/app',
'c:/backup/state.txt',
]
when 'Linux'
[
'/opt/app/bin/app',
'/etc/backup/state.txt',
]
end
chunk(:file_size) do
result = {}
file_list.each |name| do
result[name] = { :size => File.size(name) } if File.exist?(name)
end
result
end
chunk(:file_world_writable) do
result = {}
file_list.each |name| do
result[name] = { :world_writable => File.world_writable?(name) } if File.exist?(name)
end
result
end
end
Will return:
{
/opt/app/bin/app => {
size => 64328,
world_writable => null,
},
/etc/backup/state.txt => {
size => 0,
world_writable => 438,
}
}
Another example reads the Puppet agent certificate and provides the certificate extensions as a Facter hash:
Facter.add(:betadots_cert_extension) do
setcode do
require 'openssl'
require 'puppet'
require 'puppet/ssl/oids'
# set variables
extension_hash = {}
certdir = Puppet.settings[:certdir]
certname = Puppet.settings[:certname]
certificate_file = "#{certdir}/#{certname}.pem"
# get puppet ssl oids
oids = {}
Puppet::SSL::Oids::PUPPET_OIDS.each do |o|
oids[o[0]] = o[1]
end
# read the certificate
cert = OpenSSL::X509::Certificate.new File.read certificate_file
# cert extensions differs if we run via agent (numeric) or via facter (names)
cert.extensions.each do |extension|
case extension.oid.to_s
when %r{^1\.3\.6\.1\.4\.1\.34380\.1\.1}
short_name = oids[extension.oid]
value = extension.value[2..-1]
extension_hash[short_name] = value unless short_name == 'pp_preshared_key'
when %r{^pp_}
short_name = extension.oid
value = extension.value[2..-1]
extension_hash[short_name] = value unless short_name == 'pp_preshared_key'
end
end
extension_hash
end
end
Summary
Custom facts are a powerful way to extend Puppet’s built-in facts with your organization’s specific information. Whether you’re retrieving application versions, monitoring system configuration, or providing custom details for a specific environment, custom facts help ensure that Puppet has the right data to apply the correct configuration.
In this post, we’ve covered the basics of custom fact development, including:
- How to create custom facts in a local environment or as part of a module.
- The API for creating and confining facts.
- Accessing other facts and helper methods to enhance fact functionality.
- Returning structured data for complex requirements.
One more advise: Run local commands only! If connections to remote systems are required you must ensure scalability and high availability.
Happy puppetizing,
Martin
Top comments (0)