Tips for using the Puppet PowerShell module
*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 has great support for Windows, including a module on the Puppet Forge to run PowerShell-based commands. There are some instructions in the README to help people use the module, but there a few traps for people who are just starting out. So here are a few tips to help you.
PowerShell exit codes
The Puppet PowerShell module makes use of the exit codes produced by PowerShell, so it's important to understand how these codes are produced within a PowerShell process.
The exit
command can be used to exit a PowerShell script and optionally return an exit code.
Unlike a lot of scripting languages, PowerShell has the concept of terminating and non-terminating errors (more information). A terminating error will stop the script, whereas a non-terminating error will not. This can cause some headaches for people who aren't expecting it!
Take this very simple batch file:
@ECHO OFF
powershell.exe "& { exit 255 }"
ECHO Errorlevel %ERRORLEVEL%
It produces
Errorlevel 255
This is what you would expect to happen. But what if I don't explicitly set an exit code?
@ECHO OFF
powershell.exe "& { }"
ECHO Errorlevel %ERRORLEVEL%
produces
Errorlevel 0
So far, so good. So what happens if I raise an error (division by zero)?
@ECHO OFF
powershell.exe "& { 1/0 }"
ECHO Errorlevel %ERRORLEVEL%
produces
Attempted to divide by zero.
At line:1 char:5
+ & { 1/0 }
+ ~~~
+ CategoryInfo : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException
Errorlevel 0
But the exit code is still zero?!?!? What if the script syntax was broken, like a missing bracket?
@ECHO OFF
powershell.exe "& { (missing-bracket }"
ECHO Errorlevel %ERRORLEVEL%
produces
At line:1 char:21
+ & { (missing-bracket }
+ ~
Missing closing ')' in expression.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : MissingEndParenthesisInExpression
Errorlevel 1
Now the exit code is no longer zero.
What would happen if I changed the error from non-terminating to terminating?
This behavior is normally changed by specifying the ErrorAction
parameter on Cmdlets or setting the global $ErrorActionPreference
variable to Stop
@ECHO OFF
powershell.exe "& { Get-Service -Name IDontExist }"
ECHO Errorlevel %ERRORLEVEL%
powershell.exe "& { Get-Service -Name IDontExist -ErrorAction Stop }"
ECHO Errorlevel %ERRORLEVEL%
produces
Get-Service : Cannot find any service with service name 'IDontExist'.
At line:1 char:5
+ & { Get-Service -Name IDontExist }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (IDontExist:String) [Get-Service], ServiceCommandException
+ FullyQualifiedErrorId : NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand
Errorlevel 0
Get-Service : Cannot find any service with service name 'IDontExist'.
At line:1 char:5
+ & { Get-Service -Name IDontExist -ErrorAction Stop }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (IDontExist:String) [Get-Service], ServiceCommandException
+ FullyQualifiedErrorId : NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand
Errorlevel 1
So the non-terminating error (without the ErrorAction parameter) produced a zero exit code, while the terminating error (ErrorAction Stop) produced a non-zero exit code.
What does this all mean?
If you depend on PowerShell returning a meaningful exit code, you should use the exit command for all code branches. This ensures PowerShell returns the exit codes you expect.
# Don't do this
If ($foo -eq 'bar') {
exit 1
}
rather,
# Do this instead
If ($foo -eq 'bar') {
exit 1
}
exit 0
More information on -ErrorAction
More information on $ErrorActionPreference
Using onlyif and unless attributes
We’ll first look at version 1.0.6 of the Puppet PowerShell module. We’ll then look at the newly released version 2.0 of the Puppet Powershell module and what has changed.
The Puppet exec
resource has onlyif
and unless
attributes, which can be used to limit when the command is invoked — e.g., "create this file only if it does not exist," or "start this windows service unless it's already running."
The onlyif
parameter is defined as
If this parameter is set, then this exec will only run if the command has an exit code of 0.
Whereas the unless
attribute is defined as
If this parameter is set, then this exec will run unless the command has an exit code of 0.
Using what we know about PowerShell exit codes, a simple test manifest can be used to see what would happen for different scenarios:
# OnlyIf tests
exec { 'onlyif check exit 0':
command => '"OnlyifExit0"',
onlyif => 'exit 0',
provider => powershell,
} ->
exec { 'onlyif check exit 1':
command => '"OnlyifExit1"',
onlyif => 'exit 1',
provider => powershell,
} ->
exec { 'onlyif check noexit':
command => '"OnlyifNoExit"',
onlyif => '',
provider => powershell,
} ->
exec { 'onlyif check non-term error':
command => '"OnlyifNonTerm"',
onlyif => 'Get-Service -Name IDontExist;',
provider => powershell,
} ->
exec { 'onlyif check term error':
command => '"OnlyifTerm"',
onlyif => 'Get-Service -Name IDontExist -ErrorAction Stop',
provider => powershell,
} ->
# Unless tests
exec { 'unless check exit 0':
command => '"UnlessExit0"',
unless => 'exit 0',
provider => powershell,
} ->
exec { 'unless check exit 1':
command => '"UnlessExit1"',
unless => 'exit 1',
provider => powershell,
} ->
exec { 'unless check noexit':
command => '"UnlessNoExit"',
unless => '',
provider => powershell,
} ->
exec { 'unless check non-term error':
command => '"UnlessNonTerm"',
unless => 'Get-Service -Name IDontExist;',
provider => powershell,
} ->
exec { 'unless check term error':
command => '"UnlessTerm"',
unless => 'Get-Service -Name IDontExist -ErrorAction Stop',
provider => powershell,
}
produces
Notice: Compiled catalog for win-edson23cglf.localdomain in environment production in 0.17 seconds
Notice: /Stage[main]/Main/Exec[onlyif check exit 0]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[onlyif check noexit]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[unless check exit 1]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[unless check non-term error]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[unless check term error]/returns: executed successfully
Notice: Applied catalog in 18.28 seconds
So what did these tests show?
- The explicitly set exit code tests
onlyif check exit 0
,onlyif check exit 1
,unless check exit 0
andunless check exit 1
behaved exactly as the documentation stated. - The no exit code set tests
onlyif check noexit
andunless check noexit
behaved exactly as the documentation stated; i.e., default exit code of 0. - The terminating error tests (
onlyif check term error
andunless check term error
) behaved exactly as the documentation stated, remembering that PowerShell returns exit code 1 for terminating errors. - The non-terminating error tests
onlyif check non-term error
andunless check non-term error
are a bit strange. They behaved as if Powershell had returned an exit code of 1, but our PowerShell tests in the previous section showed that non-terminating errors return zero.
So as one last test, what would happen if a non-terminating error was thrown, but there was another command afterwards? After all, non-terminating errors do not terminate the script. Let's add Write-Host "Hello"
to the end of the onlyif
and unless
, and see what happens:
# OnlyIf tests
exec { 'onlyif check non-term error then command':
command => '"OnlyifNonTermThenCommand"',
onlyif => 'Get-Service -Name IDontExist; Write-Host "Success"',
provider => powershell,
} ->
# Unless tests
exec { 'unless check non-term error then command':
command => '"UnlessNonTermThenCommand"',
onlyif => 'Get-Service -Name IDontExist; Write-Host "Succes"',
provider => powershell,
} ->
produces
Notice: Compiled catalog for win-edson23cglf.localdomain in environment production in 0.16 seconds
Notice: /Stage[main]/Main/Exec[onlyif check non-term error then command]/returns: executed successfully
Notice: Applied catalog in 18.28 seconds
What happened there?!?! So now the non-terminating errors are behaving as if PowerShell had returned exit code 0.
It appears that the exit code is being determined by the last command executed. This can cause a lot of headaches when debugging onlyif
and unless
clauses.
What does all this mean?
- Always use explicit exit codes.
- Watch out for terminating errors, as the
exec
resource will treat them as non-zero exit codes.
Example:
Create an exec resource that;
Starts the FOO service unless, it doesn't exist or it exists and is already running
# Don't do this
# Start the FOO service unless already running or does not exist
exec { 'Start FOO':
command => 'Start-Service -Name "FOO"',
unless => '$foo = Get-Service -Name "FOO";
if ($foo.Status -eq "Running") { Exit 1 }',
provider => powershell,
}
In the example above, if the service doesn't exist, it will throw a non-terminating error and then process the next command. Remembering the previous weird test case, it will then execute the exec
resource, attempting to start a service that does not exist. However, this is not what we wanted.
# Do this instead
# Start the FOO service unless already running or does not exist
exec { 'Start FOO':
command => 'Start-Service -Name "FOO"',
unless => '$foo = Get-Service -Name "FOO";
if ($foo.Status -eq "Running") { Exit 1 } else { Exit 0 }',
provider => powershell,
}
Now that there is an explicit Exit 0
, the previous non-terminating error is ignored, and the exec
resource behaves as we wanted.
Update - Puppet PowerShell module, version 2.0
The previous section addresses version 1.0.6 of the Puppet PowerShell module. On 24 May, a new version was released which has made some huge performance improvements, and also fixed some bugs with error handling.
Performance Improvements
To give you some idea of the improvement, we can use the test manifest from the previous example and time how long it took to run. Of course, we can use PowerShell to test this:
#### OLD MODULE
Write-Host "*** OLD Module"
$oldModuleTime = Measure-Command { & puppet apply powershell.pp --modulepath C:\blog\old-module }
$oldModuleTime.TotalSeconds
#### NEW MODULE
Write-Host "*** NEW Module"
$newModuleTime = Measure-Command { & puppet apply powershell.pp --modulepath C:\blog\new-module }
$newModuleTime.TotalSeconds
#### BASELINE
Write-Host "*** Baseline"
$baselineTime = Measure-Command { & puppet apply -e "#" --modulepath C:\blog\old-module }
$baselineTime.TotalSeconds
### Improvement
$oldModuleTime = $oldModuleTime - $baselineTime
$newModuleTime = $newModuleTime - $baselineTime
Write-Host "*** Improvement"
Write-Host ($oldModuleTime.TotalMilliseconds / $newModuleTime.TotalMilliseconds).ToString("###%")
produces
*** OLD Module
Error: Write-error "Hello" returned 1 instead of one of [0]
Error: /Stage[main]/Main/Exec[write-error]/returns: change from notrun to 0 failed: Write-error "Hello" returned 1 instead of one of [0]
32.1743243
*** NEW Module
22.2315182
*** Baseline
12.4175615
*** Improvement
201%
So the old module took 32 seconds, while the new module took 22 seconds — but that isn't the whole story. The baseline measures how long Puppet takes to just start up, so it's not even executing PowerShell then. Taking the baseline time away, the old module took 20 seconds (32 - 12) and the new module took 10 seconds (22 - 12).
So the new module is two times faster than the old one! Also, there's less disk IO and there's no temporary file on disk, so that security concern is gone.
Obviously the performance improvement you see will change depending on your scenario, but in essence, the more PowerShell exec resources are in your manifests, the greater the improvement.
Error handling
Error handling has changed slightly in the new module, so let’s see what happens when we run the previous test manifest. I've also included the non-terminating error with extra command tests, too.
PowerShell module version 1.0.6
Notice: Compiled catalog for win-edson23cglf.localdomain in environment production in 0.17 seconds
Notice: /Stage[main]/Main/Exec[onlyif check exit 0]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[onlyif check noexit]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[onlyif check non-term error then command]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[unless check exit 1]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[unless check non-term error]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[unless check term error]/returns: executed successfully
PowerShell module version 2.0.0
Notice: Compiled catalog for win-edson23cglf.localdomain in environment production in 0.22 seconds
Notice: /Stage[main]/Main/Exec[onlyif check exit 0]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[onlyif check noexit]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[onlyif check non-term error]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[onlyif check non-term error then command]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[unless check exit 1]/returns: executed successfully
Notice: /Stage[main]/Main/Exec[unless check term error]/returns: executed successfully
So what's changed from version 1.0.6 to version 2.0?
- The
onlyif check non-term error
test now runs theexec
resource. This is the same behavior asonlyif check non-term error then command
, and now treats all non-terminating errors the same — i.e., exit code zero. - The
unless check non-term error
no longer runs theexec
resource. This is because the exit code is zero for non-terminating errors.
So when using the PowerShell module, remember:
- Always use explicit exit codes.
- Watch out for terminating errors, as the
exec
resource will treat them as non-zero exit codes. - Try to use the latest version of the PowerShell module, as it is faster and has improved error handling.
Glenn Sarti is a senior software engineer at Puppet.
Learn more
- More information about the Puppet PowerShell module on the Forge.
- Watch the webinar, Windows Configuration Management: Managing Packages, Services & Power Shell.
- Watch the webinar, Getting Up and Running with the Windows Module Pack.