Puppet, PowerShell and Facter
Puppet uses a tool called Facter to gather system information during a Puppet run.
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 are processed on all operating systems, but two additional types of facts 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. Alternatively, 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, allowing for programmatic confinement to specific environments.
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 - it can be easy to forget these rules, especially when building cross-platform modules. While there are some tricks to help stop this, it’s easy for Windows scripts to log warnings and errors making it harder to figure out when real issues occur.
But 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, this blog post should help reduce the effort it takes to write custom facts. 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 my blog 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 boiler plate 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 lets break down the boiler plate 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 only attempt resolution of this fact on Windows operating systems.
setcode do
..
end
setcode
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 initialise the variable here, otherwise when its value is later set inside the codeblock, 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 langauges 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 registy 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 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 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 - 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 like 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 ok 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. This is 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, but this is not yet enabled for PowerShell (https://tickets.puppetlabs.com/browse/FACT-1653).
puppet facts
vs facter
In my examples above I was using the command puppet facts
whereas most people would probably use facter
. This is mostly because I’m lazy. By default just running Facter (facter
) won’t evaluate custom facts in modules. External facts are fine due to pluginsync. By running puppet facts
, Puppet automatically runs Facter with all of the custom facts paths loaded. Note, facter -p
works but is deprecated in favour of puppet facts
One other reason is debugging. In most modern Puppet installations Facter is running as native Facter which can make debugging native Ruby code trickier (though not impossible). However, when using Puppet as 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 to help me out.
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. I came across a blog post which explains setting up Ruby, very similar to my environment. Even though it’s for Chef, it still works for Puppet too.
The source code for these examples is available on my blog github repo.
Thanks to Ethan Brown (@ethanjbrown) for editing.
Comments