Where Do I Add the Code for My Desired State Configuration (DSC) Module?

Welcome back to our in-depth series on Desired State Configuration (DSC)! In the previous post we created the templates using the free module from Microsoft called xDSCResourceDesigner for our new hotfix resource. Now we will take the template that was generated and add some sample code to bring the module to life.

Editor’s note: Need to catch up? Check out our previous articles in this series:

Developing the DSC Module Code

Now we can navigate to the new module files we just created and take a closer look at the file Hotfix.psm1, which is the heart of the template we just created. Inside this file, we will see that the wizard has created three primary functions that we now need to extend with the actual working logic of our module.

  • Get-TargetResource
  • Set-TargetResource
  • Test-TargetResource

Over the next few sections, we will proceed to define the code that is appropriate for each of these functions. As the focus of the post is to walk through the procedures of creating a DSC resource, leveraging GIT to keep our code managed, and sharing the results with the community, I am not going to describe each line of the code in detail. (Plus, I am sure that someone out there is far smarter than I who will break down crying when he or she sees my coding skills!)

DSC Modules and the 3 Functions

Looking to each function in turn, lets start with the Get-TargetResource command.

Get-TargetResource

As we take our initial look at the function, what is presented is the outline and the parameters appropriate for this function, along with some comments in the main body to provide us some hints and guidance.

​ function Get-TargetResource
{
   [CmdletBinding()]
   [OutputType([System.Collections.Hashtable])]
   param
   (
      [parameter(Mandatory = $true)]
      [System.String]
      $HotfixID
   )

   #Write-Verbose "Use this cmdlet to deliver information about command processing."
   #Write-Debug "Use this cmdlet to write debug information while troubleshooting."

   <#
   $returnValue = @{
      Name = [System.String]
      SourcePath = [System.String]
      Ensure = [System.String]
   }

}

The purpose of this function is to run a simple check on the Key resource properties and return their current settings on the node in the format of a hash table. This detail is then used by the Local Configuration Manager to determine whether it actually needs to run the Set-Target Resource function to apply the desired state.

​ function Get-TargetResource
{
   [CmdletBinding()]
   [OutputType([System.Collections.Hashtable])]
   param
   (
      [parameter(Mandatory = $true)]
      [System.String]
      $Name
   )

   $HotfixInfo = Get-HotFix -id $Name -ErrorAction SilentlyContinue

   if ($HotfixInfo -ne $null)
   {
      return @{
         Ensure   = "Present";
         HotfixID = $HotfixInfo.HotfixID
      }
   }
   else
   {
      return @{
         Ensure   = "Absent";
         HotfixID = $Name
      }
   }
}

Set-TargetResource

One could consider this function as the main work engine, with the the responsibility of setting up the node to the desired state. The state, of course, can be to apply or remove a specific setting or configuration (as in this example, to apply a missing hotfix or to remove a hotfix that might be already applied). The actions of this function may also call for the node to be rebooted, so the function is responsible to indicate back to the Local Configuration Manager that this action is pending, but the function should not apply such a reboot if called for.

The code below is quite verbose. I am aware of cleaner methods to implement this function, but for purpose of the example this should prove easier to read. The most important point in this code is that we need to check for all eventualities and address them, for if we miss a scenario, we could cause an error for the local configuration manager, which would then fail to execute any proceeding DSC configuration steps.

​ function Set-TargetResource
{
   [CmdletBinding()]
   param
   (
      [parameter(Mandatory = $true)]
      [System.String]
      $Name,

      [System.String]
      $SourcePath,

      [ValidateSet("Present","Absent")]
      [System.String]
      $Ensure
   )

   $HotfixInfo = Get-HotFix -id $Name -ErrorAction SilentlyContinue
   if ($HotfixInfo -ne $null)
   {
      Write-Verbose ($LocalizedData.HotfixInstalled -f $HotfixInfo.Description, $Name, $HotfixInfo.InstalledOn)
   } else {
      Write-Verbose ($LocalizedData.HotfixMissing -f $Name)
      return $false
   }

   if ($Ensure -eq 'Present')
   {
      Write-Verbose "Ensure -eq 'Present'"
      if ($HotfixInfo -eq $null)
      {
         Write-Verbose ($LocalizedData.AddingMissingHotfix -f $Name)

         if (Test-Path $SourcePath -ErrorAction SilentlyContinue)
         {
            Write-Verbose -Message "Applying Hotfix $Name"
            $Process = Start-Process $SourcePath -ArgumentList "/quiet /norestart" -Wait -PassThru
            Switch ($Process.ExitCode)
            {
            0       {
               $a = $LocalizedData.Error0000 }

            1       {
               $a = $LocalizedData.Error0001
               return $false }

            2       {
               $a = $LocalizedData.Error0002
               return $false }

            1001    {
               $a = $LocalizedData.Error1001
               $global:DSCMachineStatus = 1 }

            3010    {
               $a = $LocalizedData.Error3010
               $global:DSCMachineStatus = 1 }

            Default {
               $a = $LocalizedData.ErrorMsg
               return $false }
            }
            Write-verbose($LocalizedData.InstallationError -f $LastExitCode, $a)
         }
         Else
         {
            Write-Verbose -Message "Unable to locate hotfix $Name on source location $SourcePath"
            return $false
         }
      }
      else
      {
         Write-Verbose ($LocalizedData.HotfixIDMissing)
         return $false
      }
   }

   elseif($Ensure -eq 'Absent')
   {
      Write-Verbose "Ensure -eq 'Absent'"
      if ($HotfixInfo -ne $null)
      {
         Write-Verbose ($LocalizedData.RemovingHostfix -f $Name)
         $UpdateID = $Name.Substring(2,$Name.Length -2)
         $Process = Start-Process -Wait wusa -ArgumentList "/uninstall /kb:$UpdateID /quiet /norestart" -PassThru
         Switch ($Process.ExitCode)
         {
            0       {
               $a = $LocalizedData.Error0000 }

            1       {
               $a = $LocalizedData.Error0001
               return $false }

            2       {
               $a = $LocalizedData.Error0002
               return $false }

            1001    {
               $a = $LocalizedData.Error1001
               $global:DSCMachineStatus = 1 }

            3010    {
               $a = $LocalizedData.Error3010
               $global:DSCMachineStatus = 1 }

            Default {
               $a = $LocalizedData.ErrorMsg
               return $false }
         }

         Write-verbose($LocalizedData.InstallationError -f $LastExitCode, $a)
      }
      else
      {
         Write-Verbose ($LocalizedData.HotfixMissing -f $Name)
         return $false
      }
   }
}

