published on 6 September 2017

Editor's Note: This post was originally posted on Glenn Sarti's blog, http://glennsarti.github.io. It appears here in a slightly modified form, with Glenn's permission.

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.

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

@ECHO OFF
for /f "skip=1 tokens=3" %%k in ('reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v EditionID') do (
  set Edition=%%k
)
Echo Windows_Edition_external=%Edition%

For example on my Windows 10 laptop it outputs:

Windows_Edition_external=Professional

And from within Puppet:

> puppet facts
...
    "kernel": "windows",
    "windows_edition_external": "Professional",
    "domain": "internal.local",
    "virtual": "physical",
...

The custom fact

Firstly we need to create a boilerplate custom fact in the module by creating the following file lib/facter/windowsedition.rb:

Facter.add('windows_edition_custom') do
  confine :osfamily => :windows
  setcode do
    'testvalue'
  end
end

This creates a custom fact called windows_edition_custom which has a value of testvalue. Running Facter on my laptop we see:

> puppet facts
...
    "windows_edition_custom": "testvalue",
    "clientcert": "glennsarti.internal.local",
    "clientversion": "5.0.0",
...

Breaking down the custom fact code

So let’s break down the boilerplate code:

Facter.add('windows_edition_custom') do
...
end

This instructs Facter to create a new fact called windows_edition_custom:

  confine :osfamily => :windows

The confine statement instructs Facter to attempt resolution of this fact only on Windows operating systems.

  setcode do
..
  end

The setcode command instructs Facter to run the code block to resolve the fact's value:

    'testvalue'

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:

Facter.add('windows_edition_custom') do
  confine :osfamily => :windows
  setcode do
    value = nil
    Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\Windows NT\CurrentVersion') do |regkey|
      value = regkey['EditionID']
    end
    value
  end
end

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

When we run the updated fact we get:

> puppet facts
...
    "windows_edition_custom": "Professional",
    "clientcert": "glennsarti.internal.local",
    "clientversion": "5.0.0",
...

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.

Final touches

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:

> puppet facts
...
Warning: Facter: Could not retrieve fact='windows_edition_custom', resolution='<anonymous>': The
system cannot find the file specified.
{
...

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 begin / rescue block.

Facter.add('windows_edition_custom') do
  confine :osfamily => :windows
  setcode do
    begin
      value = nil
      Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\Windows NT\CurrentVersion') do |regkey|
        value = regkey['EditionID']
      end
      value
    rescue
      nil
    end
  end
end

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

$enclosure = Get-WMIObject -Class Win32_SystemEnclosure | Select-Object -First 1

Write-Output "chassis_type_external=$($enclosure.ChassisTypes)"

For example, on my Windows 10 laptop it outputs:

chassis_type_external=8

And from within Puppet:

> puppet facts
...
    "kernel": "windows",
    "chassis_type_external": "8",
    "domain": "internal.local",
    "virtual": "physical",
...

The custom fact

Just like the last example, we start with a boilerplate custom fact in the module by creating the following file lib/facter/chassistype.rb:

Facter.add('chassis_type_custom') do
  confine :osfamily => :windows
  setcode do
    'testvalue'
  end
end

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 begin / rescue block:

Facter.add('chassis_type_custom') do
  confine :osfamily => :windows
  setcode do
    begin
      require 'win32ole'
      wmi = WIN32OLE.connect("winmgmts:\\\\.\\root\\cimv2")
      enclosure = wmi.ExecQuery("SELECT * FROM Win32_SystemEnclosure").each.first

      enclosure.ChassisTypes
    rescue
    end
  end
end

So again, let's break this down:

      require 'win32ole'

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.

      enclosure = wmi.ExecQuery("SELECT * FROM Win32_SystemEnclosure").each.first

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.

This gives the following output:

> puppet facts
...
    "chassis_type_custom": [
      8
    ],
    "clientcert": "glennsarti.internal.local",
    "clientversion": "5.0.0",
...

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:

      enclosure.ChassisTypes.first

and this would output a single number, instead of a string:

> puppet facts
...
    "chassis_type_custom": 8,
    "clientcert": "glennsarti.internal.local",
    "clientversion": "5.0.0",
...

If you wanted it to be exactly like the external fact, we could then convert the integer into a string using to_s

      enclosure.ChassisTypes.first.to_s

and this would output a single string, instead of a number:

> puppet facts
...
    "chassis_type_custom": "8",
    "clientcert": "glennsarti.internal.local",
    "clientversion": "5.0.0",
...

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.

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.

Learn more

Share via:

Add new comment

The content of this field is kept private and will not be shown publicly.

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.