Creating a PowerShell DSC extension for your custom tasks
Following my post on automating your mundane Azure Virtual Machine Windows provisioning tasks with PowerShell DSC, it may happen that you have custom tasks that you want to do that that are not already available on PowerShell gallery. You may also have custom complex logic that you want to reuse across many different DSC scripts that you would like to centralize. This is where extensions come into play. It allows you to create custom resource operations that you can use within your nodes provisioning.
There are a few ways to create extensions:
- Using MOF-based resources in PowerShell
- Using Class-based resources in PowerShell
- Using Composite resources in PowerShell
- Using MOF-based resources in C#
- Using the Resource Designer Tool
Today, I will show you how this can be done using the Resource Designer Tool exposed by the xDscResourceDesigner.
Setup
First thing is to import (or install) the xDscResourceDesigner module. you can do so by running the following:
1 2 3 4 5 6 |
if (!(Get-Module -Name xDSCResourceDesigner)) { if (!(Get-InstalledModule -Name xDSCResourceDesigner -ErrorAction SilentlyContinue)) { Install-Module -Name xDSCResourceDesigner } Import-Module -Name xDSCResourceDesigner } |
Creating the module
The module will be created in the folder C:\Program Files\WindowsPowerShell\Modules
. This will allow us to use the Import-DscResource -ModuleName <dscModuleName>
statement as this path is in the paths contained in the environment variable used by PowerShell to resolve the modules,
$env:PSModulePath.
The module needs a module manifest. A module manifest is a PowerShell data file (.psd1) that describes the contents of a module and determines how a module is processed1. The PowerShell snippet below does exactly that. It uses the New-ModuleManifest cmdlet. Remember that a module can have multiple individual resources in it.
C:\Program Files\WindowsPowerShell\Modules
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$modules = 'C:\Program Files\WindowsPowerShell\Modules' $moduleName = 'xHelloWorldPSDesiredStateConfiguration' $description = 'This module includes all the DSC resources that will be part of the Hero HelloWorld' $moduleVersion = '0.1.0.0' $author = "Dominique St-Amand" $companyName = "DSA" if (!(Test-Path (Join-Path -Path $modules -ChildPath $moduleName))) { $moduleFolder = mkdir (Join-Path -Path $modules -ChildPath $moduleName) New-ModuleManifest -Path (Join-Path -Path $moduleFolder -ChildPath "$moduleName.psd1") ` -Guid $([system.guid]::newguid().guid) ` -Author $author ` -CompanyName $companyName ` -Copyright (Get-Date).Date.Year ` -ModuleVersion $moduleVersion ` -Description $description ` -PowerShellVersion '5.1' ` -DscResourcesToExport @($resourceName) } |
Creating the resource
Now we want to create our HelloWorld resource. You can name it the way you want, but to respect the fact that this is an extension, I put an x before the resource name.
1 2 3 4 5 6 7 |
$resourceName = "xHelloWorld" $Path = New-xDscResourceProperty -Name DestinationPath -Type String -Attribute Key -Description "The path of the file where the content will be written to" $Content = New-xDscResourceProperty -Name Content -Type String -Attribute Write -Description "The content to write into the file" $Ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present", "Absent" -Description "Determines whether the file and Contents at the Destination should exist or not. Set this property to Present to ensure the file exists. Set it to Absent to ensure they do not exist. The default value is Present." $Properties = @($Path, $Content, $Ensure) New-xDscResource -Name $resourceName -Property $Properties -FriendlyName $resourceName -ModuleName $moduleName -ClassVersion $moduleVersion -Path $modules |
As you can see in the snippet above, we create 3 property parameters for our resource: Path, Content and Ensure. We also have an attribute on each of our property parameters. The attribute determines the type of parameter your property parameter will be. The possible values are:
- Key: determines that this is the unique key to identify the resource
- Required: determines that the property is required
- Read: determines that the property is a readonly property
- Write: determines that the property can written to, that is that it can be specified in the configuration
As mentioned, each resource needs a key property to uniquely identify the resource, in case you use it more than once in the same configuration. In the example above, the Path property is the key. We also have 2 write properties, that is Content and Ensure.
Final script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
$ErrorActionPreference = 'stop' if (!(Get-Module -Name xDSCResourceDesigner)) { if (!(Get-InstalledModule -Name xDSCResourceDesigner -ErrorAction SilentlyContinue)) { Install-Module -Name xDSCResourceDesigner } Import-Module -Name xDSCResourceDesigner } $modules = 'C:\Program Files\WindowsPowerShell\Modules' $moduleName = 'xHelloWorldPSDesiredStateConfiguration' $description = 'This module includes all the DSC resources that will be part of the Hero HelloWorld' $moduleVersion = '0.1.0.0' $author = "Dominique St-Amand" $companyName = "DSA" $resourceName = "xHelloWorld" if (!(Test-Path (Join-Path -Path $modules -ChildPath $moduleName))) { $moduleFolder = mkdir (Join-Path -Path $modules -ChildPath $moduleName) New-ModuleManifest -Path (Join-Path -Path $moduleFolder -ChildPath "$moduleName.psd1") ` -Guid $([system.guid]::newguid().guid) ` -Author $author ` -CompanyName $companyName ` -Copyright (Get-Date).Date.Year ` -ModuleVersion $moduleVersion ` -Description $description ` -PowerShellVersion '5.1' ` -DscResourcesToExport @($resourceName) } if (!(Test-Path (Join-Path -Path $modules -ChildPath "$moduleName\DSCResources\$resourceName"))) { $Path = New-xDscResourceProperty -Name DestinationPath -Type String -Attribute Key -Description "The path of the file where the content will be written to" $Content = New-xDscResourceProperty -Name Content -Type String -Attribute Write -Description "The content to write into the file" $Ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present", "Absent" -Description "Determines whether the file and Contents at the Destination should exist or not. Set this property to Present to ensure the file exists. Set it to Absent to ensure they do not exist. The default value is Present." $Properties = @($Path, $Content, $Ensure) New-xDscResource -Name $resourceName -Property $Properties -FriendlyName $resourceName -ModuleName $moduleName -ClassVersion $moduleVersion -Path $modules } |
Adding some meat into the resource
Now that the module and the resource are created, it is time to add some logic in the xHelloWorld resource. If you navigate into the folder C:\Program Files\WindowsPowerShell\Modules\xHelloWorldPSDesiredStateConfiguration\DSCResources\xHelloWorld
, you will find the file xHelloWorld.psm1
. In it, you will see there’s 3 functions: Get-TargetResource, Set-TargetResource and Test-TargetResource. In each, you have some comments about what the function can return (or expects as a return value) and other useful information. If you remember, the Script resource has basically the same structure:
- Get-TargetResource: a function that returns the current state of the Node
- Set-TargetResource: a function that DSC uses to enforce compliance when the Node is not in the desired state
- Test-TargetResource: a function that determines if the Node is in the desired state
So in each, you fill in the mandatory code to satisfy the above conditions. In our case, we could have the following in our 3 functions:
Get-TargetResource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Write-Verbose "Checking if the file contains the content" $returnValue = @{ DestinationPath = $DestinationPath Content = $Content } $file = Get-Content $DestinationPath $containsContent = $file | Foreach-Object { $_ -match $Content } if ($containsContent -contains $true) { $returnValue.Add("Ensure","Present") } else { $returnValue.Add("Ensure","Absent") } $returnValue |
Test-TargetResource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Write-Verbose "Testing to see if the file contains the content" $result = [System.Boolean] $file = Get-Content $DestinationPath $containsContent = $file | Foreach-Object { $_ -match $Content } if ($containsContent -contains $true) { Write-Verbose "The content exist in the file" $result = $true } else { Write-Verbose "The content does not exist in the file" $result = $false } $result |
Set-TargetResource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
$file = Get-Content $DestinationPath $containsContent = $file | Foreach-Object { $_ -match $Content } if ($Ensure -eq "Present") { if (!($containsContent -contains $true)) { Write-Verbose "The content does not exists in the file. Adding" $newContent = $containsContent + $Content Set-Content -Path $DestinationPath -Value $newContent -Force } else { Write-Verbose "The content exist in the file. Skipping" } } else { if ($containsContent -contains $true) { Write-Verbose "The content exists in the file. Removing" $newContent = $file | Foreach-Object { if (!($_ -match $Content)) { return $_ } } Set-Content -Path $DestinationPath -Value $newContent -Force } else { Write-Verbose "The content does not exist in the file. Status Quo" } } |
Testing and using the resource
To test the resource schema, there’s a cmdlet that is available to you called Test-xDscSchema. That cmdlet can be used to test the validity of a MOF schema that you have written manually. Call the Test-xDscSchema
cmdlet, passing the path of a MOF resource schema as a parameter. The cmdlet will output any errors in the schema2.
To use the newly created resource, create a DSC configuration and import the module in which the resource lives in. In our case, it would be Import-DscResource -ModuleName xHelloWorldPSDesiredStateConfiguration. You can then use the resource operation in your Node configuration:
1 2 3 4 5 |
xHelloWorld MyHelloWorld { DestinationPath = "C:\Temp\HelloWorld.txt" Ensure = "Present" Contents = "Hello World from DSC!" } |
If you load the module into PowerShell (5.1), using Import-Module "C:\Program Files\WindowsPowerShell\Modules\xHelloWorldPSDesiredStateConfiguration\xHelloWorldPSDesiredStateConfiguration.psd1"
, you can verify that your DSCResource exists by running Get-DscResource -Module xHelloWorldPSDesiredStateConfiguration
.
Updating the resource
If you want to make modifications to your resource down the road, such as adding a new parameter, you can do so by using the Update-xDscResource cmdlet. Assume we want to add a String property to the xHelloWorld resource called EndOfLineCharacter. We can do this with the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Initial properties $Path = New-xDscResourceProperty -Name DestinationPath -Type String -Attribute Key -Description "The path of the file where the content will be written to" $Content = New-xDscResourceProperty -Name Content -Type String -Attribute Write -Description "The content to write into the file" $Ensure = New-xDscResourceProperty –Name Ensure -Type String -Attribute Write –ValidateSet "Present", "Absent" -Description "Determines whether the file and Contents at the Destination should exist or not. Set this property to Present to ensure the file exists. Set it to Absent to ensure they do not exist. The default value is Present." # New property $EndOfLineCharacter = New-xDscResourceProperty –Name EndOfLineCharacter –Type String –Attribute Write –Description "Determines the end of line that will be written to the file. Defaults to \r\n" # Property array $Properties = @($Path, $Content, $Ensure, $EndOfLineCharacter) # Path to the folder containing .psm1 and .schema.mof files. $resourcePath = "C:\Program Files\WindowsPowerShell\Modules\xHelloWorldPSDesiredStateConfiguration\DSCResources\xHelloWorld" # Update by path. This will overwrites files without prompting Update-xDscResource –Path $resourcePath –Property $Properties -ClassVersion 0.1.1.0 -Force |
After adding the property parameter, do not forget to update your psm1 file to use your new parameter!
You will also want to update your module version. To do that, you can do:
1 2 3 4 5 6 |
$Params = @{ Path = "C:\Program Files\WindowsPowerShell\Modules\xHelloWorldPSDesiredStateConfiguration\xHelloWorldPSDesiredStateConfiguration.psd1" ModuleVersion = "0.1.1.0" } Update-ModuleManifest @Params |
Conclusion
As you can see, using the designer, it is easy to create a new DSCResource. If you are more of a C# person, definitely try out the other methods as mentioned above.
If you believe your modules and extensions can be of use to everyone else, but sure to publish them on PowerShell Gallery!
Happy DSC-ing!