April 2015

Volume 30 Number 4


Windows PowerShell - Authoring Desired State Configuration Custom Resources

By Ritesh Modi | April 2015

Desired State Configuration (DSC) is a new configuration management and deployment platform from Microsoft. It’s built on Common Information Model (CIM), Web Services for Management (WSMAN) industry standards and is an extension to Windows PowerShell.

You can declaratively write DSC scripts within any Windows PowerShell console, generate DSC objects and execute them on target servers. DSC lets you configure, monitor and ensure server compliance using these configuration documents.

Writing a configuration is like defining a policy for a target server. The policy contains a group of resources along with their desired state and those are applied to the target server. DSC ensures each server complies with the policy. Read more about DSC at bit.ly/1iDeRcF.

Resources are the basic building blocks of DSC. They’re the most granular, reusable, distributable and shareable component of the DSC infrastructure. They also provide the lowest level of control within DSC and are used to author DSC documents to configure infrastructure. DSC resources can perform anything possible using Windows PowerShell, such as configuring and managing IIS, Windows Remote Management (WinRM), registry, Windows features and services.

What good is a technology these days if it doesn’t allow for extension? DSC follows the Open Closed Principle. It’s open for extension, but closed for modification. It provides the necessary extension hooks for authoring new resources.

In this article, I’ll show you how to develop a DSC custom resource and use it in a sample DSC configuration. The custom resource is named TrustedHosts. This resource will manage the WinRM trusted host configuration—adding or removing computer names to the WinRM TrustedHosts property.

Workgroup servers (not part of any domain) that want to communicate using Windows PowerShell remoting will need additional WinRM configuration. Specifically, I’ll add the names of client machines to the WinRM TrustedHosts list to allow remoting from those machines.

This resource would be responsible for managing a single computer name in the TrustedHosts list. However, if you need multiple computer names, you can add multiple resource sections using this resource within the configuration document. You could also modify the accompanied resource.

You can view the Trusted Hosts configuration by executing Get-Item command and using WSMAN provider:

Get-Item WSMan:\localhost\client\TrustedHosts

Similarly, you can modify the TrustedHosts property by executing the following command through the Windows PowerShell console:

Set-Item WSMan:\localhost\client\TrustedHosts –Value "*.contoso.com" –force

It’s important to know the building blocks and concepts required to build a custom resource. The concepts related to CIM, Managed Object Format (MOF), Windows PowerShell and its modules, and WinRM are the primary technologies upon which DSC resources are built.

DSC Resources

Before getting into implementing the custom resource, it’s important to know how to package and deploy it on your servers. DSC resources are contained within Windows PowerShell modules, which let you reuse and distribute code. You can implement modules either as Windows PowerShell scripts or compiled binaries using languages such as C#.

DSC uses the module infrastructure to host its resources. Windows PowerShell modules should reside at pre-determined folder locations on the file system. You can view these locations by dereferencing the Windows PowerShell variable $env:ModulePath from any Windows PowerShell console. Windows PowerShell installs all out-of-box modules at C:\Windows\System32\Windows­PowerShell\v1.0\modules. You should deploy all custom modules at C:\Program Files\WindowsPowerShell\modules. For the Trusted­Hosts custom resource, use C:\Program Files\WindowsPowerShell\Modules as the module base path and hosting container for the WinRM Windows PowerShell module. You’ll create a new folder named WinRM within this folder.

DSC expects resources to follow rules related to file and folder structure within a Windows PowerShell module. All DSC resources should be placed within the DSCResources folder in the module. This is the root folder containing all the related resources. Within the WinRM folder, create another folder called DSCResources. This folder hosts all DSC resources. Create one folder for each resource within this folder. In this case, you’re creating a single resource and, therefore, just one folder named TrustedHosts. The folder and resource should have the same name.  This resource-specific folder TrustedHosts contains files specific to the resource. The Folder structure for the TrustedHosts resource is shown in Figure 1.

