Published: Feb 19, 2014
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:
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.
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!)
Looking to each function in turn, lets start with the Get-TargetResource command.
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 } } }
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 } } }
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 } } }
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 '@ }
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.