PowerShell DSC Blog Series–Part 2, Authoring DSC Resources when Cmdlets already exist

Welcome to Part 2 in my PowerShell Desired State Configuration blog series.  In Part 1 last week, I pulled together a number of online resources that helped me when getting up to speed on DSC.  This week, I’d like to share a process for authoring resources that has saved me a lot of time.

Go To Example Script

This approach is specifically intended for the scenario where native cmdlets, or cmdlets that shipped from the developer of the software, already exist.

I’m not declaring this a “best practice” or the perfect method of authoring, I’m saying this is a process that works for me and has saved me a lot of time, so it might work for you too.  One of the core concepts is splatting parameter values in to native cmdlets.

about_Splatting
https://technet.microsoft.com/en-us/library/jj672955.aspx

I actually didn’t come up with it as an approach to DSC.  If you look at the DSC Resource Kit contribution xNetworking, the concept is there in xIPAddress and xDNSServerAddress.  Those resources actually ignited my thinking.  The same approach is applied in xSmbShare, xRemoteDesktopSessionHost, and probably others.

In addition to splatting parameters, here is the basic storyline I follow over and over again:

  • Let’s assume my goal would be to create a DSC resource, and the solution already has a good set of cmdlets.  Typically the cmdlets would look something like the below, although obviously there are plenty of cases where it isn’t this clean.  For demonstration purposes, let’s say it is.

      • Get-Something
      • New-Something
      • Remove-Something
      • Set-Something
  • I always start by looking at New and/or Set.  Specifically, I like to run Show-Command and look at what parameters are there that help me accomplish what I have in mind. 

    image
    You can see in the example, I instantly found out that for DNS client settings on a machine, I have to have the Alias or the Index, and I can set the addresses.  I’m finding in many cases, that’s an easy way to guess what the parameters are going to be for my resource functions.

  • Next I look at the Get cmdlet.  What I want to know is whether the parameters for the Get cmdlet line up with the parameters for the New and/or Set cmdlets.  If they do, I am in Resource authoring Nirvana, because I probably won’t have to do much more than copy and paste to create a resource.  If they don’t that’s OK, we have an opportunity to take something that is complex and make it simple for others to consume by creating a resource.

  • Once I’ve done that little bit of detective work, I’m ready to get my tools out and create a resource.

I’ve taken *-DNSClientServerAddress and made it a boilerplate example template, included at the bottom of this post.  I picked a scenario that already has a published resource on purpose so that this would be looked at as an example, not as a newly published resource.  I’ve added a lot of notes throughout so that you don’t have to come back and re-read a long blog post to figure out what I was thinking through each section.  Like any DSC Resource, it has Get, Set, and Test, but let’s take a look at the anatomy a little closer.

I have taken snips of each script section below as reference to my explanations.  Click each thumbnail to gain additional context.

imageThe notes section explains what I have in mind and calls out some special cases to watch out for.  Example, if you have a cmdlet that accepts an object type as input, you may have to pass in a string value that you use to go get the object and then pass the object on to the native cmdlets.  That would make it a two step process.

image

The Set function is second in the script but I always start with Set.  You’ll see I’ve bound the function as a region.  That’s so I can include comments and examples for testing but easily collapse the whole thing out of my way if I’m working between Get and Test in ISE.  For this section, I’m literally just running the native Set cmdlet and then splatting in the parameters.  Splatting, is a simple method of taking a hash table of values and passing it as arguments to a command.  Isn’t it convenient, PSBoundParameters is automatically generated inside a function so I don’t have to create anything.

image

For the Get function I just run the native Get cmdlet, remove Verbose and Debug from the function parameters, and then run a foreach loop to build a hash table.  If you have a lot of parameters this can save a lot of typing.  Important – this only works perfectly when the parameters are the same for Get and Set.  If your Get function has fewer parameters it usually works out because the example is pulling only what you need out of what is returned by the Get cmdlet.  If you need to return more values than the cmdlet, possibly calling multiple cmdlets, you could apply this more than once or add static entries.  In either case you just append $out using += and then return $out.

image

And finally the Test function.  Again if everything happens to have a common set of parameters (Nirvana), I literally don’t have to touch this section at all.  The way this sample works is to run the Get function and compare the output against the values from the Test function parameters.  The Set and Test functions share a common set of parameters so this validates everything that can be set.  If the Get function accepts a subset of parameters then I modify the line starting with $Get to pass those as arguments one by one.

Notice that at the bottom of each section I include a commented out test for every function.  This is to make it easier on me when doing test and validation, especially if I haven’t looked at the module in a while.

 