Windows PowerShell Desired State Configuration Resource Folder and File Specification
Figure 1 Windows PowerShell Desired State Configuration Resource Folder and File Specification

The files that define and provide resource implementation are:

  1. <<ResourceName>>.psm1—This is the resource implementation file. In this case, it’s TrustedHosts.psm1.
  2. <<ResourceName>>.psd1—This is the resource metadata file. In this case, it’s TrustedHosts.psd1. You don’t need this file if you export mandatory functions from .psm1 script file.
  3. <<ResourceName>>.Schema.mof—This is the resource definition. In this case, it’s TrustedHosts.Schema.mof.

Every Windows PowerShell module has a module manifest file with the same name as the module itself. Generate a module manifest file using New-ModuleManifest cmdlet from the Windows PowerShell console using elevated privileges, like so:

New-ModuleManifest -Path "C:\Program Files\WindowsPowerShell\Modules\WinRM\WinRM.psd1"

Now that you understand the folder and file structure rules for packaging a DSC resource, I’ll focus on the implementation files.

TrustedHosts.Schema.mof

You’ll use MOF files to define classes CIM/WMI uses to generate their instances for enabling management information exchange within datacenters. The MOF class defines a DSC resource along with its properties. The properties can include Read-Write, ReadOnly and required attributes. There should always be a Key property for CIM/WMI to uniquely identify the objects. Properties are assigned values and they eventually become the desired state of the resource to be monitored and maintained. You can generate MOF files through an MOF designer or author them through any text editor such as Notepad because they’re essentially text files.

Before creating the MOF class, analyze the information you need to manage in the TrustedHosts configuration on any machine. For managing TrustedHosts on a server, you need three properties: ComputerName to add to the TrustedHosts list; the Ensure property to determine whether ComputerName should be added or removed; and credentials to access and manage the WinRM TrustedHosts configuration.

Then an MOF class for TrustedHosts DSC resource is created, as shown here:

[ClassVersion ("1.0.0"), FriendlyName ("TrustedHosts")]
Class TrustedHosts: OMI_BaseResource
{
  [Key, Description ("Name of the host")] String ComputerName;
  [Write, ValueMap {"Present", "Absent"}, 
    Values {"Present", "Absent"}] string Ensure;
  [Write, EmbeddedInstance ("MSFT_Credential")] string Credential;
};

It’s important to know how the MOF class fits into the overall CIM ecosystem. All DSC resources are derived from the OMI_BaseResource abstract CIM class. This base class is defined at the root\Microsoft\Windows\DesiredStateConfiguration namespace and provides common properties required for all DSC resources.

These properties are ResourceID, SourceInfo, ModuleName, ModuleVersion and DependsOn. During MOF file generation, you’ll add these properties to all resource properties. These are DSC-specific internal properties and used only by DSC. The Class TrustedHosts would also become part of the root\Microsoft\Windows\DesiredStateConfiguration namespace. All three properties are of type string and they represent the WinRM TrustedHosts resource definition.

The ComputerName property has additional metadata—Key and Description. Key is a type qualifier and indicates this property is mandatory and the value of this property should be unique across all instances. There should be at least one property with key metadata within the MOF class. There can be multiple properties with Key metadata. This would create a composite key and they should both be unique across all instances. I made this property as Key because otherwise, I can’t determine whether the computer name already exists in TrustedHosts list.

Description provides textual meaning to the property. The Ensure property is annotated with ValueMap. ValueMap is a set of values the property can accept and is synonymous to an Enumeration. In this case, Ensure can accept either “Present” or “Absent” as its legal value. Both the Ensure and Credential properties are decorated with the write attribute. That means you can write into and modify this property when using the custom resource in a configuration script.

The class TrustedHosts also has metadata—ClassVersion and FriendlyName. The class version is helpful for maintaining multiple versions of the same MOF class. FriendlyName is important metadata because configuration scripts use it to refer to resources. The configuration scripts recognize the resources through their friendly name. In case of the TrustedHosts class, both the class name and friendly name are the same. You can read more about MOF file definition and its metadata at bit.ly/1AEHLTj.

