The case of the “Silly” DSC custom resource

In recent customer conversations, we got feedback that documentation on writing a custom Windows PowerShell Desired State Configuration (DSC) resource is still something which can use improvement, especially in the areas of step-by-step examples. Therefore, I have decided to write up a “silly” custom DSC resource to get our readers quickly up and running with writing custom DSC resources. In the next blog posts, I will guide you through writing a real-life example for managing your servers, and what kind of challenges come with that.


Assumptions and primers

If you’re new to DSC, or just didn’t have the time to start yet and want to start reading, please revisit some blog posts we have released previously on DSC.

 

The anatomy of a DSC resource

When you explore a DSC resource more closely, you will find that it consists of 3 components:

  1. The DSC module - a PowerShell module file with the extension psm1
  2. The DSC data file - a PowerShell data file with the extension psd1
  3. The DSC schema file - a Managed Object Format file with extension MOF

We require these 3 components for our custom resource. Luckily, the Windows PowerShell team has provided us with the Resource Designer Tool, which enables you to generate the DSC module and schema files, and helps prevent you from making syntax errors. You don’t have to use the Resource Designer Tool, but it makes writing your first DSC resource a lot easier. After you play around a bit, you will figure out that you could also leverage a boilerplate approach, as mentioned by Michael Greene in this blog post. Effectively, its about reusing templates, which makes total sense if you’re writing multiple custom DSC resources.


The objective of our “silly” DSC resource

Fair warning:

When I start with a new technology I would like to understand better, I tend to keep things as simple as I can to focus on the concepts. So in this blog post I will walk you through the creation of a custom DSC resource. The sample resource won't be of any use to you other then to understand the authoring process better. There will always be multiple options to choose from if it comes to writing PowerShell scripts. So please consider this blog post as an conceptual example for writing a custom DSC resource.  For that reason, no error handling has been added, which in a production environment, should always be the case. In addition, I didn’t want to change any configuration on my machine to test-drive DSC.

In my “silly” example I simply want to check if a text file has the value of either Red, Blue or None. For that to happen we will leverage the DSC keywords Present and Absent. So if I use the the combination of Present and Red, DSC makes sure that the value of Red is set. If I use the combination of Absent and Red, DSC makes sure that the value is not Red. That leads me to think about what kind of parameters I want to use in my example. So far, I’ve come up with the color I need to validate, so that’s 1. Oh, and of course, I need the path and file name to check, so that makes 2. Let’s call those parameters $Color and $ColorFilePath, and start with those.

Time to download and install the Resource Designer Tool from here. Unzip the files under the $env:ProgramFiles\WindowsPowerShell\Modules folder. Make sure that the module is available by opening a Windows PowerShell window and run Get-Module –ListAvailable to verify that xDscResourceDesigner is listed.

image

Now that we have that in place, we can start authoring our “silly” custom DSC resource.


The skeleton of our “silly” custom DSC resource

The previously-mentioned blog post already talks about the Resource Designer Tool in depth, so no need to repeat that content here. Let’s start with writing up our definition by starting a Windows PowerShell ISE session, and start to define our custom resource:

001 002 003 004 005 006 007 008 009 Import-Module xDSCResourceDesigner # Define my DSC parameters $Color = New-xDscResourceProperty -Name Color -Type String -Attribute Key -ValidateSet "Red", "Blue", "None" $Ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present", "Absent" $ColorFilePath = New-xDscResourceProperty -Name ColorFilePath -Type String -Attribute Write # Create the DSC resource New-xDscResource -Name "SillyDSCresource" -Property $Color, $Ensure, $ColorFilePath -Path "C:\Program Files\WindowsPowerShell\Modules\SillyDSCmodule" -ClassVersion 1.0 -FriendlyName "SillyDSCresource" -Force

I first need to import the DSC Resource Designer module. Then I need to define my parameters. In addition to my Color and ColorFilePath parameter, I also need to define the Ensure parameter. This takes care of the Present and Absent keywords I’ve mentioned earlier. Notice that I’m using ValidateSet to constrain my values, but besides that, I’m using normal attributes like Type to define my parameter types

On line 009, I invoke the creation of my new DSC resource, and specify its name, path and friendly name. Make sure that your Property values match your variables, or this will throw an error. This is likely to happen when you forget a parameter, and try to add it without using Update-DscResource, but decided to generate your DSC resource all over.

At this point, you could also update your MOF file directly in any editor if you know what to edit. If you do so, make sure that you validate it by running Test-DscSchema which returns a true  - we’re all good - or false - you probably made a syntax error somewhere. Now that we have our DSC schema in place, it's time to examine the files that were created by our New-xDscResource command. In your specified path, you will find a new DSC module and MOF file:

image

