Fun with Chef and Powershell

When writing configuration code, you only want to take action when necessary. Chef often takes care of this for you. For example, if you use the “package” resource to install a package, it will only install it if it is not already installed. Sometimes, though, you have to handle this logic yourself. This is what it means to make a resource declaration “idempotent”.

To support this, Chef provides guards: only_if and not_if which can be given a bit of ruby (block) or shell script (string). The latter is implemented using the shell_out mixin which knows how to deal with various malfunctioning Unix fork implementations, Ruby garbage collection bugs, reaping dead children, etc. If the shell command returns zero, it is interpreted as a Ruby True value, which controls the guard appropriately. This all works well and predictably in Mac/Linux.

Windows, however, is another beast which tripped me up in a subtle way. Hence this post.

First off, you can try to set guard_interpreter :powershell_script in your resource, and that will execute a guard string in powershell. In my case, I wanted to use select-string to replace grep but select-string does not set exit codes like grep does, so it seemed like a non-starter.

Then I learned about powershell_out, a mixin from the community powershell cookbook which mimics the behavior of shell_out on Windows. That was moved to the windows 3rd party cookbook, parts of which are now part of Chef core. Lots of docs say you have to include 3rd party cookbooks, or add special “Mixin” incantations to your recipe file. In fact, we should be able to simply do this:

package 'USB_3.0_Driver' do
 source "#{driver_exe}"
 action :install
 options '/s' # silent
 installer_type :custom
 only_if { powershell_out('driverquery | select-string "USB 3.0"').stdout.empty? }
 end

But this, in fact, will install the driver every time. The key to debugging this was to realize there is another version of powershell_out: powershell_out! which raises exceptions on error (Chef will silently suppress them otherwise). That clearly showed the problem; lots of these:

Select-String : The input object cannot be bound to any parameters for the command either because the command does not
 take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
 At line:1 char:28
 + driverquery | select-string <<<< USB 3.0
 + CategoryInfo : InvalidArgument: (WinUSB Wi...010 2:43:56 AM :PSObject) [Select-String], Parameter
 BindingException
 + FullyQualifiedErrorId : InputObjectNotBound,Microsoft.PowerShell.Commands.SelectStringCommand

followed by:

Ran powershell.exe -NoLogo -NonInteractive -NoProfile -ExecutionPolicy Unrestricted -InputFormat None -Command "driverquery | select-string "USB 3.0"" returned 1

Oh, snap! It’s a quoting issue. All I had to do was reverse the polarity of the quotes in the powershell_out command to fix the problem:

package 'USB_3.0_Driver' do
 source "#{driver_exe}"
 action :install
 options '/s' # silent
 installer_type :custom
 only_if { powershell_out("driverquery | select-string 'USB 3.0'").stdout.empty? }
 end

After thinking about this more, I realized that it should be possible to set the exit code using a powershell if statement, and sure enough, this also works:

package 'USB_3.0_Driver' do
 source "#{driver_exe}"
 action :install
 options '/s' # silent
 installer_type :custom
 guard_interpreter :powershell_script
 only_if "if ( $( driverquery ) -Match 'USB 3.0' ) {exit 1} else {exit 0}"
 end

And it also works if I put the quotes the other way around:

only_if 'if ( $( driverquery ) -Match "USB 3.0" ) {exit 1} else {exit 0}'

Given that more stable behavior around quotes, and not having to involve another method, I think I will prefer this to powershell_out.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s