After defining the MOF class, persist it as ASCII or Unicode format on the file system with the file name TrustedHosts.Schema.mof within the TrustedHostsfolder created earlier. When you use this custom resource in a DSC configuration, you’ll be assigning values to these properties to indicate the desired configuration state.

TrustedHosts.psm1

Now I’ll cover the most crucial aspect of a custom DSC resource—the resource module script. This contains the custom resource implementation and determines how it will work. It also manages the resource by keeping it aligned to the expected configuration. Every DSC resource script module must implement three functions. These are mandatory and each function has rules governing their implementation.

Get-TargetResource: This function must declare and accept all properties in the MOF class marked as Key as parameters. It can also accept required and write properties as parameters. The Key and required parameters should be marked as mandatory while declaring the parameter.

The Get-TargetResource function should implement script to retrieve the current state of resource using the Key property values. It should return a hashtable containing all properties defined in the MOF class with values as the current state of resource.

Set-TargetResource:This function must declare and accept all properties in the MOF class marked as Key, required and write properties as parameters. The Key and required parameters should be marked as mandatory while declaring the parameter. The Set-TargetResource function should implement script that should use all resource property values provided as parameters to get the resource instance and perform one of the these actions:

  • Create a new resource instance.
  • Update an existing resource instance.
  • Delete an existing resource instance.

The Set-TargetResource function shouldn’t return any value.

Test-TargetResource:The Test-TargetResource function is similar to the Set-TargetResource function, but it doesn’t return the current resource state. Instead, it returns a true or false Boolean value depending on whether the current resource state matches the expected resource state as specified in the configuration script. If there’s a complete match between the two states, this function returns true. It will return false if there’s a mismatch on any Key, required or write property.

These three functions are invoked by the Local Configuration Manager (LCM) component of DSC. LCM is the agent running on all computers running Windows Management Framework 4.0. Think of LCM as the DSC client available on all servers within a network. DSC can communicate with this client and provide necessary configuration information. It is the responsibility of the LCM (DSC client) to manage, monitor and ensure configuration compliance on the target machines. To do its job, it invokes DSC resource functions. It first invokes the Test-TargetResource function to determine whether the resource configuration is matching the expected configuration. If the value returned is true, it means that current state of resource is the same as that of the expected configuration. However, it should return false if the state doesn’t match.

If LCM gets false as the return value, it invokes the Set-TargetResource, which does the following:

  1. If the resource doesn’t exist and the ensure property is set to Present, it will create a new instance of the resource and assign the property values it gets from the configuration script.
  2. If the resource exists and the ensure property is set to Present, but some of its property values don’t match the property values authored in configuration scripts, this function should update only these resource properties.
  3. If the resource exists and the ensure property is set to Present and all property values match the property values authored in configuration scripts, this function should do nothing.
  4. If the resource exists and the ensure property is set to Absent, then it should be removed.
  5. If the resource doesn’t exists and the ensure property is set to Absent, then it should do nothing.

Now it’s time to implement these three functions related to the TrustedHosts custom resource. Open Windows PowerShell ISE, implement these three functions and save it in the WinRM\DSC­Resources\TrustedHosts Directory with the name TrustedHosts.psm1.

Get-TargetResource for TrustedHosts Custom Resource

This function takes all properties defined in an MOF file as parameters. The credential parameter is of type PSCredential. This is a Microsoft .NET Framework class exposed by Windows PowerShell for capturing and storing username and password. The rest of the parameters are of type string.

Within this function, create a hashtable for returning current resource properties, as shown in Figure 2. It uses the WSMAN provider to read the current TrustedHosts configuration list. The script checks to see if there are any computer names available in the list. If there are, it loops through and matches them to the provided computer names. If it finds an exact match, it updates the Ensure property with value Present or with value Absent. It also adds the same computer name to the returning hashtable.

Figure 2 Get-TargetResource Function for TrustedHosts Resource

