Starting out writing custom facts for Windows
Puppet, PowerShell and Facter
Puppet uses a tool called Facter to gather system information during a Puppet run. This information is known as facts within Puppet. As the Facter documentation says:
Facter is Puppet's cross-platform system profiling library. It discovers and reports per-node facts, which are available in your Puppet manifests as variables.
External facts provide a way to use arbitrary executables or scripts to generate facts as basic key / value pairs. If you’ve ever wanted to write a custom fact in Perl, C or PowerShell, this is how. Additionally, external facts may contain static structured data in a JSON or YAML file.
Custom facts are written in Ruby and have more advanced features — for example, programmatic confinement to specific operating systems, which is not possible with external facts.
Most people new to Facter will write PowerShell scripts as external facts. However, there is a downside. The execution time for PowerShell scripts can be a little slow as a result of the time required to start a new PowerShell process for each fact. Another downside is that Windows will use file extensions to determine if a fact may be executed, while Unix-based operating systems will look for the executable bit (+x). It can be easy to forget these rules, especially when building cross-platform modules, causing warnings and errors to appear in Puppet logs.
The apparent learning curve to writing Ruby looks steep; if all you want to do is read a registry key and output the result, why should a Windows administrator have to learn Ruby? Well, reading this blog post should help you reduce the effort it takes to write custom facts, and then you'll be able to speed up your Puppet runs. Also, if you squint, the Ruby language looks a lot like (and in some cases operates similarly to) PowerShell.
The source code for these examples is available on myblog GitHub repo.
Writing a registry-based custom fact
The external fact
For this example, we'll convert a batch-file-based external fact to a Ruby external fact. This fact reads the
EditionID of the operating system from the registry and then populates a fact called
For example on my Windows 10 laptop it outputs:
And from within Puppet:
The custom fact
Firstly we need to create a boilerplate custom fact in the module by creating the following file
This creates a custom fact called
windows_edition_custom which has a value of
testvalue. Running Facter on my laptop we see:
Breaking down the custom fact code
So let’s break down the boilerplate code:
This instructs Facter to create a new fact called
confine statement instructs Facter to attempt resolution of this fact only on Windows operating systems.
setcode command instructs Facter to run the code block to resolve the fact's value:
As this is just a demonstration, we are using a static string. This is the code we'll subsequently change to output a real value.
Reading the registry in Puppet and Ruby
You can access registry functions using the
Win32::Registry namespace. This is our new custom fact:
So we've added five lines of code to read the registry. Let's break these down too:
First we set value of the fact to nil. We need to initialize the variable here, otherwise when its value is later set inside the code block, its value will be lost due to variable scoping.
Next we open the registry key
SOFTWARE\Microsoft\Windows NT\CurrentVersion. Note that unlike the batch file, it doesn't have the
HKLM at the beginning. This is because we're using the
HKEY_LOCAL_MACHINE class, so adding that to the name is redundant. By default, the registry key is opened as Read Only and for 64-bit access.
Next, once we have an open registry key, we get the registry value as a key in the
regkey object, thus
Lastly, we output the value for Facter. Ruby uses the output from the last line, so we don't need an explicit
return statement like you would in languages like C#.
When we run the updated fact we get:
Tada! We've now converted a batch-file-based external registry fact to a custom Ruby fact in 10 lines. But there's still a bit of cleaning up to do.
If the registry key or value does not exist, Facter raises a warning. For example, if I change
value = regkey['EditionID'] to
value = regkey['EditionID_doesnotexist'] I see these errors in the output:
We could write some code to test for existence of registry keys, but as this is just a fact we can simply swallow any errors and not output the fact. We can do this with a
Much like the
catch in PowerShell or C#,
rescue will catch the error and just output
nil for the fact value if an error occurs.
Writing a WMI-based custom fact
The external fact
For this example we'll convert a PowerShell file based external fact, to a Ruby external fact. This fact reads the
ChassisTypes property of the Win32_SystemEnclosure WMI (Windows Management Implementation) class. This describes the type of physical enclosure for the computer — for example a mini tower, or in my case, a portable device.
For example, on my Windows 10 laptop it outputs:
And from within Puppet:
The custom fact
Just like the last example, we start with a boilerplate custom fact in the module by creating the following file
Accessing WMI in Puppet and Ruby
We can access WMI using the
WIN32OLE Ruby class and
winmgmts:// WMI namespace. If you ever used WMI in VBScript (Yes, I'm that old!) this may look familiar.
Note that I've already added the
So again, let's break this down:
Much as in PowerShell or C#, we need to import modules (or gems for Ruby) into our code. We do this with the
require statement. This enables us to use the
WIN32OLE object on later lines.
We then connect to the local computer (local computer is denoted by the period) WMI, inside the
root\cimv2 scope. Note that in Ruby the backslash is an escape character, so each backslash must be escaped as a double backslash. Although WMI can understand using forward slashes, I had some Ruby crashes in Ruby 2.3 using forward slashes.
Now that we have a WMI connection, we can send it a standard WQL query for all Win32_SystemEnclosure objects. As this returns an array, and there is only a single enclosure, we get the first element (
.each.first) and discard anything else.
And now we simply output the
ChassisTypes parameter as the fact value.
This gives the following output:
Huh. So the output is slightly different. In external executable facts, all output is considered a string. However, as we are now using WMI and custom Ruby facts, we can properly understand data types. Looking at the MSDN documentation,
ChassisTypes is indeed an array type.
If this was okay for any dependent Puppet code, we could leave the code as is. However, if you wanted just the first element we could use:
and this would output a single number, instead of a string:
If you wanted it to be exactly like the external fact, we could then convert the integer into a string using
and this would output a single string, instead of a number:
Structured facts allow people to send more data than just a simple text string, usually as encoded JSON or YAML data. External facts have been able to provide structured facts — for instance, using a batch file to output pre-formatted JSON text. At the time of writing, this was not available for PowerShell due to a bug causing all output to be seen as key-value pairs instead of structured data. You can watch the Jira ticket FACT-1653 to find out when this gets fixed.
puppet facts vs
In my examples above I was using the command
puppet facts, whereas most people would probably use
facter. By default, just running Facter (
facter) won't evaluate custom facts in modules. External facts are fine due to pluginsync, which ensures that all your nodes have the most current version of your plug-ins, including external facts, before a Puppet agent run. By running
puppet facts, Puppet automatically runs Facter with all of the custom facts paths loaded. Note that
facter -p also works, but is deprecated in favour of
Another reason I use the command
puppet facts is to provide for debugging. In most modern Puppet installations, Facter runs as native Facter, which can make debugging native Ruby code trickier (though not impossible). However, when you use the Puppet gem instead of installing the puppet-agent package (common during module development), it uses the Facter gem. The Facter gem allows for using standard Ruby debugging tools, which I find helpful.
I hope this blog post helps you see that writing simple custom facts isn't too daunting. In fact, the hardest part is setting up a Ruby development environment. The Puppet Development Kit (PDK) makes it easy to set up your module development environment, including Ruby.
The source code for these examples is available on my blog github repo.
Thanks to my Puppet colleague Ethan Brown (@ethanjbrown) for editing this post.
Glenn Sarti is a senior software engineer at Puppet.