DSC in Chef: Converting Ruby Hashes to PowerShell Types

Background

Chef on Windows in the past has not had a great story but over the last 6 months they have come a long way to improve their support for the platform. In February of 2016 Microsoft also released the Windows Management Framework 5.0 with significant Desired State Configuration (DSC) improvements. Chef and DSC are closely related but can be used together to get the best of both worlds.  In this post we will look at an example and explain how to avoid a type conversion issue.

dsc_resource

Chef comes with some DSC integration in the box, namely the dsc_resource and dsc_script providers.  These allow you to utilize the DSC framework from within the context of Chef.  This is interesting because DSC seems to be moving much quicker (with the full backing of Microsoft and the community) than the native Chef providers for Windows.  As an example the xWebAdministration module provides many useful IIS related DSC resources that Chef has no corresponding providers for.

Here is an example of calling Invoke-DSCResource via PowerShell to create a site:

Invoke-DscResource -Name xWebsite -Method Set -Property @{ Name="testingDSC1";Ensure="Present";PhysicalPath="C:temptestingDSC"} -ModuleName xWebAdministration -Verbose

From Chef we can easily utilize the dsc_resource provider to create an IIS website on the target via DSC

siteName = 'testingDSC_fromChef1' 
siteDirectory = "C:\temp\#{siteName}" 
dsc_resource 'websiteDirectory' do 
  resource :file 
  property :destinationpath, siteDirectory 
  property :type, 'Directory' 
end 
dsc_resource 'createWebsite' do 
  resource :xWebsite 
  property :name, siteName 
  property :PhysicalPath, siteDirectory 
end

:

The Problem

Depending on your setup in IIS you will likely see binding conflict issues creating a website using either method above at some point.  IIS sites can’t share the same IPs and ports unless they are configured with host name entries. Thats okay, the xWebsite DSC resource supports setting the bindings as a property as well.  You can simply add the following property to the PowerShell DSC call:

;BindingInfo=[ciminstance[]](new-ciminstance -classname MSFT_xWebBindingInformation -Namespace root/microsoft/Windows/DesiredStateConfiguration -Property @{Protocol='HTTP';IPAddress='127.0.0.1';Port=8080}

When we attempt to set the property in Chef its get more complicated.  We can’t simply set the BindingInfo to a string because it will pass it to PowerShell as a string then you get a type conversion issue.

Invoke-DscResource : Convert property 'BindingInfo' value from type 'STRING' to type 'INSTANCE[]' failed

The Solution – to_psobject

After digging in the source of the dsc_resource (I love open source software!), I found it uses the translate_type method from the Chef::Mixin::PowershellTypeCoercions class.  This method has a dictionary of well known types for translating Ruby types to PowerShell types: Fixnum, Float, True/False, Array, etc. If a matching type can’t be found it defaults back to passing it as a string.  But before that it also checks to see if the Ruby object has a to_psobject method.  BINGO!

Also notice the dsc_resource provider is simply using the Invoke-DSCResource PowerShell cmdlet. So all we have to do in this method is provide a string that mimics the correct PowerShell syntax above.

class WebsiteBindings 
  @bindings = [] 
  def initialize(bindings) 
    @bindings = bindings 
  end 
	
  def to_psobject() 
    bindings = Array.new() 
    @bindings.each do |b| 
      bindings.push("(new-ciminstance -classname MSFT_xWebBindingInformation -Namespace root/microsoft/Windows/DesiredStateConfiguration -Property @{Protocol='#{b[:protocol]}';IPAddress='#{b[:ip]}';Port=#{b[:port]}} -ClientOnly)") 
    end 
    "[ciminstance[]](#{bindings.join(',')})" 
  end 
end 

bindings = WebsiteBindings.new([ 
  { protocol: 'HTTP', ip: '127.0.0.1', port: 8080 }, 
  { protocol: 'HTTP', ip: '127.0.0.1', port: 8081 } ]) 

siteName = 'testingDSC_fromChef1' 
siteDirectory = "C:\temp\#{siteName}" 

dsc_resource 'websiteDirectory' do 
  resource :file 
  property :destinationpath, siteDirectory 
  property :type, 'Directory' 
end 
dsc_resource 'createWebsite' do 
  resource :xWebsite 
  property :name, siteName 
  property :PhysicalPath, siteDirectory 
  property :BindingInfo, bindings 
end

Here I added a WebsiteBindings Ruby class with a constructor method that takes a hash of binding info.  In the to_psobject method we iterate over the hash and create the script representing a ciminstance PowerShell object for each binding and finally join those strings together inside an array of ciminstance .

With that the Invoke-DSCResource call is made with the correct bindings and the Chef run completes successfully.

image

Hope this helps others as I found very little online about this issue.

If you have questions or suggestions please continue this discussion in the comments below or on Twitter.

6 thoughts on “DSC in Chef: Converting Ruby Hashes to PowerShell Types”

  1. Hello,

    I’m running into this exact problem trying to pass the bindings to dsc_resource. I’m new to chef and tried adding your code to my recipe however I’m getting an “undefined method” error. Any guidance here would be greatly appreciated.

    Thanks,
    Ruben

  2. Thank you for the workaround, any idea how to get this to add a hostname as well? I added hostname to your array of bindings attributes but it simply ignores it

    1. I had the same issue. I did not dig into it to far so I’m not exactly sure of why it works this way, but after some trial and error I was able to get it to work by updating a few parts.

      First in the bindings.push row I had to add syntax for hostname (its row 10 in the example code above)
      @{Protocol=’#{b[:protocol]}’;IPAddress=’#{b[:ip]}’;Port=#{b[:port]};HostName=’#{b[:hostname]}’}

      I HAD to put it after port, if I put it before it didn’t work. I also had to make sure that Port never had single quotes around it.

      Then when I defined it, I just had to do
      bindings = WebsiteBindings.new([
      { protocol: ‘HTTP’, ip: ‘*’, port: 80, hostname: ‘servername.fqdn’ }])

Leave a Reply