Function Get-TargetResource
{
  param(
  # Computer name to be checked within TrustedHosts List
  [parameter(mandatory)]
  [string] $ComputerName,
  # This property determines whether computer name
  # should be added or removed from TrustedHosts
  [parameter(mandatory)]
  [string] $Ensure,
  # Credentials needed to manage WinRM TrustedHosts configuration
  [parameter(mandatory)]
  [PSCredential] $Credential
  )
  # Hashtable containing values is returned from this function
  $retval = @{}
  $retval.Add("Ensure","")
  $retval.Add("ComputerName", "")
  try{
    # Get current TrustedHosts comma-separated list using supplied credentials
    $TH = $(Get-Item WSMan:\localhost\Client\TrustedHosts `
      -credential $Credential).value
    Write-Verbose "Current TrustedHosts Configuration has $TH"
    if($TH.Length -gt 0){
      $temp = $TH -split ","
      [string] $newNode = ""
      # Check if value in TrustedHosts list already have few computer names
      if($temp.Length -gt 0) {
        for($i = 0; $i -lt $temp.Length; $i++){
          if($temp[$i].Trim() -eq $ComputerName.Trim())
          {
            $retval.Ensure = "Present"  # Found computer name
            $retval.ComputerName = $ComputerName
            break;
          } else {
            $retval.Ensure = "Absent" # Computer name is not in the list
            $retval.ComputerName = $ComputerName
          }
        }
      }
    } else {
      # TrustedHosts list is empty
      $retval.Ensure = "Absent"
      $retval.ComputerName = $ComputerName
    }
  } catch {
     Write-Verbose " Error executing Get-TargetResource function"
     Write-Verbose $Error[0].Exception.ToString()
  }
}

Set-TargetResource for TrustedHosts Custom Resource

This function also takes the same set of parameters as Get-Target­Resource. In this function, you get the current computer names using WSMAN provided from TrustedHosts list. Then, depending on the value of the Ensure property, the computer name is added to or removed from the TrustedHosts list.

If the value is Present, the computer name is added to the list. If the value is Absent, it’s removed. Either action is accomplished with the Set-Item cmdlet and WSMAN provider, as shown in Figure 3. There’s no return value from the function. You should use Write-Verbose to provide additional feedback on the console when using the verbose switch.

Figure 3 Set-TargetResource Function for TrustedHosts Resource

Function Set-TargetResource
{
  param(
  # Computer name to be added within TrustedHosts List
  [parameter(mandatory)]
  [string] $ComputerName,
  # This property determines whether computer name should be
  # added or removed from TrustedHosts
  [parameter(mandatory)]
  [string] $Ensure,
  # Credentials needed to manage WinRM TrustedHosts configuration
  [parameter(mandatory)]
  [PSCredential] $Credential
  )
  try {
    # Get current TrustedHosts comma-separated list
    # using supplied credentials
    $TH = (Get-Item WSMan:\localhost\Client\TrustedHosts `
      -credential $Credential).value
    # Computer name should be added to the TrustedHosts list
    if($Ensure -eq "Present") {
      Write-Verbose "The current value is $TH"
      if($TH.Length -gt 0)
        {
          # Adding Computer name when the TrustedHosts list
           # configuration is not empty
          $TH += ",$ComputerName"
          Set-Item WSMan:\localhost\Client\TrustedHosts `
            -Value $TH -credential $Credential -FORCE
        } else {
          # Adding Computer name when the TrustedHosts list
           # configuration is empty
          $TH += "$ComputerName"
          Set-Item WSMan:\localhost\Client\TrustedHosts `
            -Value $TH -credential $Credential -FORCE
        }
          Write-Verbose "The New value is $TH"
      } else {
        # Computer name should be removed from TrustedHosts list
        Write-Verbose "The current value is $TH"
        if($TH.Length -gt 0) {
          $temp = $TH -split ","
          [string] $newNode = ""
          if($temp.Length -gt 0)
            {
               for($i = 0; $i -lt $temp.Length; $i++){
                 if($temp[$i].Trim() -ne $ComputerName.Trim())
                   {
                     $newNode += $temp[$i] + ","
                   }
                 }
                         $newNode = $newNode.TrimEnd(",")
                    # Updating list after removing the Computer name
                    Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newNode
                      -credential $Credential -Force
                    Write-Verbose "The New value is $TH"
               }
             }
     }
   } catch {
      Write-Verbose " Error executing Set-TargetResource function"
      Write-Verbose $Error[0].Exception.ToString()
   }
}