If we open our new DSC module in Windows PowerShell ISE, you will find our parameters, and the three core DSC TargetResource functions:

  • Get – this will get whatever we need to validate
  • Set – this will set whatever we need to set
  • Test – this will validate whatever it is we need to validate and will return a true or false state

The DSC data file

A DSC resource depends on the existence of a DSC data file, a Windows PowerShell psd1 file. The fastest and easiest way to create one is to copy an existing one and modify it. Since you’ve already installed the xDSCResourceDesigner module, you have a DSC data file which you can use.

image

Copy the xDSCResourceDesigner.psd1 file to your custom DSC module folder, and rename it to match your module name. So in my example, I’ve copied and renamed it to SillyDSCresource.psd1. Make sure that you locate it directly under your module folder at the same level as the DSCResources folder.

Make sure that you modify the values as highlighted in the example above. To generate a unique GUID, open up a new Windows PowerShell window, and run the command  [guid]::newguid() to generate a new GUID:

image


Authoring our “silly” DSC module

Now let’s start modifying our silly DSC module file, and open the module (psm1) file. We will work on the Get-TargetResource function first to make our param section more complete; we'll also configure our returnValue parameter:

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 #region GET Settings function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [Parameter(Mandatory)] [ValidateSet("Red", "Blue", "None")] [System.String]$Color, [Parameter(Mandatory)] [System.String]$ColorFilePath ) $returnValue = @{ Color = $Color Ensure = $Ensure } $returnValue } #endregion

Now we have that in place, we can start working on the most interesting part: the make it so section under Set-TargetResource. Here is where we set the desired value (if required). Since this is all PowerShell, we can even write to a log file to log our actions or do other things:

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 #region SET Settings function Set-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [Parameter(Mandatory)] [ValidateSet("Red", "Blue", "None")] [System.String]$Color, [Parameter(Mandatory)] [System.String]$ColorFilePath ) $ColorToEvaluate = (Get-Content $ColorFilePath) #In case the configuration is set to Present, the value of $ColorToEvaluate needs to be equal to $Color, if it's not, we make it so if (($Ensure -eq "Present") -and ($ColorToEvaluate -ne $Color)) { #write to log file for debugging Add-Content c:\log\silly_log.txt "Not compliant - Present defined, detected value is: $ColorToEvaluate, setting value to $Color" #Set the correct value and write to file Out-File C:\log\color.txt -inputobject $Color } #If the configration is set to Absent and IF the colors match, we will set it to "None" elseif (($Ensure -eq "Absent") -and ($ColorToEvaluate -eq $Color)) { Add-Content c:\log\silly_log.txt "Not compliant, Absent defined, detected non-compliant value is: $ColorToEvaluate, setting value to None" Out-File C:\log\color.txt -inputobject "None" } } #endregion

 

Finally, there's the Test-TargetResource section, where we validate our configuration, which returns true or false. This is also the place where you can add Write-Verbose commands to display output to the user, which shows up when you invoke the Start-DscConfiguration command:

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 #region TEST Settings function Test-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [Parameter(Mandatory)] [ValidateSet("Red", "Blue", "None")] [System.String]$Color, [Parameter(Mandatory)] [System.String]$ColorFilePath ) $ColorToEvaluate = (Get-Content $ColorFilePath) $bool = $false if (($Ensure -eq "Present") -and ($ColorToEvaluate -eq $Color)) { #Verbose output will be shown if the Start-DscConfiguration is being invoked Write-Verbose "Compliant: Present defined, value should be $Color, detected value is: $ColorToEvaluate" $bool = $true } elseif (($Ensure -eq "Present") -and ($ColorToEvaluate -ne $Color)) { Write-Verbose "Non-Compliant: Present defined, value should be $Color, detected value: $ColorToEvaluate, making it so" $bool = $false } elseif (($Ensure -eq "Absent") -and ($ColorToEvaluate -ne $Color)) { Write-Verbose "Compliant: Absent defined, value should NOT be $Color, detected value: $ColorToEvaluate" $bool = $true } elseif (($Ensure -eq "Absent") -and ($ColorToEvaluate -eq $Color)) { Write-Verbose "Non-Compliant: Absent defined, value should NOT be $Color, detected value: $ColorToEvaluate, correcting..." $bool = $false } $bool } #endregion Export-ModuleMember -Function *-TargetResource

 

Now that we have configured our three core DSC functions, we can save it to our module folder, and create a configuration to be deployed.


Creating our “silly” DSC configuration

With our Get, Set and Test DSC functions in place, it should be really straightforward to create a configuration:

001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 param ($MachineName="localhost") Configuration SillyTest { Import-DscResource -ModuleName SillyDSCmodule node $MachineName { SillyDSCresource MySillyTest { Ensure = 'Present' Color = 'Red' ColorFilePath = 'c:\DSC\color.txt' } } } $MOFpath = "c:\DSC\MOF" SillyTest -MachineName "localhost" -OutputPath $MOFpath Start-DscConfiguration -ComputerName 'localhost' -wait -force -verbose -path $MOFpath

