IISAdministrationDsc.psm1
|
$script:ModuleRoot = $PSScriptRoot enum Ensure { Absent Present } class Reason { [DscProperty()] [string] $Code [DscProperty()] [string] $Phrase } [DscResource()] class IISConfigAttribute { #region DSC Properties <# The properties you can define in configuration settings. The [DscProperty(...)] attribute has a few possible values: - <empty>: Nothing specified makes this an optional property you can leave empty when defining the configuration setting. - Mandatory: A Property that MUST be set when defining the configuration setting. - Key: The property is considered as the identifier for the resource modified. It is mandatory AND there cannot be multiple configuration entries with the same value for this property! - NotConfigurable: ReadOnly property. Mostly used for integration into Azure Guest Configurations Example Properties: [DscProperty(Key)] [string]$Path [DscProperty(Mandatory)] [string]$Text #> [DscProperty(Mandatory, Key)] [string] $SectionPath [DscProperty(Key)] [string] $Site [DscProperty(Key)] [string] $Path [DscProperty(Mandatory, Key)] [string] $AttributeName [DscProperty(Mandatory)] [string] $AttributeValue [DscProperty()] [Ensure]$Ensure = 'Present' [DscProperty(NotConfigurable)] [Reason[]] $Reasons # Reserved for Azure Guest Configuration #endregion DSC Properties [object] GetValue() { return $this.AttributeValue | ConvertFrom-Json } [void]Set() { # Apply Desired State $currentElement = $this.GetElement() if ($null -eq $currentElement) { throw 'Element not found: {0} > {1} > {2}' -f $this.SectionPath, $this.Site, $this.Path } Set-IISConfigAttributeValue -ConfigElement $currentElement -AttributeName $this.AttributeName -AttributeValue $this.GetValue().PSObject.BaseObject } [IISConfigAttribute]Get() { # Return current actual state $currentConfig = $this.GetElement() $current = [IISConfigAttribute]::new() $current.SectionPath = $this.SectionPath $current.Site = $this.Site $current.Path = $this.Path $current.AttributeName = $this.AttributeName $current.AttributeValue = @($currentConfig.Attributes).Where{ $_.Name -eq $this.AttributeName }.Value $current.Ensure = 'Present' if (-not $currentConfig) { $current.Ensure = 'Absent' } return $current } [bool]Test() { # Check whether current state = desired state $current = $this.Get() return ( $this.Ensure -eq $current.Ensure -and $this.GetValue() -eq $current.AttributeValue ) } [object]GetElement() { $pathElements = $this.Path -split '/' $param = @{ SectionPath = $this.SectionPath } try { if ($this.Site -and $this.SectionPath -notlike 'system.applicationHost/*') { $param.CommitPath = $this.Site } $currentConfig = Get-IISConfigSection @param if ($this.SectionPath -eq 'system.applicationHost/sites' -and $this.Site) { $collection = Get-IISConfigCollection -ConfigElement $currentConfig $currentConfig = Get-IISConfigCollectionElement -ConfigCollection $collection -ConfigAttribute @{ name = $this.Site } } foreach ($pathElement in $pathElements) { if (-not $pathElement) { continue } $currentConfig = Get-IISConfigElement -ConfigElement $currentConfig -ChildElementName $pathElement } } catch { return $null } return $currentConfig } [Hashtable] GetConfigurableDscProperties() { # This method returns a hashtable of properties with two special workarounds # The hashtable will not include any properties marked as "NotConfigurable" # Any properties with a ValidateSet of "True","False" will beconverted to Boolean type # The intent is to simplify splatting to functions # Source: https://gist.github.com/mgreenegit/e3a9b4e136fc2d510cf87e20390daa44 $dscProperties = @{} foreach ($property in [IISConfigAttribute].GetProperties().Name) { # Checks if "NotConfigurable" attribute is set $notConfigurable = [IISConfigAttribute].GetProperty($property).GetCustomAttributes($false).Where({ $_ -is [System.Management.Automation.DscPropertyAttribute] }).NotConfigurable if (!$notConfigurable) { $value = $this.$property # Gets the list of valid values from the ValidateSet attribute $validateSet = [IISConfigAttribute].GetProperty($property).GetCustomAttributes($false).Where({ $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues if ($validateSet) { # Workaround for boolean types if ($null -eq (Compare-Object @('True', 'False') $validateSet)) { $value = [System.Convert]::ToBoolean($this.$property) } } # Add property to new $dscProperties.add($property, $value) } } return $dscProperties } } [DscResource()] class IISConfigCollection { #region DSC Properties <# The properties you can define in configuration settings. The [DscProperty(...)] attribute has a few possible values: - <empty>: Nothing specified makes this an optional property you can leave empty when defining the configuration setting. - Mandatory: A Property that MUST be set when defining the configuration setting. - Key: The property is considered as the identifier for the resource modified. It is mandatory AND there cannot be multiple configuration entries with the same value for this property! - NotConfigurable: ReadOnly property. Mostly used for integration into Azure Guest Configurations #> [DscProperty(Mandatory, Key)] [string] $SectionPath [DscProperty(Key)] [string] $Site [DscProperty(Key)] [string] $Path [DscProperty(Mandatory, Key)] [string] $Name # PowerShell Code that will be passed to Where-Object to determine legal collection elements # E.g. '$false' (Empty Collection), '$_.Name -in "txt","json"' (Elements with name txt or json) [DscProperty(Mandatory)] [string] $Filter [DscProperty()] [Ensure] $Ensure = 'Present' [DscProperty(NotConfigurable)] [Reason[]] $Reasons # Reserved for Azure Guest Configuration #endregion DSC Properties [array] $Elements [void]Set() { # Apply Desired State if ($this.Test()) { return } $parent = $this.GetElement() if (-not $parent) { throw "Parent Element of collection $($this.Name) does not exist: '$($this.SectionPath) > $($this.Site) > $($this.Path)'" } $current = $this.Get() if ( $this.Ensure -eq 'Absent' -and $current.Ensure -eq 'Present' ) { throw 'Deleting collections is not supported' } if ( $this.Ensure -eq 'Present' -and $current.Ensure -eq 'Absent' ) { throw 'Creating collections is not supported' } try { $code = [scriptblock]::Create($this.Filter) } catch { throw "Invalid Filter: $($this.Filter)" } $collection = Get-IISConfigCollection -ConfigElement $parent -CollectionName $this.Name # Note: RawAttributes is a calculated property and regenerated on each request foreach ($element in $collection) { Add-Member -InputObject $element -MemberType NoteProperty -Name AttributesEx -Value $element.RawAttributes } $legalElementsAttributes = $($collection).AttributesEx | Where-Object $code $legalElements = $collection | Where-Object AttributesEx -in $legalElementsAttributes $countChanges = 0 foreach ($illegal in $collection | Where-Object { $_ -notin $legalElements } ) { $collection.Remove($illegal) $countChanges++ } if (0 -eq $countChanges) { return } $iis = Get-IISServerManager $iis.CommitChanges() } [IISConfigCollection]Get() { # Return current actual state $current = [IISConfigCollection]::new() $current.SectionPath = $this.SectionPath $current.Site = $this.Site $current.Path = $this.Path $current.Name = $this.Name $current.Filter = $this.Filter $parent = $this.GetElement() if (-not $parent) { $current.Ensure = 'Absent' return $current } $collection = Get-IISConfigCollection -ConfigElement $parent -CollectionName $this.Name if (-not $collection) { $current.Ensure = 'Absent' return $current } $current.Ensure = 'Present' $current.Elements = $($collection) return $current } [bool]Test() { # Check whether current state = desired state $collection = $this.Get() if ($this.Ensure -ne $collection.Ensure) { return $false } if (-not $this.Filter) { return $true } try { $code = [scriptblock]::Create($this.Filter) } catch { throw "Invalid Filter: $($this.Filter)" } # Note: RawAttributes is a calculated property and regenerated on each request foreach ($element in $collection.Elements) { Add-Member -InputObject $element -MemberType NoteProperty -Name AttributesEx -Value $element.RawAttributes } $legalElementsAttributes = $($collection.Elements).AttributesEx | Where-Object $code $legalElements = $collection.Elements | Where-Object AttributesEx -in $legalElementsAttributes if ($collection.Elements | Where-Object { $_ -notin $legalElements }) { return $false } return $true } [object]GetElement() { $pathElements = $this.Path -split '/' $param = @{ SectionPath = $this.SectionPath } try { if ($this.Site -and $this.SectionPath -notlike 'system.applicationHost/*') { $param.CommitPath = $this.Site } $currentConfig = Get-IISConfigSection @param if ($this.SectionPath -eq 'system.applicationHost/sites' -and $this.Site) { $collection = Get-IISConfigCollection -ConfigElement $currentConfig $currentConfig = Get-IISConfigCollectionElement -ConfigCollection $collection -ConfigAttribute @{ name = $this.Site } } foreach ($pathElement in $pathElements) { if (-not $pathElement) { continue } $currentConfig = Get-IISConfigElement -ConfigElement $currentConfig -ChildElementName $pathElement } } catch { return $null } return $currentConfig } [Hashtable] GetConfigurableDscProperties() { # This method returns a hashtable of properties with two special workarounds # The hashtable will not include any properties marked as "NotConfigurable" # Any properties with a ValidateSet of "True","False" will beconverted to Boolean type # The intent is to simplify splatting to functions # Source: https://gist.github.com/mgreenegit/e3a9b4e136fc2d510cf87e20390daa44 $dscProperties = @{} foreach ($property in [IISConfigCollection].GetProperties().Name) { # Checks if "NotConfigurable" attribute is set $notConfigurable = [IISConfigCollection].GetProperty($property).GetCustomAttributes($false).Where({ $_ -is [System.Management.Automation.DscPropertyAttribute] }).NotConfigurable if (!$notConfigurable) { $value = $this.$property # Gets the list of valid values from the ValidateSet attribute $validateSet = [IISConfigCollection].GetProperty($property).GetCustomAttributes($false).Where({ $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues if ($validateSet) { # Workaround for boolean types if ($null -eq (Compare-Object @('True', 'False') $validateSet)) { $value = [System.Convert]::ToBoolean($this.$property) } } # Add property to new $dscProperties.add($property, $value) } } return $dscProperties } } [DscResource()] class IISConfigCollectionItem { #region DSC Properties <# The properties you can define in configuration settings. The [DscProperty(...)] attribute has a few possible values: - <empty>: Nothing specified makes this an optional property you can leave empty when defining the configuration setting. - Mandatory: A Property that MUST be set when defining the configuration setting. - Key: The property is considered as the identifier for the resource modified. It is mandatory AND there cannot be multiple configuration entries with the same value for this property! - NotConfigurable: ReadOnly property. Mostly used for integration into Azure Guest Configurations Example Properties: [DscProperty(Key)] [string]$Path [DscProperty(Mandatory)] [string]$Text [DscProperty(Mandatory)] [Ensure]$Ensure #> [DscProperty(Mandatory, Key)] [string] $SectionPath [DscProperty(Key)] [string] $Site [DscProperty(Key)] [string] $Path [DscProperty(Mandatory, Key)] [string] $CollectionName [DscProperty(Mandatory)] [string] $Data [DscProperty(Mandatory, Key)] [string]$ItemID [DscProperty()] [bool] $ExactMatch = $true # Filter condition, on whether an item in the collection is our configured element (which might need to be updated) # If NOT specified, all properties on each item will be compared with the entries in the hashtable defined in $Data [DscProperty()] [string] $Filter [DscProperty()] [Ensure]$Ensure = 'Present' [DscProperty(NotConfigurable)] [Reason[]] $Reasons # Reserved for Azure Guest Configuration #endregion DSC Properties [array] $MatchingItems [hashtable]GetData() { $content = $this.Data | ConvertFrom-Json return $content | ConvertTo-Hashtable } [void]Set() { # Apply Desired State $current = $this.Get() if (@($current.MatchingItems).Count -gt 1) { throw 'Unexpected state: More than one collection item matches the intended item!' } if ($this.Test()) { return } # Case: Create if (-not $current.MatchingItems) { $parent = $this.GetElement() $collection = Get-IISConfigCollection -ConfigElement $parent -CollectionName $this.CollectionName New-IISConfigCollectionElement -ConfigCollection $collection -ConfigAttribute $this.GetData() } # Case: Delete elseif ($this.Ensure -eq 'Absent') { $parent = $this.GetElement() $collection = Get-IISConfigCollection -ConfigElement $parent -CollectionName $this.CollectionName $collection.Remove($this.MatchingItems) } # Case: Update else { $dataSet = $this.GetData() foreach ($key in $dataSet.Keys) { if ($dataSet.$key -eq $this.MatchingItems.RawAttributes.$key) { continue } Set-IISConfigAttributeValue -ConfigElement $this.MatchingItems -AttributeName $key -AttributeValue $dataSet.$key } } $iis = Get-IISServerManager $iis.CommitChanges() } [IISConfigCollectionItem]Get() { # Return current actual state $code = {} if ($this.Filter) { try { $code = [scriptblock]::Create($this.Filter) } catch { throw "Invalid PowerShell Syntax in Filter! $($this.Filter)" } } $current = [IISConfigCollectionItem]::new() $current.SectionPath = $this.SectionPath $current.Site = $this.Site $current.Path = $this.Path $current.CollectionName = $this.CollectionName $current.ItemID = $this.ItemID $current.ExactMatch = $this.ExactMatch $current.Filter = $this.Filter $parent = $this.GetElement() $collection = Get-IISConfigCollection -ConfigElement $parent -CollectionName $this.CollectionName #region Match Elements # Matching by filter if ($this.Filter) { foreach ($element in $collection) { Add-Member -InputObject $element -MemberType NoteProperty -Name AttributesEx -Value $element.RawAttributes } $matchingAttributes = $($collection).AttributesEx | Where-Object $code $matching = $collection | Where-Object AttributesEx -In $matchingAttributes } # Match by Attribute Match else { $matching = $collection | Where-Object { Test-Hashtable -Intended $this.GetData() -Actual $_.RawAttributes -ExactMatch:$this.ExactMatch } } #endregion Match Elements #region Process Matches # Case: No match found if (-not $matching) { $current.Ensure = 'Absent' return $current } # Case: Exact one match found if (@($matching).Count -eq 1) { $current.Ensure = 'Present' $newData = @{} foreach ($key in $matching.RawAttributes.Keys) { $newData[$key] = $matching.RawAttributes.$key } $current.Data = $newData | ConvertTo-Json -Depth 99 $current.MatchingItems = $matching return $current } # Case: Too many matches found $current.Ensure = 'Present' $current.MatchingItems = $matching return $current #endregion Process Matches } [bool]Test() { # Check whether current state = desired state $current = $this.Get() if ($current.Ensure -ne $this.Ensure) { return $false } if (@($current.MatchingItems).Count -gt 1) { return $false } if ($this.Ensure -eq 'Absent') { return $true } return Test-Hashtable -Intended $this.GetData() -Actual $current.GetData() -ExactMatch:$this.ExactMatch } [object]GetElement() { $pathElements = $this.Path -split '/' $param = @{ SectionPath = $this.SectionPath } try { if ($this.Site -and $this.SectionPath -notlike 'system.applicationHost/*') { $param.CommitPath = $this.Site } $currentConfig = Get-IISConfigSection @param if ($this.SectionPath -eq 'system.applicationHost/sites' -and $this.Site) { $collection = Get-IISConfigCollection -ConfigElement $currentConfig $currentConfig = Get-IISConfigCollectionElement -ConfigCollection $collection -ConfigAttribute @{ name = $this.Site } } foreach ($pathElement in $pathElements) { if (-not $pathElement) { continue } $currentConfig = Get-IISConfigElement -ConfigElement $currentConfig -ChildElementName $pathElement } } catch { return $null } return $currentConfig } [Hashtable] GetConfigurableDscProperties() { # This method returns a hashtable of properties with two special workarounds # The hashtable will not include any properties marked as "NotConfigurable" # Any properties with a ValidateSet of "True","False" will beconverted to Boolean type # The intent is to simplify splatting to functions # Source: https://gist.github.com/mgreenegit/e3a9b4e136fc2d510cf87e20390daa44 $dscProperties = @{} foreach ($property in [IISConfigCollectionItem].GetProperties().Name) { # Checks if "NotConfigurable" attribute is set $notConfigurable = [IISConfigCollectionItem].GetProperty($property).GetCustomAttributes($false).Where({ $_ -is [System.Management.Automation.DscPropertyAttribute] }).NotConfigurable if (!$notConfigurable) { $value = $this.$property # Gets the list of valid values from the ValidateSet attribute $validateSet = [IISConfigCollectionItem].GetProperty($property).GetCustomAttributes($false).Where({ $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues if ($validateSet) { # Workaround for boolean types if ($null -eq (Compare-Object @('True', 'False') $validateSet)) { $value = [System.Convert]::ToBoolean($this.$property) } } # Add property to new $dscProperties.add($property, $value) } } return $dscProperties } } function Compare-Hashtable { <# .SYNOPSIS Compares a provided reference hashtable to a provided difference hashtable. .DESCRIPTION Compares a provided reference hashtable to a provided difference hashtable and returns the differences as specified. .PARAMETER ReferenceHashtable The reference hashtable to use for comparison. .PARAMETER DifferenceHashtable The difference hashtable to use for comparison. .PARAMETER IncludeEqual When specified, results are returned when the Reference and Difference are equal. .PARAMETER ExcludeDifferent When specified, results are not returned when the Reference and Difference are different. .EXAMPLE PS C:\> Compare-Hashtable -ReferenceHashtable $MyConfigReference -DifferenceHashtable (Get-Something -Config) Compares the loaded ReferenceHashtable to the result of Get-Something. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable] $ReferenceHashtable, [Parameter(Mandatory = $true)] [hashtable] $DifferenceHashtable, [switch] $IncludeEqual, [switch] $ExcludeDifferent ) begin { function New-Change { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [hashtable] $Reference, [hashtable] $Difference, [string] $Name, [ValidateSet('==', '!=', '=>', '<=')] [string] $Direction, [AllowEmptyCollection()] [AllowEmptyString()] [AllowNull()] $RefValue, [AllowEmptyCollection()] [AllowEmptyString()] [AllowNull()] $DifValue ) [PSCustomObject]@{ PSTypeName = 'Hashtable.Comparison' Reference = $Reference Difference = $Difference Name = $Name Direction = $Direction RefValue = $RefValue DifValue = $DifValue } } } process { $paramChange = @{ Difference = $DifferenceHashtable Reference = $ReferenceHashtable } $flatReference = Read-HashTable -Hashtable $ReferenceHashtable -AsHashtable $flatDifference = Read-HashTable -Hashtable $DifferenceHashtable -AsHashtable foreach ($pair in $flatReference.GetEnumerator()) { if (-not $ExcludeDifferent) { if ($flatDifference.Keys -notcontains $pair.Key) { New-Change @paramChange -Name $pair.Key -Direction '=>' -RefValue $pair.Value continue } if ($pair.Value -ne $flatDifference[$pair.Key] -or $flatDifference[$pair.Key] -ne $pair.Value) { New-Change @paramChange -Name $pair.Key -Direction '!=' -RefValue $pair.Value -DifValue $flatDifference[$pair.Key] } } if ($IncludeEqual) { if ($pair.Value -eq $flatDifference[$pair.Key] -and $flatDifference[$pair.Key] -eq $pair.Value) { New-Change @paramChange -Name $pair.Key -Direction '==' -RefValue $pair.Value -DifValue $flatDifference[$pair.Key] } } } foreach ($pair in $flatDifference.GetEnumerator()) { if (-not $ExcludeDifferent -and $flatReference.Keys -notcontains $pair.Key) { New-Change @paramChange -Name $pair.Key -Direction '<=' -DifValue $pair.Value continue } } } } <# # Examle usage $hash1 = @{ Foo = 23 Bar = 1 Data = @{ Answer = 42 Name = 'Fred' Age = 37 } } $hash2 = @{ Foo = 42 Data = @{ Answer = 42 Name = 'Max' Age = @(37,38) } } Read-HashTable -Hashtable $hash1 Compare-Hashtable -ReferenceHashtable $hash1 -DifferenceHashtable $hash2 | ft Name, Direction, DifValue, RefValue Compare-Hashtable -ReferenceHashtable $hash1 -DifferenceHashtable $hash1 | ft Name, Direction, DifValue, RefValue Compare-Hashtable -ReferenceHashtable $hash1 -DifferenceHashtable $hash1 -IncludeEqual | ft Name, Direction, DifValue, RefValue Compare-Hashtable -ReferenceHashtable $hash1 -DifferenceHashtable $hash2 -IncludeEqual -ExcludeDifferent | ft Name, Direction, DifValue, RefValue #> function ConvertTo-Hashtable { <# .SYNOPSIS Simple Object to Hashtable conversion. .DESCRIPTION Simple Object to Hashtable conversion. Only converts at a flat level. - Hashtables are cloned - Other Dictionaries are translated to hashtable - Other items have their PSObject's properties enumerated and copied to hashtable Primitive types will not be handled gracefully and likely end in an empty hashtable. .PARAMETER InputObject The object to convert to hashtable. .EXAMPLE PS C:\> $data | ConvertTo-Hashtable Converts $data to hashtable. #> [OutputType([hashtable])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $InputObject ) process { if ($null -eq $InputObject) { return } if ($InputObject -is [hashtable]) { return $InputObject.Clone() } $hash = @{} if ($InputObject -is [System.Collections.IDictionary]) { foreach ($key in $InputObject.Keys) { $hash[$key] = $InputObject.$key } } else { foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = $property.Value } } $hash } } function Read-Hashtable { <# .Synopsis Reads a hash table and returns its contents as key/value objects. .DESCRIPTION Reads a hash table and returns its contents as a hierarchical structure. Use the AsHashtable switch to return the result as a flat hashtable, rather than one object per. .PARAMETER Hashtable The input hash table that is to be read. .PARAMETER Namespace The namespace under which the keys should be read. Used to recursively resolve hashtables .PARAMETER AsHashtable Specifies if the contents of the hash table are returned as an object hierarchy or as a hash table itself. .EXAMPLE $struct = @{ first = @{ second = '213' } } Read-Hashtable -Hashtable $struct Name Value ---- ----- first.second 213 #> [OutputType([hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable] $Hashtable, [string] $Namespace, [switch] $AsHashtable ) $prefix = '' if ($Namespace) { $prefix = "$Namespace." } $results = foreach ($pair in $Hashtable.GetEnumerator()) { $name = '{0}{1}' -f $prefix, $pair.Key if ($pair.Value -is [hashtable] -and $pair.Value.Count -gt 0) { Read-HashTable -Namespace $name -Hashtable $pair.Value continue } [PSCustomObject]@{ Name = $name Value = $pair.Value } } if (-not $AsHashtable) { return $results } $resultHash = @{ } foreach ($result in $results) { $resultHash[$result.Name] = $result.Value } $resultHash } function Test-Hashtable { <# .SYNOPSIS Tests, whether the provided Hashtable matches the intended one. .DESCRIPTION Tests, whether the provided Hashtable matches the intended one. Compares hashtables / dictionaries in depth, including nested hashtables. .PARAMETER Intended The hashtable containing the desired data. .PARAMETER Actual The object collected from the field that is compared to the desired state. .PARAMETER ExactMatch Require an exact match between the two hashtables. By default, the actual hashtable may contain entries in addition to the oones the intended one requires. .EXAMPLE PS C:\> Test-Hashtable -Intended $template -Actual $data Tests, whether the hashtable in $data has all the settings defined in $template. $data may contain additional settings, beyond what is defined in $template. .EXAMPLE PS C:\> Test-Hashtable -Intended $template -Actual $data -ExactMatch Tests, whether the hashtable in $data has all the settings defined in $template. $data may NOT contain additional settings, beyond what is defined in $template. #> [OutputType([bool])] [CmdletBinding()] param ( [System.Collections.IDictionary] $Intended, [System.Collections.IDictionary] $Actual, [switch] $ExactMatch ) process { $intendedHash = @{} if ($Intended -is [hashtable]) { $intendedHash = $Intended } else { foreach ($key in $Intended.Keys) { $intendedHash[$key] = $Intended[$Key] } } $actualHash = @{} if ($Actual -is [hashtable]) { $actualHash = $Actual } else { foreach ($key in $Actual.Keys) { $actualHash[$key] = $Actual[$Key] } } $delta = Compare-Hashtable -ReferenceHashtable $intendedHash -DifferenceHashtable $actualHash if (-not $delta) { return $true } if ($delta.Direction -contains '!=') { return $false } if ($delta.Direction -contains '<=' -and $ExactMatch) { return $false } if ($delta.Direction -contains '=>') { return $false } return $true } } |