Test-TargetResource for TrustedHosts Custom Resource

This function is similar to Get-TargetResource. The only difference is it returns either Boolean True or False from this function. First, query the current value of TrustedHosts property using WSMAN provider. Because there could be multiple comma-separated computer names, you’ll need to loop through them.

While looping, if you find a computer name match with the name the configuration provides, check whether it should be present or absent. If the Ensure property value is Present and the computer name exists in the TrustedHosts setting, the return value is set to True. If the Ensure property value is Absent and the computer name exists in the TrustedHosts setting, the return value is set to False.

If the Ensure property value is Present and the computer name doesn’t exist in the TrustedHosts setting, the return value is set to False. If the Ensure property value is Absent and the computer name doesn’t exist in TrustedHosts setting, the return value is set to True, as shown in Figure 4.

Figure 4 Test-TargetResource Function for TrustedHosts Resource

Function Test-TargetResource
{
  param(
  # Computer name to be added within TrustedHosts List
  [parameter(mandatory)]
  [string] $ComputerName,
  # This property determines whether computer name should
  # be added or removed from TrustedHosts
  [parameter(mandatory)]
  [string] $Ensure,
  # Credentials needd to manage WinRM TrustedHosts configuration
  [parameter(mandatory)]
  [PSCredential] $Credential
  )
  # Boolean return variable from this function
  $retval = $false
  try {
    # Get current TrustedHosts comma-separated list using supplied credentials
    $TH = (Get-Item WSMan:\localhost\Client\TrustedHosts `
      -credential $Credential).value
    if($TH.Length -gt 0) {
      $temp = $TH -split ","
      [string] $newNode = ""
      if($temp.Length -gt 0) {
        for($i = 0; $i -lt $temp.Length; $i++){
          if($temp[$i].Trim() -eq $ComputerName.Trim()) {
            # Computer name exists within TrustedHosts list
            if($Ensure -eq "Present")
            {
              # Computer name exists and expected to be present
              $retval= $true
              break;
            } else {
              # Computer name exists and is not expected to be present
              $retval = $false
            }
            break;
          } else {
            # Computer name does not exist
            if($Ensure -eq "Present")
            {
              $retval = $false
            } else {
              $retval = $true
            }
          }
        }
      }
    } else {
      # TrustedHosts list is empty
      if($Ensure -eq "Present"){
        $retval= $false
      } else {
        $retval = $true
      }
    }
      return $retval
  } catch {
    Write-Verbose " Error executing Test-TargetResource function"
    Write-Verbose $Error[0].Exception.ToString()
  }
}

Export Custom Resource functions

After implementing the three mandatory DSC resource functions, you should export them. There are two approaches for doing this:

  1. Create a Windows PowerShell module manifest .psd1 file alongside the .psm1 file.
  2. Export the functions from the script file itself using Export-ModuleMember cmdlet.

Use the second approach for exporting the functions. In this approach, export all functions with TargetResource suffix from the module using Export-ModuleMember cmdlet. If you’re interested in using the first approach, follow the same steps used previously for creating the module manifest file for each DSC resource. Primarily, execute the New-ModuleManifest command and store the generated .psd1 within the TrustedHosts folder alongside .psm1 and .Schema.mof file:

# Exports the three functiona as part of the module
Export-ModuleMember -Function *-TargetResource

Use Custom Resource TrustedHosts in Configuration

Next, you’ll create a configuration script to use the new TrustedHosts custom resource. Apply this configuration to the localhost node and provide values to the three properties of the TrustedHosts resource. The configuration script takes a mandatory Credential parameter of type [PSCredential]. This parameter is assigned to the resource’s credential property, as shown in Figure 5.

Figure 5 Get-TargetResource Function for TrustedHosts Resource

Configuration TestWinRMTrustedHosts
{
  param([parameter(mandatory)] [pscredential] $Credential)
  import-dscresource -modulename WinRM
  node localhost
    {
      TrustedHosts TrustedHostEntry
        {
          ComputerName = "Client01.Contoso.com"
          Ensure ="Present"
          Credential = $Credential
        }
    }
}

By default, DSC doesn’t let you use credentials in plain text. Do this through the configuration data hashtable. Within this hashtable, add the PSDSCAllowPlainTextPassword property with true as its value. Using PSDSCAllowPlainTextPassword is a security risk because it lets you store passwords as plain text in MOF files. You should use Security Certificates as a best practice to ensure passwords aren’t stored as plain text in MOF files and securely transmitted using this configuration data:

$ConfigData = @{
AllNodes = @(
  @{
    NodeName = "localhost"
    PSDscAllowPlainTextPassword = $true   
  }
  )
}

Next, create a configuration object by executing the configuration script and passing in the location of the MOF file, the Configuration­Data variable and credentials with the Get-Credential command:

TestWinRMTrustedHosts -OutputPath "C:\CR" -ConfigurationData $ConfigData `
  -credential (get-credential)

Executing this command generates an MOF file named localhost.mof at location C:\CR. When this executes, it will ask for username and password. After generating the MOF file, it’s time to apply the configuration in PUSH mode. Do this by executing the Start-DscConfiguration command:

Start-DscConfiguration -Wait -Force -Path "C:\CR" -Verbose

After applying the configuration, check the value of the localhosts WinRM TrustedHosts setting. The ComputerName value DC01 should now be part of the TrustedHosts value. You can add or remove computer names to the TrustedHosts value using this custom resource.

Import-DSCResource

You’ll use Import-DSCResource to import custom resources into the configuration script in order to perform design time validation of the custom resource. This is another way to ensure the custom resource has been well authored. Custom resources are available at C:\ProgramFiles\WindowsPowerShell\Modules. To import and load these modules, use the Import-DSCResource function. Import-DSCResource is a dynamic function and only available in configuration scripts. This function can’t be outside of the configuration block, and it takes two non-positional parameters:

  1. ModuleName—The name of the module that should be imported.
  2. Name—The name of the resource that should be imported.

If only ModuleName argument is provided and Name is omitted, all resources available within the module would be imported. You can use this approach to load the TrustedHosts resource in the sample configuration:

Import-DSCResource –ModuleName WinRM

If only Name argument is provided and ModuleName is omitted, DSC will search all module locations available from $env:PSModulePath to find the resource. Once found, it imports the resource:

Import-DSCResource –Name TrustedHosts

If both ModuleName and Name arguments are provided, the resource is loaded and imported from the provided module name. This is by far the fastest mechanism to find and load resources because DSC doesn’t have to perform an extensive search:

Import-DSCResource –ModuleName WinRM –ResourceName TrustedHosts

You can load multiple resources at the same time using comma-­separated resource names as well.

Wrapping Up

DSC gives you the necessary extensions to create new resources and use them in configuration. Windows PowerShell simplifies authoring DSC custom resources by implementing a few mandatory functions. These functions should follow few rules in their implementation.

If the resources provided out-of-the-box or from the community don’t meet your needs, you can easily create your own. This article showed you how to create a simple custom resource with complete implementation. It also showed how you should package these resources within a Windows PowerShell module, with rules governing its file and folder structure.


Ritesh Modi is an architect with Microsoft Services. He has more than a decade of experience building and deploying enterprise solutions. He’s an expert on Windows PowerShell, Desired State Configuration and System Center. He has spoken at TechEd, does internal training and blogs at automationnext.wordpress.com. Reach him at rimodi@microsoft.com.

Thanks to the following technical expert for reviewing this article: Abhik Chatterjee