Before we apply our DSC configuration, let’s make sure that we have a text file with a value in it, stored in our ColorFilePath:

image


Applying and testing our DSC configuration

When we run our DSC script, and have invoked Start-DscConfiguration, we can see our configuration being applied nicely:

image

Did you notice the verbose output as defined in the Test-TargetResource?

And we even have our log file as configured under the Set-TargetResource section, nice!

image

When we invoke Test-DscConfiguration, we should be good; let’s check if the value actually changed:

image

Nice!


Test driving configuration drift

Let’s see what happens if we change our configuration, we set Ensure to Absent, and we invoke Start-DscConfiguration:

001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 param ($MachineName="localhost") Configuration SillyTest { Import-DscResource -ModuleName SillyDSCmodule node $MachineName { SillyDSCresource MySillyTest { Ensure = 'Absent' Color = 'Red' ColorFilePath = 'c:\DSC\color.txt' } } } $MOFpath = "c:\DSC\MOF" SillyTest -MachineName "localhost" -OutputPath $MOFpath Start-DscConfiguration -ComputerName 'localhost' -wait -force -verbose -path $MOFpath

Remember that the value of our c:\DSC\color.txt file is still Red.

image

Ah…nice!

Let’s check our C:\DSC\color.txt value:

image

If you are surprised at the None outcome, revisit the Set-TargetResource section Smile (hint: look at line 035).

If we now change the value of C:\DSC\color.txt to Red, and run Test-DscConfiguration, we should see false as our output:

image

If we then run Start-DscConfiguration, this is nicely corrected again.


Wrapping up

I hope you found this “silly” DSC example useful. Stay tuned for a real-life example in the next blog post!

To wrap up, I’m including the full module here below. Until next time, happy automating!


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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 # Silly Custom DSC Resource # This is a silly custom DSC Resource for testing purposes only # It checks the content of a dummy text file, which should either contain "Red", "Blue" or "None" #region GET Settings function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [Parameter(Mandatory)] [ValidateSet("Red", "Blue", "None")] [System.String]$Color, [Parameter(Mandatory)] [System.String]$ColorFilePath ) $returnValue = @{ Color = $Color Ensure = $Ensure } $returnValue } #endregion #region SET Settings function Set-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [Parameter(Mandatory)] [ValidateSet("Red", "Blue", "None")] [System.String]$Color, [Parameter(Mandatory)] [System.String]$ColorFilePath ) $ColorToEvaluate = (Get-Content $ColorFilePath) #In case the configuration is set to Present, the value of $ColorToEvaluate needs to be equal to $Color, if it's not, we make it so if (($Ensure -eq "Present") -and ($ColorToEvaluate -ne $Color)) { #write to log file for debugging Add-Content c:\log\silly_log.txt "Not compliant - Present defined, detected value is: $ColorToEvaluate, setting value to $Color" #Set the correct value and write to file Out-File C:\DSC\color.txt -inputobject $Color } #If the configration is set to Absent and IF the colors match, we will set it to "None" elseif (($Ensure -eq "Absent") -and ($ColorToEvaluate -eq $Color)) { Add-Content c:\log\silly_log.txt "Not compliant, Absent defined, detected non-compliant value is: $ColorToEvaluate, setting value to None" Out-File C:\DSC\color.txt -inputobject "None" } } #endregion #region TEST Settings function Test-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory)] [ValidateSet("Present","Absent")] [System.String]$Ensure, [Parameter(Mandatory)] [ValidateSet("Red", "Blue", "None")] [System.String]$Color, [Parameter(Mandatory)] [System.String]$ColorFilePath ) $ColorToEvaluate = (Get-Content $ColorFilePath) $bool = $false if (($Ensure -eq "Present") -and ($ColorToEvaluate -eq $Color)) { #Verbose output will be shown if the Start-DscConfiguration is being invoked Write-Verbose "Compliant: Present defined, value should be $Color, detected value is: $ColorToEvaluate" $bool = $true } elseif (($Ensure -eq "Present") -and ($ColorToEvaluate -ne $Color)) { Write-Verbose "Non-Compliant: Present defined, value should be $Color, detected value: $ColorToEvaluate, making it so" $bool = $false } elseif (($Ensure -eq "Absent") -and ($ColorToEvaluate -ne $Color)) { Write-Verbose "Compliant: Absent defined, value should NOT be $Color, detected value: $ColorToEvaluate" $bool = $true } elseif (($Ensure -eq "Absent") -and ($ColorToEvaluate -eq $Color)) { Write-Verbose "Non-Compliant: Absent defined, value should NOT be $Color, detected value: $ColorToEvaluate, correcting..." $bool = $false } $bool } #endregion Export-ModuleMember -Function *-TargetResource