The Goal: I need to return the versions of PowerShell and the latest .NET framework installed as facts.
I’ve done this before and I’ll do this again, but one of the bigger aggravations I have about puppet development on Windows is the reliance on many PowerShell calls to find simple pieces of information. In the case above, however, I can get all that information from the $PSVersionTable. I just needed to get that parsed and back into Facter in a way that made sense to a person that didn’t want to determine .NET revision numbers vs. the “on the box” version of .NET (and don’t get me started on that…).
So…first step is a fact to get the information from Windows sent back into a structured facts. Then I can create additional facts that parse that information without relying on another PowerShell call. Let’s create a “module” to contain these facts. Via whatever directory creation methods you want to use, make this happen:
windows_facts/
windows_facts/lib
windows_facts/lib/facter
Now, within the windows_facts/lib/facter directory, create psversiontable.rb:
require 'json'
Facter.add("psversiontable") do
confine :kernel => :windows
setcode do
powershell = if File.exists?("#{ENV['SYSTEMROOT']}\sysnative\WindowsPowershell\v1.0\powershell.exe")
"#{ENV['SYSTEMROOT']}\sysnative\WindowsPowershell\v1.0\powershell.exe"
elsif File.exists?("#{ENV['SYSTEMROOT']}\system32\WindowsPowershell\v1.0\powershell.exe")
"#{ENV['SYSTEMROOT']}\system32\WindowsPowershell\v1.0\powershell.exe"
else
"powershell.exe"
end
query = "$ErrorActionPreference = 'SilentlyContinue';$psv = $PSVersionTable | ConvertTo-Json; $psv.ToLower()"
response = JSON.parse(Facter::Util::Resolution.exec(%Q{#{powershell} -command "#{query}"}))
if response
response
else
"unavailable"
end
end
end
Let’s talk about what we’re doing here. We are creating a new fact called ‘psversiontable’ and confining it to any system where the ‘kernel’ fact is equal to ‘windows’. Then we open our code block and do the following:
- Require the Ruby JSON library. Needed for some JSON fun later in the script. I’m pretty sure if you are using a version Puppet new enough that it supports structured facts you should have a newer-ish version of Ruby with the JSON library bundled with core, so there should not be a dependency issue here. I haven’t run into one on PE 3.7.2 and up so feel free to yell at me if you have issues.
- Ceate a new fact called ‘psversiontable’.
- Confine that fact to any system where the ‘kernel’ fact is equal to Windows. This is very important in mixed environments, or else there will be some super fun errors on your Linux boxes when Puppet runs.
- A conditional statement that looks for the powershell.exe. If it’s not in %systemroot%\sysnative it will look in %systemroot%\system32 (this is straight up stolen out of the powershell provider).
- A query variable containing the PowerShell want to run. The query does the following:
a. Set the error action default to silently continue. This means if it doesn’t work the response variable will be empty.
b. Set a psv variable, equal to the output of the PSVersionTable object, then converting those values from a PSObject to a JSON blob.
c. Set the text in the JSON blob to lowercase, which you may think is purely cosmetic but helps when moving back into Linux work where case is a thing that matters. - Set the response variable. The value should be the parsed JSON blob, which should convert nicely to a Ruby hash and in turn a structured fact.
- A conditional statement. If for some reason the PowerShell side of this didn’t work, say an old, rusty version of PowerShell doesn’t know what you are talking about, then return a fact value of “unavailable”. Otherwise, use the hash.
Save the file and run puppet. The fact should plugin sync and run. To verify, run facter -p psversiontable and you should see something like this:
PS C:\Users\vagrant> facter -p psversiontable
{"psversion"=>{"major"=>4, "minor"=>0, "build"=>-1, "revision"=>-1, "majorrevision"=>-1, "minorrevision"=>-1}, "wsmansta
ckversion"=>{"major"=>3, "minor"=>0, "build"=>-1, "revision"=>-1, "majorrevision"=>-1, "minorrevision"=>-1}, "serializat
ionversion"=>{"major"=>1, "minor"=>1, "build"=>0, "revision"=>1, "majorrevision"=>0, "minorrevision"=>1}, "clrversion"=>
{"major"=>4, "minor"=>0, "build"=>30319, "revision"=>34209, "majorrevision"=>0, "minorrevision"=>-31327}, "buildversion"
=>{"major"=>6, "minor"=>3, "build"=>9600, "revision"=>17400, "majorrevision"=>0, "minorrevision"=>17400}, "pscompatiblev
ersions"=>[{"major"=>1, "minor"=>0, "build"=>-1, "revision"=>-1, "majorrevision"=>-1, "minorrevision"=>-1}, {"major"=>2,
"minor"=>0, "build"=>-1, "revision"=>-1, "majorrevision"=>-1, "minorrevision"=>-1}, {"major"=>3, "minor"=>0, "build"=>-
1, "revision"=>-1, "majorrevision"=>-1, "minorrevision"=>-1}, {"major"=>4, "minor"=>0, "build"=>-1, "revision"=>-1, "maj
orrevision"=>-1, "minorrevision"=>-1}], "psremotingprotocolversion"=>{"major"=>2, "minor"=>2, "build"=>-1, "revision"=>-
1, "majorrevision"=>-1, "minorrevision"=>-1}}
Gross! However, you now have the powershell and dotnet versions contained within. A little extra parsing and fact building and we are on our way. I’ll save that for the next post.