Test-TargetResource

Now, the final function we need to define is the Test-TargetResouce, which is to simply check the status of the resource instance that is specified in the key parameters. If the actual status of the resource instance does not match the values specified in the parameter set, return False. Otherwise, we will return True.

​ function Test-TargetResource
{
   [CmdletBinding()]
   [OutputType([System.Boolean])]
   param
   (
      [parameter(Mandatory = $true)]
      [System.String]
      $Name,

      [System.String]
      $SourcePath,

      [ValidateSet("Present","Absent")]
      [System.String]
      $Ensure
   )

   $HotfixInfo = Get-HotFix -id $Name -ErrorAction SilentlyContinue

   if ($Ensure -eq 'Present')
   {
      if ($HotfixInfo -eq $null)
      {
         Write-Verbose ($LocalizedData.HotfixMissing -f $Name)
         return $false
      }
      else
      {
         Write-Verbose ($LocalizedData.HotfixInstalled -f $HotfixInfo.Description, $Name, $HotfixInfo.InstalledOn)
         return $true
      }
   }
   elseif($Ensure -eq 'Absent')
   {
      if ($HotfixInfo -ne $null)
      {
         Write-Verbose ($LocalizedData.HotfixInstalled -f $HotfixInfo.Description, $Name, $HotfixInfo.InstalledOn)
         return $false
      }
      else
      {
         Write-Verbose ($LocalizedData.HotfixMissing -f $Name)
         return $true
      }
   }
}

Comments and Localization

As we are going to share our code, it is good practice to include comments and some details related to each revision of the code. Over time others may offer to help, and if you provide some details in the file, it makes things much easier for everyone to understand what the code is doing and what changes or fixes you might be applying. I like placing a header at the top of the file to get it up and running

​ #
# Author  : Damian Flynn (www.DamianFlynn.com \ petri.com/author/damian-flynn)
# Date    : 15 Jan 2014
# Name    : Windows Hotfix DSC Module
# Build   : 1.0 Petri.co.il example release
# Purpose : DSC Module to manage a Hotfix status on Servers
#         : Primary use for this module, is to ensure servers are configured
#         : using hotfixes which may not be auto-deployed using tools like WSUS
#         : common use would be with Hyper-V and Clustering Server roles
#

#
# Revision: 1.0  16/01/2014  Initial version from Petri.co.il Blog Example
#

As you read through the code above, you will notice that I am not actually defining the string that is reported back as part of the messages we are logging. Instead, I am referencing a hash table called $LocalizedData and selecting the name of a specific entry in that table to represent the message Iwish to convey. This practice enables us to support localization of our modules with great ease, requiring no change in the code; instead just updating the actual strings with the relevant language sentences that we wish to report back.

To achieve this, at the top of the file I am defining my LocalizedData for en-US as follows. Note that I am also leveraging the string replacement functions to allow me to place specific results from the functions where I desire in the output message.

​ # Fallback message strings in en-US
DATA localizedData
{
# same as culture = "en-US"
ConvertFrom-StringData @'
HotfixInstalled=The {0} Hotfix {1} is installed {2}.
HotfixMissing=The Hotfix {0} is not installed.
AddingMissingHotfix=The Hotfix {0} is missing so adding it.
RemovingHostfix=The Hotfix {0} is been removed.
InstallationError=Error {0}: {1}.
Error0000=Action completed without error
Error0001=Another instance of this application is already running
Error0002=Invalid command line parameter
Error1001=A pending restart blocks installation
Error3010=A restart is needed
ErrorMsg=An unknown error occurred installing prerequisites
HotfixIDMissing=No HotfixID was provided
'@
}

Commit Our Changes

With the first version of our module now in place, we will update our GIT repository with this new version.

​ git add . –A
git commit –m "Implemented the code to enable our new module to manage Hotfixes, as shared on http://petri.com"

Now, we can go back to basics, and see if we can discover our new module.