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

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

  • Comments 2
  • Likes

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
http://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

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
##########################################################################
# 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 required


function 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 PSBoundParameters


function 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 scenario


function 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

#endregion


Export-ModuleMember -function *-TargetResource
Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Great post! Looking forward to more.

  • Awesome post!! Would definitely help me save ton of time as well :)