Want to write a DSC resource where only a single instance can be configured?

I’ve heard from a few DSC resource authors that they need a method to implement a resource that has a single instance; a singleton. The problem they encountered is a DSC resource must define a Key property but singleton resources generally should not have a key because there is only one instance that can be updated.

The problem can been seen by examining the xTimeZone and xRemoteDesktopAdmin resources. xTimeZone configures the system’s current time zone and uses the time zone as the key. The problem is this would let you specify the time zone multiple times during your configuration even though there is only one time zone setting that can be configured. If multiple time zone entries are defined, it would cause DSC to set the time zone back and forth between these zones.

Remote desktop has a slightly different issue; it is a single setting that either enables or disables it. xRemoteDesktopAdmin controls this through Ensure and Absent value which is also a key. If the resource is mistakenly defined more than once, the result could cause remote desktop to be toggled on and off.

To solve this problem, I’ve defined a best practice that ensures this type of resource can only be defined once. In this post, I’ll discuss the best practice and how to implement it using DSC Script based and DSC Class based resources.

Prerequisites

I assume that you have basic understanding of how to develop DSC Resources via either:

Overview

DSC requires that all resources have at least one key property. The key property should ensure that the resource uniquely identifies the resource instance. If a resource is only a single instance often times, there is no explicit key property. For example the key property for a file resource, is the path to the file. Now, let’s take Time Zone, what is the key. In DSC, we are always configuring a node, so the machine is already specified. We need to create a key property that lets the Time Zone be specified once and only once.

Best Practice

Until DSC allows a proper single instance resource, we need a best practice that allows a consistent and reliable single instance resource experience. The best practice proposed is to define a key property named IsSingleInstance and restrict it to a single value Yes using a Value map. The key property is named to tell the user of the resource why it is there and be an intuitive as possible.

Script Resource – Schema

When I started looking into this problem the schema for the xTimeZone resource was specified like this:

[ClassVersion("1.0.0.0"), FriendlyName("xTimeZone")]
class xTimeZone : OMI_BaseResource
{
    [Key, Description("Specifies the TimeZone.")] String TimeZone;
};

With this schema, especially in a large configuration, it is possible for this resource to be specified multiple times. This could lead to confusion about why the Time Zone is not getting set to the desired state. The solution is to add an artificial key. The pattern we are proposing is that the key name is IsSingleInstance with one allowed value for Yes. This changes the above schema to:

[ClassVersion("1.0.0.0"), FriendlyName("xTimeZone")]
class xTimeZone : OMI_BaseResource
{
    [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'"), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance;
    [Required, Description("Specifies the TimeZone.")] String TimeZone;
};

Now we must update the Get, Set and Test TargetResource function to take the parameter.

Script Resource – Code

Because the parameter validation is handled by the schema, the only thing we have to do is add the $IsSingleInstance parameter, with the attributes required of a key property, [parameter(Mandatory = $true)] and [ValidateNotNullOrEmpty()], and ignore it. (You can see the original code on GitHub.) Get-TargetResource becomes:

    function Get-TargetResource
    {
        [CmdletBinding()]
        [OutputType([Hashtable])]
        param
        (
            [parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [String]
            $IsSingleInstance, 

            [parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [String]
            $TimeZone
        )

       #Existing Get implementation
    }

Set-TargetResource becomes:

    function Set-TargetResource
    {
        [CmdletBinding(SupportsShouldProcess=$true)]
        param
        (
            [parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [String]
            $IsSingleInstance, 

            [parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [String]
            $TimeZone
        )

        # Existing Set Implementation
    }

Test-TargetResource becomes:

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([Boolean])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $IsSingleInstance, 

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $TimeZone
    )

    # Existing Test Implementation
}

Now we have a single instance script resource, one which can be in a configuration once and only once. Let’s look at how to use it.

configuration MyTime
{
    Import-DscResource -ModuleName xTimeZone

       xTimeZone PacificTime
        {
            TimeZone         = 'Pacific Standard Time'
            IsSingleInstance = 'Yes'
        }
}

Classes are a newer way of writing DSC resources. It simplifies the process by eliminating the schema. Let’s look at how to do this with a Class based Resource.

Class Resource

Class based Resources define properties which play the same role as the schema in a script based resources. I’ll stick with the TimeZone example. The skeleton of the class based resource before changing it to a single instance resource would look like this:

[DscResource()]
class xTimeZone
{
   [DscProperty(Key)] 
   [ValidateNotNullorEmpty()]
   [String] $TimeZone = $null

   #Set function similar to Set-TargetResource
   [void] Set()
   {
      #Set code goes here
   }

   #Test function similar to Test-TargetResource
   [bool] Test()
   {
        #Test code goes here, returns $true or $false
   }

   #Get function similar to Get-TargetResource
   [xTimeZone] Get()
   {
      # Get code goes here, and updates properties on $this
      return $this
   }
}

In order to make it a single instance resource, we follow the same pattern. We add the $IsSingleInstance property and make the only valid value Yes. The code to do this looks like this:

[DscResource()]
class xTimeZone
{
   [ValidateSet('Yes')]
   [DscProperty(Key)] 
   [ValidateNotNullorEmpty()]
   [String] $IsSingleInstance

   [DscProperty(Mandatory)] 
   [ValidateNotNullorEmpty()]
   [String] $TimeZone = $null

   #Set function similar to Set-TargetResource
   [void] Set()
   {
      #Set code goes here
   }

   #Test function similar to Test-TargetResource
   [bool] Test()
   {
        #Test code goes here, returns $true or $false
   }

   #Get function similar to Get-TargetResource
   [xTimeZone] Get()
   {
      # Get code goes here, and updates properties on $this
      return $this
   }
}

See GitHub for the full code.

Summary

You should be able to update existing or build new resource that should be single instance with this pattern. Add the $IsSingleInstance property and make the only valid value Yes. The DSC team, including myself, are looking into ways to make it simpler in the future. But for now, this pattern should be followed to ensure that DSC can detect two resource instances trying to configure the same resource in the same configuration.

Travis Plunk Azure DSC Extension