DscResource.Base.psm1
#Region '.\prefix.ps1' -1 $script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/DscResource.Common' Import-Module -Name $script:dscResourceCommonModulePath $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' #EndRegion '.\prefix.ps1' 5 #Region '.\Enum\1.Ensure.ps1' -1 <# .SYNOPSIS The possible states for the DSC resource parameter Ensure. #> enum Ensure { Present Absent } #EndRegion '.\Enum\1.Ensure.ps1' 11 #Region '.\Classes\002.Reason.ps1' -1 class Reason { [DscProperty()] [System.String] $Code [DscProperty()] [System.String] $Phrase } #EndRegion '.\Classes\002.Reason.ps1' 11 #Region '.\Classes\010.ResourceBase.ps1' -1 <# .SYNOPSIS A class with methods that are equal for all class-based resources. .DESCRIPTION A class with methods that are equal for all class-based resources. .NOTES This class should be able to be inherited by all DSC resources. This class shall not contain any DSC properties, neither shall it contain anything specific to only a single resource. #> class ResourceBase { # Property for holding localization strings hidden [System.Collections.Hashtable] $localizedData = @{} # Property for derived class to set properties that should not be enforced. hidden [System.String[]] $ExcludeDscProperties = @() # Default constructor ResourceBase() { $this.ImportLocalization($null) } ResourceBase([System.String] $BasePath) { $this.ImportLocalization($BasePath) } hidden [void] ImportLocalization([System.String] $BasePath) { $getLocalizedDataRecursiveParameters = @{ ClassName = ($this | Get-ClassName -Recurse) } if (-not [System.String]::IsNullOrEmpty($BasePath)) { <# Passing the base directory of the module that contain the derived class. #> $getLocalizedDataRecursiveParameters.BaseDirectory = $BasePath } <# TODO: When this fails, for example when the localized string file is missing the LCM returns the error 'Failed to create an object of PowerShell class SqlDatabasePermission' instead of the actual error that occurred. #> $this.localizedData = Get-LocalizedDataRecursive @getLocalizedDataRecursiveParameters } [ResourceBase] Get() { $this.Assert() # Get all key properties. $keyProperty = $this | Get-DscProperty -Attribute 'Key' Write-Verbose -Message ($this.localizedData.GetCurrentState -f $this.GetType().Name, ($keyProperty | ConvertTo-Json -Compress)) $getCurrentStateResult = $this.GetCurrentState($keyProperty) $dscResourceObject = [System.Activator]::CreateInstance($this.GetType()) # Set values returned from the derived class' GetCurrentState(). foreach ($propertyName in $this.PSObject.Properties.Name) { if ($propertyName -in @($getCurrentStateResult.Keys)) { $dscResourceObject.$propertyName = $getCurrentStateResult.$propertyName } } $keyPropertyAddedToCurrentState = $false # Set key property values unless it was returned from the derived class' GetCurrentState(). foreach ($propertyName in $keyProperty.Keys) { if ($propertyName -notin @($getCurrentStateResult.Keys)) { # Add the key value to the instance to be returned. $dscResourceObject.$propertyName = $this.$propertyName $keyPropertyAddedToCurrentState = $true } } if (($this | Test-DscProperty -Name 'Ensure') -and -not $getCurrentStateResult.ContainsKey('Ensure')) { # Evaluate if we should set Ensure property. if ($keyPropertyAddedToCurrentState) { <# A key property was added to the current state, assume its because the object did not exist in the current state. Set Ensure to Absent. #> $dscResourceObject.Ensure = [Ensure]::Absent $getCurrentStateResult.Ensure = [Ensure]::Absent } else { $dscResourceObject.Ensure = [Ensure]::Present $getCurrentStateResult.Ensure = [Ensure]::Present } } <# Returns all enforced properties not in desires state, or $null if all enforced properties are in desired state. #> $propertiesNotInDesiredState = $this.Compare($getCurrentStateResult, @()) <# Return the correct values for Reasons property if the derived DSC resource has such property and it hasn't been already set by GetCurrentState(). #> if (($this | Test-DscProperty -Name 'Reasons') -and -not $getCurrentStateResult.ContainsKey('Reasons')) { # Always return an empty array if all properties are in desired state. $dscResourceObject.Reasons = $propertiesNotInDesiredState | Resolve-Reason -ResourceName $this.GetType().Name | ConvertFrom-Reason } # Return properties. return $dscResourceObject } [void] Set() { # Get all key properties. $keyProperty = $this | Get-DscProperty -Attribute 'Key' Write-Verbose -Message ($this.localizedData.SetDesiredState -f $this.GetType().Name, ($keyProperty | ConvertTo-Json -Compress)) $this.Assert() <# Returns all enforced properties not in desires state, or $null if all enforced properties are in desired state. #> $propertiesNotInDesiredState = $this.Compare() if ($propertiesNotInDesiredState) { $propertiesToModify = $propertiesNotInDesiredState | ConvertFrom-CompareResult $propertiesToModify.Keys | ForEach-Object -Process { Write-Verbose -Message ($this.localizedData.SetProperty -f $_, $propertiesToModify.$_) } <# Call the Modify() method with the properties that should be enforced and was not in desired state. #> $this.Modify($propertiesToModify) } else { Write-Verbose -Message $this.localizedData.NoPropertiesToSet } } [System.Boolean] Test() { # Get all key properties. $keyProperty = $this | Get-DscProperty -Attribute 'Key' Write-Verbose -Message ($this.localizedData.TestDesiredState -f $this.GetType().Name, ($keyProperty | ConvertTo-Json -Compress)) $this.Assert() $isInDesiredState = $true <# Returns all enforced properties not in desires state, or $null if all enforced properties are in desired state. #> $propertiesNotInDesiredState = $this.Compare() if ($propertiesNotInDesiredState) { $isInDesiredState = $false } if ($isInDesiredState) { Write-Verbose -Message $this.localizedData.InDesiredState } else { Write-Verbose -Message $this.localizedData.NotInDesiredState } return $isInDesiredState } <# Returns a hashtable containing all properties that should be enforced and are not in desired state, or $null if all enforced properties are in desired state. This method should normally not be overridden. #> hidden [System.Collections.Hashtable[]] Compare() { # Get the current state, all properties except Read properties . $currentState = $this.Get() | Get-DscProperty -Attribute @('Key', 'Mandatory', 'Optional') return $this.Compare($currentState, @()) } <# Returns a hashtable containing all properties that should be enforced and are not in desired state, or $null if all enforced properties are in desired state. This method should normally not be overridden. #> hidden [System.Collections.Hashtable[]] Compare([System.Collections.Hashtable] $currentState, [System.String[]] $excludeProperties) { # Get the desired state, all assigned properties that has an non-null value. $desiredState = $this | Get-DscProperty -Attribute @('Key', 'Mandatory', 'Optional') -HasValue $CompareDscParameterState = @{ CurrentValues = $currentState DesiredValues = $desiredState Properties = $desiredState.Keys ExcludeProperties = ($excludeProperties + $this.ExcludeDscProperties) | Select-Object -Unique IncludeValue = $true # This is needed to sort complex types. SortArrayValues = $true } <# Returns all enforced properties not in desires state, or $null if all enforced properties are in desired state. #> return (Compare-DscParameterState @CompareDscParameterState) } # This method should normally not be overridden. hidden [void] Assert() { # Get the properties that has a non-null value and is not of type Read. $desiredState = $this | Get-DscProperty -Attribute @('Key', 'Mandatory', 'Optional') -HasValue $this.AssertProperties($desiredState) } <# This method can be overridden if resource specific property asserts are needed. The parameter properties will contain the properties that was assigned a value. #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidEmptyNamedBlocks', '')] hidden [void] AssertProperties([System.Collections.Hashtable] $properties) { } <# This method must be overridden by a resource. The parameter properties will contain the properties that should be enforced and that are not in desired state. #> hidden [void] Modify([System.Collections.Hashtable] $properties) { throw $this.localizedData.ModifyMethodNotImplemented } <# This method must be overridden by a resource. The parameter properties will contain the key properties. #> hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) { throw $this.localizedData.GetCurrentStateMethodNotImplemented } } #EndRegion '.\Classes\010.ResourceBase.ps1' 285 #Region '.\Private\ConvertFrom-CompareResult.ps1' -1 <# .SYNOPSIS Returns a hashtable with property name and their expected value. .DESCRIPTION Returns a hashtable with property name and their expected value. .PARAMETER CompareResult The result from Compare-DscParameterState. .EXAMPLE ConvertFrom-CompareResult -CompareResult (Compare-DscParameterState) Returns a hashtable that contain all the properties not in desired state and their expected value. .OUTPUTS [System.Collections.Hashtable] #> function ConvertFrom-CompareResult { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [System.Collections.Hashtable[]] $CompareResult ) begin { $returnHashtable = @{} } process { $CompareResult | ForEach-Object -Process { $returnHashtable[$_.Property] = $_.ExpectedValue } } end { return $returnHashtable } } #EndRegion '.\Private\ConvertFrom-CompareResult.ps1' 48 #Region '.\Private\ConvertFrom-Reason.ps1' -1 <# .SYNOPSIS Returns a array of the type `System.Collections.Hashtable`. .DESCRIPTION This command converts an array of [Reason] that is returned by the command `Resolve-Reason`. The result is an array of the type `[System.Collections.Hashtable]` that can be returned as the value of a DSC resource's property **Reasons**. .PARAMETER Reason Specifies an array of `[Reason]`. Normally the result from the command `Resolve-Reason`. .EXAMPLE Resolve-Reason -Reason (Resolve-Reason) -ResourceName 'MyResource' Returns an array of `[System.Collections.Hashtable]` with the converted `[Reason[]]`. .OUTPUTS [System.Collections.Hashtable[]] #> function ConvertFrom-Reason { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Because the rule does not understands that the command returns [System.Collections.Hashtable[]] when using , (comma) in the return statement')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when the output type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] [CmdletBinding()] [OutputType([System.Collections.Hashtable[]])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AllowEmptyCollection()] [AllowNull()] [Reason[]] $Reason ) begin { # Always return an empty array if there are nothing to convert. $reasonsAsHashtable = [System.Collections.Hashtable[]] @() } process { foreach ($currentReason in $Reason) { $reasonsAsHashtable += [System.Collections.Hashtable] @{ Code = $currentReason.Code Phrase = $currentReason.Phrase } } } end { return , [System.Collections.Hashtable[]] $reasonsAsHashtable } } #EndRegion '.\Private\ConvertFrom-Reason.ps1' 59 #Region '.\Private\Get-ClassName.ps1' -1 <# .SYNOPSIS Get the class name of the passed object, and optional an array with all inherited classes. .DESCRIPTION Get the class name of the passed object, and optional an array with all inherited classes .PARAMETER InputObject The object to be evaluated. .PARAMETER Recurse Specifies if the class name of inherited classes shall be returned. The recursive stops when the first object of the type `[System.Object]` is found. .EXAMPLE Get-ClassName -InputObject $this -Recurse Get the class name of the current instance and all the inherited (parent) classes. .OUTPUTS [System.String[]] #> function Get-ClassName { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Because the rule does not understands that the command returns [System.String[]] when using , (comma) in the return statement')] [CmdletBinding()] [OutputType([System.String[]])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSObject] $InputObject, [Parameter()] [System.Management.Automation.SwitchParameter] $Recurse ) begin { # Create a list of the inherited class names $class = @() } process { $class += $InputObject.GetType().FullName if ($Recurse.IsPresent) { $parentClass = $InputObject.GetType().BaseType while ($parentClass -ne [System.Object]) { $class += $parentClass.FullName $parentClass = $parentClass.BaseType } } } end { return , [System.String[]] $class } } #EndRegion '.\Private\Get-ClassName.ps1' 71 #Region '.\Private\Get-LocalizedDataRecursive.ps1' -1 <# .SYNOPSIS Get the localization strings data from one or more localization string files. .DESCRIPTION Get the localization strings data from one or more localization string files. This can be used in classes to be able to inherit localization strings from one or more parent (base) classes. The order of class names passed to parameter `ClassName` determines the order of importing localization string files. First entry's localization string file will be imported first, then next entry's localization string file, and so on. If the second (or any consecutive) entry's localization string file contain a localization string key that existed in a previous imported localization string file that localization string key will be ignored. Making it possible for a child class to override localization strings from one or more parent (base) classes. .PARAMETER ClassName An array of class names, normally provided by `Get-ClassName -Recurse`. .PARAMETER BaseDirectory Specifies a base module path where it also searches for localization string files. .EXAMPLE Get-LocalizedDataRecursive -ClassName $InputObject.GetType().FullName Returns a hashtable containing all the localized strings for the current instance. .EXAMPLE Get-LocalizedDataRecursive -ClassName (Get-ClassName -InputObject $this -Recurse) Returns a hashtable containing all the localized strings for the current instance and any inherited (parent) classes. .OUTPUTS [System.Collections.Hashtable] #> function Get-LocalizedDataRecursive { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [System.String[]] $ClassName, [Parameter()] [System.String] $BaseDirectory ) begin { $localizedData = @{} } process { foreach ($name in $ClassName) { if ($name -match '\.psd1$') { # Assume we got full file name. $localizationFileName = $name -replace '\.psd1$' } else { # Assume we only got class name. $localizationFileName = '{0}.strings' -f $name } Write-Debug -Message ($script:localizedData.DebugImportingLocalizationData -f $localizationFileName) if ($name -eq 'ResourceBase') { # The class ResourceBase will always be in the same module as this command. $path = $PSScriptRoot } elseif ($null -ne $BaseDirectory) { # Assuming derived class that is not part of this module. $path = $BaseDirectory } else { # Assuming derived class that is not part of this module. $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( $script:localizedData.ThrowClassIsNotPartOfModule, 'DRB0002', [System.Management.Automation.ErrorCategory]::InvalidOperation, $name ) ) } # Get localized data for the class $classLocalizationStrings = Get-LocalizedData -DefaultUICulture 'en-US' -BaseDirectory $path -FileName $localizationFileName -ErrorAction 'Stop' # Append only previously unspecified keys in the localization data foreach ($key in $classLocalizationStrings.Keys) { if (-not $localizedData.ContainsKey($key)) { $localizedData[$key] = $classLocalizationStrings[$key] } } } } end { Write-Debug -Message ($script:localizedData.DebugShowAllLocalizationData -f ($localizedData | ConvertTo-JSON)) return $localizedData } } #EndRegion '.\Private\Get-LocalizedDataRecursive.ps1' 122 #Region '.\Private\Resolve-Reason.ps1' -1 <# .SYNOPSIS Returns a array of the type `[Reason]`. .DESCRIPTION This command builds an array from the properties that is returned by the command `Compare-DscParameterState`. The result is an array of the type `[Reason]`. .PARAMETER Property The result from the command Compare-DscParameterState. .PARAMETER ResourceName The name of the resource. Will be used to populate the property Code with the correct value. .EXAMPLE Resolve-Reason -Property (Compare-DscParameterState) -ResourceName 'MyResource' Returns an array of `[Reason]` that contain all the properties not in desired state and why a specific property is not in desired state. .OUTPUTS [Reason[]] #> function Resolve-Reason { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when the output type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] [CmdletBinding()] [OutputType([Reason[]])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AllowEmptyCollection()] [AllowNull()] [System.Collections.Hashtable[]] $Property, [Parameter(Mandatory = $true)] [System.String] $ResourceName ) begin { # Always return an empty array if there are no properties to add. $reasons = [Reason[]] @() } process { foreach ($currentProperty in $Property) { if ($currentProperty.ExpectedValue -is [System.Enum]) { # Return the string representation of the value (instead of the numeric value). $propertyExpectedValue = $currentProperty.ExpectedValue.ToString() } else { $propertyExpectedValue = $currentProperty.ExpectedValue } if ($property.ActualValue -is [System.Enum]) { # Return the string representation of the value so that conversion to json is correct. $propertyActualValue = $currentProperty.ActualValue.ToString() } else { $propertyActualValue = $currentProperty.ActualValue } <# In PowerShell 7 the command ConvertTo-Json returns 'null' on null value, but not in Windows PowerShell. Switch to output empty string if value is null. #> if ($PSVersionTable.PSEdition -eq 'Desktop') { if ($null -eq $propertyExpectedValue) { $propertyExpectedValue = '' } if ($null -eq $propertyActualValue) { $propertyActualValue = '' } } # Convert the value to Json to be able to easily visualize complex types $propertyActualValueJson = $propertyActualValue | ConvertTo-Json -Compress $propertyExpectedValueJson = $propertyExpectedValue | ConvertTo-Json -Compress # If the property name contain the word Path, remove '\\' from path. if ($currentProperty.Property -match 'Path') { $propertyActualValueJson = $propertyActualValueJson -replace '\\\\', '\' $propertyExpectedValueJson = $propertyExpectedValueJson -replace '\\\\', '\' } $reasons += [Reason] @{ Code = '{0}:{0}:{1}' -f $ResourceName, $currentProperty.Property # Convert the object to JSON to handle complex types. Phrase = 'The property {0} should be {1}, but was {2}' -f @( $currentProperty.Property, $propertyExpectedValueJson, $propertyActualValueJson ) } } } end { return $reasons } } #EndRegion '.\Private\Resolve-Reason.ps1' 119 |