A few enhancements I sometimes use:

  • If I am using Ensure: {Present, Absent} as is common in DSC, I run a Switch and take a set of actions based on whether I am adding or removing.
  • If the native cmdlets allow me to do both Set and New, I find it works out well to call the Get function from the Set function and If/Else based on whether the settings are already applied.  This makes the Set function idempotent across two scenarios.  If it exists, validate the configuration, if it doesn’t, create it.

I’ll wrap up here and paste the boilerplate example at the bottom so you don’t have to expect additional reading after the large script sample block. 

I’ve had a number of people ask me ‘'What’s the point of creating a DSC resource, when you already have cmdlets.  Why not just write a script?”  Well, I’m not going to get on much of a soapbox but here are a few bullets to ponder.

  1. Don’t get hung up on Resource vs. Script.  A Resource is a script module authored using a specific way of thinking.  Think of it as a way to take all the scripts and modules you have written and give that effort the credit it deserves by making the automation very easily consumable by others.
  2. I can hand off a Configuration script to someone who would like to build out an environment who knows nothing about PowerShell, DSC, or even automation in general, and they get the benefit of reduced time to deployment while I get the benefit of a process that helps ensure the operational best practices for my environment are being followed.
  3. While you could write a script that follows all the same best practices of declarative language and being idempotent, in the end you would basically have your own recreation of DSC without taking advantage of what is built in to the operating system to consume it in a predictable, repeatable, logged, stored, and expected way which is key for others when troubleshooting.

Thanks and stay tuned to Building Clouds Blog!

Example Script

001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118 ########################################################################### EXAMPLE SCRIPT MODULE# Simple template for Resource when Cmdlets already exist## PREMISE - if a set of cmdlets already exist that include Set and Get, # creating a resource should be mostly boilerplate.## CANONICAL EXAMPLE - cmdlets to Get and Set Client DNS IP## KNOWN COMPLEXITY# - cmdlets that accept objects as input would require two steps# - due to CmdletBinding, Verbose and Debug must handled for Test and Get## SPLATTING - preffered, which leads to building params list from Set#region BOILERPLATE GET FUNCTION## SPECIAL CASES# - Params list is based on Set function, however native Get cmdlets # sometimes require params that cannot be set# - Native Get functions may require filters to get expected output# - In complex scenarios more than one native Get cmdlet might be required to# build the complete hash table# - In many cases a Get will return multiple values and use of "contains"# might be requiredfunction Get-TargetResource {[CmdletBinding()]param ([Parameter(Mandatory)][string]$InterfaceAlias,[Parameter(Mandatory)][string]$ServerAddresses) # Native Get cmdlet $Get = Get-DNSClientServerAddress -AddressFamily IPv4 | ? InterfaceAlias -eq $InterfaceAlias # Removing Verbose and Debug from output $PSBoundParameters.Remove("Verbose") | out-null $PSBoundParameters.Remove("Debug") | out-null # Build Hashtable from native cmdlet values foreach ($Prop in $PSBoundParameters.Keys) { $out += @{$Prop = $Get | % $Prop} Write-Verbose "$Prop = $($Get | % $Prop)" } $out }# Get-TargetResource 'Wi-fi' '192.168.0.1' -verbose#endregion#region BOILERPLATE SET FUNCTION## SPECIAL CASES# - Occasionally param values must be discovered or hard coded and# ammended to PSBoundParametersfunction Set-TargetResource {[CmdletBinding()]param([Parameter(Mandatory)][string]$InterfaceAlias,[Parameter(Mandatory)][string]$ServerAddresses) # Native Set cmdlet Set-DnsClientServerAddress @PSBoundParameters }# Set-TargetResource 'Wi-fi' '192.168.0.1' -Verbose#endregion#region BOILERPLATE TEST FUNCTION## SPECIAL CASES# - When some other native Get cmdlet is the only# way to test the scenariofunction Test-TargetResource {[CmdletBinding()]param([Parameter(Mandatory)][string]$InterfaceAlias,[Parameter(Mandatory)][string]$ServerAddresses) # Output from Get-TargetResource $Get = Get-TargetResource @PSBoundParameters # Removing Verbose and Debug from output $PSBoundParameters.Remove("Verbose") | out-null $PSBoundParameters.Remove("Debug") | out-null # Compare dictionary and hash table $bool = $true $PSBoundParameters.keys | % { if ($PSBoundParameters[$_] -ne $Get[$_]) { $bool = $false write-verbose "$($_): $($PSBoundParameters[$_]) -ne $($Get[$_])" } } $bool }# Test-TargetResource 'Wi-fi' '192.168.0.1' -verbose#endregionExport-ModuleMember -function *-TargetResource