We have touched on how to create PowerShell c4 DSC Resources briefly as we have worked our way through this book, and here, we will dive into the details of doing so in depth. We will cover the folder structures and files needed as well as the mindset and best practices necessary in order to make custom DSC Resources that are effective and useful to you.
A v4 DSC Resource has a strict folder structure. By convention, each PowerShell module that contains one or more DSC Resources is stored in the $env:ProgramFilesWindowsPowerShellModules
folder. You can store DSC Resources in any of the standard $env:PSModulePath
paths, or you can store them in a location of your choosing, so long as you append the location to the $env:PSModulePath
variable before compiling the Resource.
The strict folder structure for v4 DSC Resources is as shown:
$env:ProgramFilesWindowsPowerShellModules |- ExampleModule |- ExampleModule.psd1 |- DSCResources |- Example_InstallFoo |- Example_InstallFoo.psd1 (optional) |- Example_InstallFoo.psm1 (required) |- Example_InstallFoo.schema.mof (required) |- Example_ConfigureFoo |- Example_ConfigureFoo.psd1 (optional) |- Example_ConfigureFoo.psm1 (required) |- Example_ConfigureFoo.schema.mof (required)
A folder called DSCResources
inside the root folder, with other folders that are also DSC Resources themselves, and files with schemas separate from the code module files? Like with the DSC composite resource, you must be wondering why they make this so hard! It is difficult to keep straight at first, but once you have done one or two DSC Resources, it's not really that hard to keep straight. It is easier to understand once you see that each DSC Resource is made up of one or more PowerShell modules.
A DSC Resource contains one or more PowerShell modules inside a "root" PowerShell module. This root PowerShell module declares the DSC Resource and has the information necessary for the DSC engine to load the modules into memory. The root module typically does not contain any code itself, as it won't be available without significant effort to the submodules. Each submodule in the DSCResources
folder represents an actual DSC Resource. Inside each module folder is a .psm1
file and a .mof
file. The .psm1
file is the file that contains all of the executable code that DSC uses to determine and set the state on the target node. The .mof
file contains schema data that describes the parameters in use for a particular DSC Resource.
Let's look at an example DSC Resource provided by Microsoft instead of a contrived example like the preceding folder structure. We will look at xTimeZone
as it is a relatively simple resource that has one submodule and few files. Inside the root module folder, we can see the DSCResources
folder that holds all the submodules, as shown:
[PS]> ls $env:ProgramFilesWindowsPowerShellModulesxTimeZone Directory: C:Program FilesWindowsPowerShellModulesxTimeZone Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 7/9/2015 10:11 PM DSCResources -a--- 7/9/2015 10:11 PM 969 xTimeZone.psd1 -a--- 7/9/2015 10:11 PM 1988 xTimeZone.Tests.ps1
As you can see listed in the preceding structure, the main DSC Resource folder also contains a folder called DSCResources
. The DSCResources
folder contains all the actual DSC Resource modules. The "root" folder, in this case xTimezone
, is our container module. It holds all the information needed to discover and parse DSC Resources, namely the xTimeZone.psd1
file. This is the basic PowerShell module format and function here, nothing new. The DSCResources
subfolder is a special folder; the DSC engine looks for that folder, and if present, will know to enumerate that directory for DSC Resources to load.
Looking at the DSCResources
folder, we see the following structure:
[PS]> ls $env:ProgramFilesWindowsPowerShellModulesxTimeZoneDSCResources Directory: C:Program FilesWindowsPowerShellModulesxTimeZoneDSCResources Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 7/9/2015 10:11 PM xTimeZone
As we can see in the preceding listing, the DSCResources
folder contains all the actual DSC Resource module folders. In this case, we see a folder called xTimeZone
, which is the same name of the root module. In our next example, we'll see one that diverges from this and find out why it does, but for now, let's continue down the path we're on.
Looking at the xTimeZone
folder, we see two files, at which point we have come to the last stop in our tour of a DSC Resource:
[PS]> ls $env:ProgramFilesWindowsPowerShellModulesxTimeZoneDSCResourcesxTimeZone Directory: C:Program FilesWindowsPowerShellModulesxTimeZoneDSCResourcesxTimeZone Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 7/9/2015 10:11 PM 3194 xTimeZone.psm1 -a--- 7/9/2015 10:11 PM 173 xTimeZone.schema.mof
These two folders contain the actual meat of a DSC Resource, and hold all information needed for the DSC engine to parse and use a DSC Resource on a target node. The xTimeZone.psm1
file holds the PowerShell code that operates on the target node. The xTimeZone.schema.mof
file holds the MOF schema that defines the parameter statement for the DSC Resource. We will cover these two files in detail in the following section.
The PowerShell v4 DSC Resource syntax can be described by dividing it up into the DSC Resource definition file and the DSC Resource PowerShell module file.
A DSC Resource definition file is an MOF schema file that has a specific CIM syntax. It is at first very obtuse and unfriendly to look at, but becomes easier to read the more you work with it. The MOF files are supposed to be easy for the machine to read and not you, so there is some initial effort in understanding the syntax and format for them. Don't worry too much about this as you aren't meant to look at it yourself much. MOF schema files are expected to be automatically created and updated using the DSC tooling Microsoft has released.
The main purpose of a DSC Resource MOF schema file is to provide the name and version of the DSC Resource and to declare the parameters the DSC Resource will accept. An example of a DSC Resource MOF schema file is as shown:
[ClassVersion("1.0.0"), FriendlyName("InstallFoo")] class Example_InstallFoo: OMI_BaseResource { [Key] string Name; [Required] string PhysicalPath; [write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] string Ensure; };
If you are reading CIM syntax, class names, and writeable values and starting to sweat, don't worry. Your fears will be allayed in the Authoring custom DSC Resources section.
There is still benefit in going over the syntax of the MOF schema file, as you might have to troubleshoot issues in parsing when creating your own DSC Resources or troubleshoot DSC Resources created by others. If this section is getting through to you, you can reference it later when the time comes.
There are two DSC Resource names to keep track of, the CIM class name (sometimes referred to as the fully qualified name) and the friendly name. The CIM class name is the name that DSC uses internally to refer to a DSC Resource, and the friendly name is typically a shorter and easier to read version for a human.
You have to be careful in specifying these names, as your choice in which one to include determines how your users ask for your DSC Resource. If you specify the same name for both, then a user uses that name when they ask for the DSC Resource to be imported. If you specify the CIM class name but not the friendly name, then the user has to specify the CIM class name. If you specify the friendly name, then the user must use that to import your DSC Resource and cannot use the CIM class name. If all this sounds confusing, do like the author does and keep it simple by setting both names the same when authoring DSC Resources.
We can simplify naming the DSC Resource in the MOF example we used earlier, as shown:
[ClassVersion("1.0.0"), FriendlyName("InstallFoo")] class InstallFoo: OMI_BaseResource { [Key] string Name; [Required] string PhysicalPath; [write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] string Ensure; };
Why the complicated naming? It was explained that these different names account for situations where DSC Resources are named the same and allow the user to specify which ones to load and use. As we will see in the Creating a custom PowerShell v5 DSC Resource section, this concept has been simplified greatly and is easier to use.
The second piece of information to include is the DSC Resource version. Versioning your DSC Resources is very important during the lifetime of use of your DSC Resource. Along with the DSC Resource name, users can specify an exact version to load. This ensures they will get the exact DSC Resource they are expecting.
The most important piece of information in the DSC MOF schema file is the property declarations.
As we will cover in The DSC PowerShell module file coming up, the parameters or properties the DSC Resource accepts have to match each of the TargetResource
functions. This rule also applies to the MOF schema file.
The parameter
statement has the same information as the parameter
statement in the TargetResource
functions, but it looks drastically different. This is because it is using the more verbose CIM syntax, which is less pleasant on the eyes than the PowerShell syntax. There are a few rules to keep in mind when specifying the parameter set here, and we will cover those in the next section when we author our own DSC Resources.
Each parameter can have one or more qualifiers: Key
, Required
, Read
, and Write
. These qualifiers describe what the parameter is and what it is allowed to do, listed as follows:
Key
qualifier indicates that the parameter is mandatory and is used to uniquely identify the DSC Resource in the DSC configuration script file. This means that the value for this parameter has to be unique in the script file and has to be singular, as well; it cannot be an array or a collection. It must also be a string or numeral value, it cannot be any other type. All DSC Resources are required to have at least one Key
property. For DSC Resources with more than one Key
property defined, DSC uses a combination of all Key
properties put together as the Key
qualifier used.Required
qualifier indicates that the parameter is mandatory but is not required to be unique for the DSC Resource.Write
qualifier is used to pass information and values to the DSC Resource, but is not required for successful execution of the DSC Resource. If used, this parameter is not mandatory.Read
qualifier is only used in the Get-TargetResource
function, as it returns information only.The DSC PowerShell module file (or implementation file) is the file where the actual code that determines a target node's state resides. The DSC PowerShell module file has the same name as what was declared as the CIM class name in the DSC Resource definition file.
Each DSC Resource module file (the PowerShell .psm1
file) is required to contain three functions with specific names in order to work. Each function is required to have the same parameters that were defined in the DSC Resource definition file. Any deviation in parameter set declarations (typos, missing or added parameters, conflicting types, and so on) between functions is not allowed.
Adding up all the places in which you must keep track of parameter statements ultimately makes four places you need to keep track of. If this sounds to you like a recipe for syntax errors and forgotten parameters, then you aren't alone. Keep this in mind when we get to the next section on v5 DSC Resources.
The Get-TargetResource
function is responsible for reporting on the current state of the system. This function will never change the system; it is read-only. Instead, it inspects the system state and returns a PowerShell hash containing the information obtained.
This function is required to implement all Key
and Required
properties defined in the DSC Resource definition file. The Write
(non-mandatory) parameters do not have to be included. In practice, many people include all properties in an effort to not miss out on including important ones.
A Get-TargetResource
function is required to return a single PowerShell hashtable that contains the information collected from the target system. This hashtable must contain all parameters that were defined in the DSC Resource definition file, as well as their corresponding values.
The Test-TargetResource
function is similar to the Get-TargetResource
function in that it inspects the current state of the system, but it takes things one step further. After inspecting the current state of the system, it compares it to the expected values provided by the user. Depending on the way the values are specified, it tests to see if they match.
This function is required to implement all Key
, Required
, and Write
parameters defined in the DSC Resource definition file. Key
and Required
properties are required to be defined as mandatory parameters.
The Test-TargetResource
function always returns a Boolean
value of either $true
or $false
. If the target node is in the desired state, the function should return $true
. If the target node is not in the desired state, it should return $false
.
The Set-TargetResource
function actually brings the state of the system to the desired state. This is the place where idempotency comes into play; the PowerShell code here is responsible for detecting the current state and only changing it if it does not match what it is expected.
This function is required to implement all Key
, Required
, and Write
parameters defined in the DSC Resource definition file. Key
and Required
properties are required to be defined as mandatory parameters.
A Set-TargetResource
function does not return any value to the calling code. This function either succeeds or fails; it does not indicate any state in between. Failure is indicated by throwing an exception with message text indicating the failure. Success is indicated by the function completing without error.
Optionally, a Set-TargetResource
function can support the WhatIf
PowerShell behavior and provide a summary of what would be changed on a target node without actually changing the target node state. The function can support this by implementing the PowerShell's SupportsShouldProcess
feature.
If the target node must be restarted after Set-TargetResource
has successfully implemented the changes it was instructed to do, it can indicate this to the DSC engine by setting the global variable $global:DSCMachineStatus
to 1
before exiting.
Now that we have covered the syntax and structure of DSC Resources, we can finally cover how to make your own DSC Resources. The fun begins!
The secret to creating DSC Resources manually is: to not create DSC Resources manually. Seriously; there are easier ways and you needn't put yourself through the pain. As with most things that have a lot of easy-to-reproduce structure, we can use tools to perform most of the grunt work for us. This is not just to avoid doing extra work; this removes the possibility of getting something wrong due to human error.
While you can create DSC Resources manually by creating the files and folders required by hand, you do not have to. Microsoft released a PowerShell module as part of their DSC Resource Kit that automates creating DSC Resources called xDscResourceDesigner
.
Using the xDscResourceDesigner
module requires a little upfront thought and planning, but returns that investment in making DSC Resources that are syntactically correct and properly arranged easily and without much fuss.
Before you start to create your DSC Resource, you must make a decision on the following points:
Once you have this information decided in creating the DSC Resource, using the xDscResourceDesigner
module is as easy as these two steps:
DscResourceProperty
(or, as we have been calling them, parameters) using the New-xDscResourceProperty
Cmdlet.New-DscResource
Cmdlet.Putting them all together, here is an interactive session that creates the xExampleResource
with two parameters:
[PS]> $Ensure = New-xDscResourceProperty -Name "Ensure" -Type "String" -Attribute Write -ValidateSet "Present","Absent" -Description "Ensure Present or Absent" [PS]> $ExampleSetting = New-xDscResourceProperty -Name "ExampleSetting" -Type "String" -Attribute Key -Description "Set an Example Setting" [PS]> New-xDscResource -Name "xExampleResource" -Property $Ensure,$ExampleSetting -Path "$env:ProgramFilesWindowsPowerShellModulesxExampleResource" -ClassVersion 1.0 -FriendlyName "Example" -Force Directory: C:Program FilesWindowsPowerShellModules Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 7/9/2015 9:57 PM xExampleResource Directory: C:Program FilesWindowsPowerShellModulesxExampleResource Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 7/9/2015 9:57 PM DSCResources Directory: C:Program FilesWindowsPowerShellModulesxExampleResourceDSCResources Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 7/9/2015 9:57 PM xExampleResource
The preceding code creates the following resource:
[PS]> ls $env:ProgramFilesWindowsPowerShellModulesxExampleResourceDSCResourcesxExampleResource Directory: C:Program FilesWindowsPowerShellModulesxExampleResourceDSCResourcesxExampleResource Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 7/16/2015 9:57 PM 3004 xExampleResource.psm1 -a--- 7/16/2015 9:57 PM 594 xExampleResource.schema.mof
Looking at xExampleResource.schema.mof
, we see New-xDscResource
did all the heavy lifting for us and not only set up the CIM class for us but also correctly set all the parameter statement information we needed:
[ClassVersion("1.0"), FriendlyName("Example")] class xExampleResource : OMI_BaseResource { [Write, Description("Ensure Present or Absent"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; [Key, Description("Set an Example Setting")] String ExampleSetting; };
Inspecting xExampleResource.psm1
, we see that New-xDscResource
created stub TargetResource
functions for us with example code. More importantly, it added the parameters in the correct formats to each function:
function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [parameter(Mandatory = $true)] [System.String] $ExampleSetting ) #Write-Verbose "Use this cmdlet to deliver information about command processing." #Write-Debug "Use this cmdlet to write debug information while troubleshooting." <# $returnValue = @{ Ensure = [System.String] ExampleSetting = [System.String] } $returnValue #> } function Set-TargetResource { [CmdletBinding()] param ( [ValidateSet("Present","Absent")] [System.String] $Ensure, [parameter(Mandatory = $true)] [System.String] $ExampleSetting ) #Write-Verbose "Use this cmdlet to deliver information about command processing." #Write-Debug "Use this cmdlet to write debug information while troubleshooting." #Include this line if the resource requires a system reboot. #$global:DSCMachineStatus = 1 } function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [ValidateSet("Present","Absent")] [System.String] $Ensure, [parameter(Mandatory = $true)] [System.String] $ExampleSetting ) #Write-Verbose "Use this cmdlet to deliver information about command processing." #Write-Debug "Use this cmdlet to write debug information while troubleshooting." <# $result = [System.Boolean] $result #> } Export-ModuleMember -Function *-TargetResource
So, we have seen that the Cmdlets from the xDscResourceDesigner
module are really useful in making the authoring of custom DSC Resources easier and quicker, while at the same time maintaining correct syntax and structure. This is all well and fine if we know ahead of time all the parameters and properties we want to use in our DSC Resource. What happens if we use these cmdlets, write a bunch of code, and then find out we need another parameter? Do we have to manually add it ourselves and risk getting the syntax wrong? Never fear; Update-xDscResource
is here!
To use Update-xDscResource
, all we have to do is create a new variable for our new parameter and point Update-xDscResource
at it, and it will update the existing DSC Resource with our new parameter, as shown:
[PS]> $ExampleFooBar = New-xDscResourceProperty -Name "FooBar" -Type "String" -Attribute Write -Description "Set an Example FooBar" [PS}> Update-xDscResource -Name "xExampleResource" -Property $ExampleFooBar,$Ensure,$ExampleSetting -ClassVersion 1.0 -Force
Looking at the MOF schema file, we see our new parameter added:
[ClassVersion("1.0"), FriendlyName("Example")] class xExampleResource : OMI_BaseResource { [Write, Description("Set an Example FooBar")] String FooBar; [Write, Description("Ensure Present or Absent"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; [Key, Description("Set an Example Setting")] String ExampleSetting; };
If you look back at our Update-xDscResource
statement, you'll notice we specified our previous variables as well as our new one. This is the one inconvenience with using Update-xDscResource
; you have to provide the same set of parameters as you did when you first created the DSC Resource when you want to update it with new ones. This is not too much of an imposition when you are actively making a DSC Resource, but becomes more of a problem when you come back to a DSC Resource after months elapse. You most likely won't have saved the text you used to create the DSC Resource and will have to create it all over again.
While you are in active development of a DSC Resource, you could keep around in a text file the commands you used to create it for easier updating later. You could also serialize your parameter statement to a file using the Export-Clixml
Cmdlet and then reimport it at a later time using Import-Clixml
:
$params = $Ensure,$ExampleSetting,$FooBar $params | Export-Clixml –Path .myDscParams.xml # time passes $params = Import-Clixml –Path .myDscParams.xml Update-xDscResource -Name "xExampleResource" -Property $params -ClassVersion 1.0 –Force
How you choose to handle this depends largely on how much you expect your DSC Resource properties to change in the future.