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.

There are some core facts which Facter evaluates on all operating systems. However there are two additional types of facts that can be used to extend facter: external facts and custom facts.

External facts

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

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.

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

So we've added five lines of code to read the registry. Let's break these down too:

value = nil

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 regkey['EditionID'].

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

Much like the try / catch in PowerShell or C#, begin / 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_SystemEnclosureWMI (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.

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.

wmi = WIN32OLE.connect("winmgmts:\\\\.\\root\\cimv2")

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.

enclosure.ChassisTypes

And now we simply output the ChassisTypes parameter as the fact value.

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:

Final notes

Structured facts

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 facter

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

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.

Conclusion

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.