PortableLcm.psm1
data LocalizedData { # culture="en-US" ConvertFrom-StringData -StringData @' ContainerDetected = Folder detected for path '{0}'. FoundInstances = Found {0} resources in file {1}. ModulePresent = Module '{0}' with version '{1}' is present. InstallModule = Installing module '{0}' version '{1}' to scope '{2}'. MandatoryParameter = Parameter '{0}' is mandatory. ModuleNotPresent = Module '{0}-{1}' not present. Skipping resource '{2}'. ParametersValidated = Parameters for resource '{0}' passed validation. ResourceValidated = Resource '{0}' passed validation. CallExternalFunction = Calling {0}-TargetResource for resource '{1}'. ExternalFunctionError = Resource: {0}\n{1} Error: {2}. ParametersNotValidated = Parameters for resource '{0}' failed validation. ResourceNotInDesiredState = Resource '{0}' is not in desired state. ResourceInDesiredState = Resource '{0}' is in desired state. TestException = Exception thrown: {0}. MonitorOnlyResource = Resource '{0}' is set to monitor only. Set will be skipped. MofDoesNotExist = Publishing new MOF config '{0}' with hash '{1}'. HashMismatch = Hash mismatch for MOF '{0}'. Current hash: '{1}'. New hash: '{2}'. Overwriting existing configuration. ModeMismatch = Mode mismatch for MOF '{0}'. Current mode: '{1}'. New mode: '{2}'. Overwriting existing configuration. MofExists = MOF '{0}' with hash '{1}' already exists. Skipping. DependencyDesiredState = Resource '{0}' dependency '{1}' is in desired state. DependencyNotInDesiredState = Resource '{0}' dependency '{1}' is not in desired state, skipping. DependencySet = Resource '{0}' dependency '{1}' has been set. DependencyNotSet = Resource '{0}' dependency '{1}' has not been set, skipping. CopyMof = Copying '{0}' to {1}'. RebootRequiredNotAllowed = A reboot is required to finish applying configuration but reboots are not allowed. RebootNotRequired = A reboot is not required. Reboot = Rebooting to finish applying configuration. CredentialNotSupported = Credential property detected in resource '{0}'. Credentials are currently not supported. Skipping. LcmBusy = A compliance check is already in progress in process '{0}'. StopWait = Waiting '{0}' seconds before shutting down. MissingProcessId = Unable to stop lcm process. Process ID is missing from the configuration. '@ } function Initialize-Lcm { $configParentPath = Join-Path -Path $env:ProgramData -ChildPath 'PortableLcm' $configPath = Join-Path -Path $configParentPath -ChildPath 'config.json' New-Variable -Name 'MofConfigPath' -Option 'ReadOnly' -Scope 'Global' -Value $configPath -Force if (-not (Test-Path -Path $configPath)) { if (-not (Test-Path -Path $configParentPath)) { $null = New-Item -Path $configParentPath -ItemType 'Directory' } $config = [ordered]@{ Settings = @{ AllowReboot = $true Status = 'Idle' ProcessId = $null Cancel = $false CancelTimeoutInSeconds = 300 } Configurations = @() } $config | ConvertTo-Json | Out-File -FilePath $configPath } } class Resource { [string] $ResourceId [string] $Type [string] $ModuleName [string] $ModuleVersion [bool] $InDesiredState [string] $Exception [string] $LastSet [string] $LastTest [string] $Mode [string] $DependsOn [hashtable] $Properties Resource([string]$ResourceId, [string]$Type, [String]$ModuleName, [String]$ModuleVersion, [string]$Mode, [string]$DependsOn) { $this.ResourceId = $ResourceId $this.Type = $Type $this.ModuleName = $ModuleName $this.ModuleVersion = $ModuleVersion $this.Mode = $Mode $this.DependsOn = $DependsOn } Resource([string]$ResourceId, [string]$Type, [String]$ModuleName, [String]$ModuleVersion, [string]$Mode, [string]$DependsOn, [hashtable]$Properties) { $this.ResourceId = $ResourceId $this.Type = $Type $this.ModuleName = $ModuleName $this.ModuleVersion = $ModuleVersion $this.Mode = $Mode $this.DependsOn = $DependsOn $this.Properties = $Properties } } #region Helpers function Get-TimeStamp { return Get-Date -Format 'MM/dd/yy hh:mm:ss' } <# .SYNOPSIS Converts a CIM Instance to a Resource object with only relative properties. .PARAMETER Instance Cim instance to convert. .PARAMETER Mode Mode to apply to instance, either 'ApplyAndAutoCorrect' or 'ApplyAndMonitor'. .PARAMETER IncludeProperties If supplied, properties will be included with resource output. .EXAMPLE $resource = @{ ResourceID = '[RegistryPolicyFile][V-46473][medium][DTBI014-IE11-TLS setting]::[InternetExplorer]BrowserStig' ValueName = 'SecureProtocols' Ensure = 'Present' Key = 'Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings' ValueData = '{2560}' SourceInfo = 'C:\Program Files\WindowsPowerShell\Modules\PowerSTIG\4.3.0\DSCResources\Resources\windows.Registry.ps1::40::13::RegistryPolicyFile' ValueType = 'Dword' ModuleName = 'GPRegistryPolicyDsc' TargetType = 'ComputerConfiguration' ModuleVersion = '1.2.0' ConfigurationName = 'MyConfiguration' PSComputerName = '' } Convert-MofInstance -Instance $instance #> function Convert-MofInstance { [CmdletBinding()] [OutputType([Resource])] param ( [Parameter(Mandatory = $true)] [Microsoft.Management.Infrastructure.CimInstance] $Instance, [Parameter()] [ValidateSet('ApplyAndAutoCorrect', 'ApplyAndMonitor')] [string] $Mode = 'ApplyAndAutoCorrect', [Parameter()] [switch] $IncludeProperties ) if ($Instance.ResourceID -match '(?<=\[).*?(?=\])') { $type = $Matches[0] } if ($IncludeProperties) { $properties = Get-MofInstanceProperties -Instance $Instance return [Resource]::new($Instance.ResourceID, $type, $Instance.ModuleName, $Instance.ModuleVersion, $Mode, $Instance.DependsOn, $properties) } else { return [Resource]::new($Instance.ResourceID, $type, $Instance.ModuleName, $Instance.ModuleVersion, $Mode, $Instance.DependsOn) } } <# .SYNOPSIS Extracts properties embedded in a CIM instance. .PARAMETER Instance CIM instance to get properties from. #> function Get-MofInstanceProperties { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $true)] [Microsoft.Management.Infrastructure.CimInstance] $Instance ) $filterProperties = @('ConfigurationName', 'ModuleName', 'ModuleVersion', 'SourceInfo', 'ResourceID', 'PSComputerName') $properties = $Instance.CimInstanceProperties.Where({$filterProperties -notcontains $_.Name}) $propertyTable = @{} foreach ($property in $properties) { $type = $property.CimType.ToString() if ($type -notlike '*Array') { if ($type -eq 'SInt64') { $type = 'Long' } elseif ($type -eq 'Instance') { $typeName = ($property.Value | Get-Member).TypeName if ($typeName -contains 'Microsoft.Management.Infrastructure.CimInstance#MSFT_Credential') { throw ($LocalizedData.CredentialNotSupported -f $Instance.ResourceId) } } $propertyTable[$($property.Name)] = ($property.Value -as ([type]$type)) } else { $propertyTable[$($property.Name)] = @($property.Value) } } return $propertyTable } <# .SYNOPSIS Tests a resource's properties to ensure it is not missing any mandatory parameters. .PARAMETER Name Name of the function to test against. .PARAMETER Values Property hashtable to validate against the function. .EXAMPLE $properties = @{ ValueName = 'SecureProtocols' ValueData = '{2560}' Ensure = 'Present' ValueType = 'Dword' TargetType = 'ComputerConfiguration' Key = 'Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings' } Test-MandatoryParameter -Name 'Test-RegistryPolicyFileTargetResource' -Values $properties #> function Test-MandatoryParameter { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [Hashtable] $Values ) $ignoreResourceParameters = [System.Management.Automation.Cmdlet]::CommonParameters + [System.Management.Automation.Cmdlet]::OptionalCommonParameters $hasErrors = $false $command = Get-Command -Name $Name $parameterNames = $command.Parameters foreach ($key in $parameterNames.Keys) { if ($ignoreResourceParameters -notcontains $key) { $metadata = $command.Parameters.$($name) if ($($metadata.Attributes | Where-Object {$_.TypeId.Name -eq 'ParameterAttribute'}).Mandatory -and -not $Values.$($key)) { Write-Warning -Message ($LocalizedData.MandatoryParameter -f $key) $hasErrors = $true } } } return (-not $hasErrors) } <# .SYNOPSIS Tests if a specific version of a module is present. .PARAMETER ModuleName Name of the module. .PARAMETER ModuleVersion Version of the module. .EXAMPLE Test-ModulePresent -ModuleName 'ComputerManagementDsc' -ModuleVersion '1.0.0' #> function Test-ModulePresent { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string] $ModuleName, [Parameter(Mandatory = $true)] [string] $ModuleVersion ) $moduleMatches = Get-Module -Name $ModuleName -ListAvailable -Verbose:$false | Select-Object @{Name='Version'; Expression = {$_.Version.ToString()}} if ($moduleMatches.Version -contains $ModuleVersion) { Write-Verbose -Message ($LocalizedData.ModulePresent -f $ModuleName, $ModuleVersion) return $true } else { return $false } } <# .SYNOPSIS Checks to see if a hashtable contains valid parameters from a function. .PARAMETER Name Name of the function to test parameters against. .PARAMETER Values Hashtable of properties to test. .EXAMPLE $properties = @{ Key = 'Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings' TargetType = 'ComputerConfiguration' AccountName = '' PsDscRunAsCredential = '' ValueType = 'Dword' ValueData = '{2560}' Ensure = 'Present' ValueName = 'SecureProtocols' Path = '' DependsOn = '' } Merge-MofResourceParameter -Name 'Test-RegistryPolicyFileTargetResource' -Values $properties #> function Merge-MofResourceParameter { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [Hashtable] $Values ) $ignoreResourceParameters = [System.Management.Automation.Cmdlet]::CommonParameters + [System.Management.Automation.Cmdlet]::OptionalCommonParameters $command = Get-Command -Name $Name $parameterNames = $command.Parameters $properties = @{} foreach ($key in $parameterNames.Keys) { if ($ignoreResourceParameters -notcontains $key) { if ($Values.ContainsKey($key)) { $properties.Add($key, $Values.$key) } } } $verboseSetting = $PSCmdlet.MyInvocation.BoundParameters['Verbose'].IsPresent -and $PSCmdlet.MyInvocation.BoundParameters['Verbose'] $properties.Add('Verbose', $verboseSetting) $properties.Add('ErrorAction', 'Stop') return $properties } <# .SYNOPSIS Imports a function from a specified module with a prefix. .PARAMETER ModuleName Name of the module. .PARAMETER ModuleVersion Version of the module. .PARAMETER Operation Which function to get; get, set or test. .PARAMETER ResourceName Name of the DSC resource to retrieve a function from. .EXAMPLE Import-TempFunction -ModuleName ComputerManagementDsc -ModuleVersion 8.4.0 -Operation Get -ResourceName TimeZone #> function Import-TempFunction { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $true)] [string] $ModuleName, [Parameter(Mandatory = $true)] [string] $ModuleVersion, [Parameter(Mandatory = $true)] [ValidateSet('Get', 'Test', 'Set')] [string] $Operation, [Parameter(Mandatory = $true)] [string] $ResourceName ) $progPref = $ProgressPreference $global:ProgressPreference = 'SilentlyContinue' $functionName = "$Operation-TargetResource" $tempFunctionName = $functionName.Replace("-", "-$ResourceName") try { $dscResource = (Get-DscResource -Module $ModuleName -Name $ResourceName -Verbose:$false).Where({$_.Version -eq $ModuleVersion}) | Select-Object -First 1 if (-not (Get-Command -Name $tempFunctionName -ErrorAction 'SilentlyContinue') -and $null -ne $dscResource) { Import-Module -FullyQualifiedName $dscResource.Path -Function $functionName -Prefix $ResourceName -Verbose:$false } } catch { throw $_.Exception } finally { $global:ProgressPreference = $progPref } return @{ Name = $tempFunctionName Path = $dscResource.Path } } <# .SYNOPSIS Executes the test method for a given resource. .PARAMETER Resource Resource object to test. .EXAMPLE $resource = @{ Name = 'RegistryPolicyFile' MofFile = 'C:\Temp\myConfig.mof' ResourceId = '[RegistryPolicyFile][V-46473][medium][DTBI014-IE11-TLS setting]::[InternetExplorer]BrowserStig' ModuleName = 'GPRegistryPolicyDsc' ModuleVersion = '1.2.0' Property = @{ ValueName = 'SecureProtocols' ValueData = '{2560}' Ensure = 'Present' ValueType = 'Dword' TargetType = 'ComputerConfiguration' Key = 'Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings' } } Test-MofResource -Resource $resource #> function Test-MofResource { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [Resource] $Resource ) $verboseSetting = $PSCmdlet.MyInvocation.BoundParameters['Verbose'].IsPresent -and $PSCmdlet.MyInvocation.BoundParameters['Verbose'] try { $tempFunction = Import-TempFunction -ModuleName $Resource.ModuleName -ModuleVersion $Resource.ModuleVersion -ResourceName $Resource.Type -Operation 'Test' if (Test-MandatoryParameter -Name $tempFunction.Name -Values $Resource.Properties) { Write-Verbose -Message ($LocalizedData.ParametersValidated -f $Resource.ResourceId) $splatProperties = Merge-MofResourceParameter -Name $tempFunction.Name -Values $Resource.Properties -Verbose:$verboseSetting Write-Verbose -Message ($LocalizedData.CallExternalFunction -f 'Test', $Resource.Type) try { $result = &"$($tempFunction.Name)" @splatProperties } catch { throw $_.Exception } if ($result) { Write-Verbose -Message ($LocalizedData.ResourceInDesiredState -f $Resource.ResourceId) } else { Write-Warning -Message ($LocalizedData.ResourceNotInDesiredState -f $Resource.ResourceId) } return $result } else { Write-Warning -Message ($LocalizedData.ParametersNotValidated -f $Resource.ResourceId) } } catch { throw $_.Exception } finally { if ($null -ne $tempFunction -and $tempFunction.ContainsKey('Path') -and $null -ne $tempFunction.Path) { #Remove-Module -FullyQualifiedName $tempFunction.Path } } return $Resource } <# .SYNOPSIS Executes the set method for a given resource. .PARAMETER Resource Resource object to set. .EXAMPLE $resource = @{ Name = 'RegistryPolicyFile' MofFile = 'C:\Temp\myConfig.mof' ResourceId = '[RegistryPolicyFile][V-46473][medium][DTBI014-IE11-TLS setting]::[InternetExplorer]BrowserStig' ModuleName = 'GPRegistryPolicyDsc' ModuleVersion = '1.2.0' Property = @{ ValueName = 'SecureProtocols' ValueData = '{2560}' Ensure = 'Present' ValueType = 'Dword' TargetType = 'ComputerConfiguration' Key = 'Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings' } } Set-MofResource -Resource $resource #> function Set-MofResource { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Resource] $Resource ) $verboseSetting = $PSCmdlet.MyInvocation.BoundParameters['Verbose'].IsPresent -and $PSCmdlet.MyInvocation.BoundParameters['Verbose'] try { $tempFunction = Import-TempFunction -ModuleName $Resource.ModuleName -ModuleVersion $Resource.ModuleVersion -ResourceName $Resource.Type -Operation 'Set' if(Test-MandatoryParameter -Name $tempFunction.Name -Values $Resource.Properties) { Write-Verbose -Message ($LocalizedData.ParametersValidated -f $Resource.ResourceId) $splatProperties = Merge-MofResourceParameter -Name $tempFunction.Name -Values $Resource.Properties -Verbose:$verboseSetting Write-Verbose -Message ($LocalizedData.CallExternalFunction -f 'Set',$Resource.Type) try { $Resource.LastSet = Get-TimeStamp &"$($tempFunction.Name)" @splatProperties $Resource.InDesiredState = $true } catch { $Resource.Exception = $_.Exception throw $_.Exception } return $Resource } else { Write-Warning -Message ($LocalizedData.ParametersNotValidated -f $Resource.ResourceId) return } } catch { throw $_.Exception } finally { if ($null -ne $tempFunction -and $tempFunction.ContainsKey('Path') -and $null -ne $tempFunction.Path) { #Remove-Module -FullyQualifiedName $tempFunction.Path } } } function Write-EventLogEntry { param ( [Parameter(Mandatory = $true)] [string] $EntryMessage, [Parameter(Mandatory = $true)] [string[]] $EntryArguments, [Parameter()] [ValidateSet("Error", "Warning", "Information")] $EntryType = "Information" ) if ($env:OS -eq 'Windows_NT' -and $PSEdition -eq 'Desktop') { $eventParams = @{ Category = 8 # Pipeline Execution Details EventId = 1000 LogName = "Windows PowerShell" Source = "PowerShell" Message = ($EntryMessage -f $EntryArguments) EntryType = $EntryType } Write-EventLog @eventParams } if ($EntryType -in @("Error", "Warning")) { Write-Warning -Message ($EntryMessage -f $EntryArguments) } } <# .SYNOPSIS Converts resources from MOF file into a JSON file. .PARAMETER MofPath Path to the MOF file. .PARAMETER Mode DSC mode to apply to the configuration, either ApplyAndAutoCorrect (default) or ApplyAndMonitor. .PARAMETER Force Forces the overwrite of an existing JSON file. .EXAMPLE Import-MofConfig -MofPath C:\test\test.mof -JsonPath c:\test\test.json #> function Import-MofConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateScript({Test-Path -Path $_})] [ValidateScript({[System.IO.Path]::GetExtension($_) -eq '.mof'})] [string] $Path, [Parameter()] [ValidateSet('ApplyAndAutoCorrect', 'ApplyAndMonitor')] [string] $Mode = 'ApplyAndAutoCorrect' ) $allInstances = Get-MofCimInstances -Path $Path $output = @() foreach ($instance in $allInstances) { $output += Convert-MofInstance -Instance $instance -Mode $Mode } return $output } <# .SYNOPSIS Returns a sorted hashtable of DSC partial configuration dependencies .PARAMETER Graph Hashtable of DSC partial configuration dependencies #> function Invoke-SortDependencyGraph { param ( [Parameter(Mandatory = $true)] [Hashtable] $Graph ) # To hold ordered dependencies $sorted = @() # To hold remaining dependencies $remaining = @{} foreach ($key in $Graph.Keys) { $remaining[$key] = [System.Collections.Generic.List[string]]$Graph[$key] } while ($remaining.Count -gt 0) { $leaf = Get-Leaf $remaining if (-not $leaf) { throw "No leaf found in graph. Sorted: $sorted Remaining: $($remaining.Keys)" } # Remove leaf from remaining and add to ordered list $sorted += $leaf # Remove leaf from remaining $remaining.Remove($leaf) foreach ($key in $remaining.Keys) { [string]$leafName = $remaining[$key].Where( {$_ -ieq $leaf}) if (-not [string]::IsNullOrEmpty($leafName)) { $null = $remaining[$key].Remove($leafName) } } } return $sorted } function Get-Leaf { param ( [Parameter(Mandatory = $true)] [Hashtable] $Graph ) foreach ($key in $Graph.Keys) { if ($Graph[$key].Count -eq 0) { return $key } } } #endregion Helpers #region Public function Stop-Lcm { [CmdletBinding()] param ( [Parameter()] [switch] $Force ) $lcm = Get-LcmConfig if ($Force) { if (-not [string]::IsNullOrEmpty($lcm.Settings.ProcessId)) { Stop-Process -Id $lcm.Settings.ProcessId -Force Reset-Lcm } else { Write-Warning -Message $LocalizedData.MissingProcessId Reset-Lcm } } else { $lcm.Settings.Cancel = $true $lcm | ConvertTo-Json -Depth 6 | Out-File $MofConfigPath if (-not [string]::IsNullOrEmpty($lcm.Settings.CancelTimeoutInSeconds)) { $stopwatch = [System.Diagnostics.Stopwatch]::new() $stopwatch.Start() if ($null -ne (Get-Process -Id $lcm.Settings.ProcessId -ErrorAction 'SilentlyContinue')) { while ($stopwatch.Elapsed.TotalSeconds -lt $lcm.Settings.CancelTimeoutInSeconds) { Start-Sleep -Seconds 1 if ($null -eq (Get-Process -Id $lcm.Settings.ProcessId -ErrorAction 'SilentlyContinue')) { break } } } Stop-Process -Id $lcm.Settings.ProcessId -Force -ErrorAction 'SilentlyContinue' Reset-Lcm } } } function Reset-Lcm { [CmdletBinding()] param() $lcm = Get-LcmConfig $lcm.Settings.Cancel = $false $lcm.Settings.Status = 'Idle' $lcm.Settings.ProcessId = $null $lcm | ConvertTo-Json -Depth 6 | Out-File $MofConfigPath } <# .SYNOPSIS Applies configuration from a MOF file or from stored JSON configuration. .PARAMETER Path Path to the folder containing many MOF files or path to a singular MOF file. .EXAMPLE Assert-DscMofConfig -Path C:\temp\file.mof #> function Assert-DscMofConfig { [CmdletBinding()] [OutputType([bool])] param ( [Parameter()] [switch] $Force ) $verboseSetting = $PSCmdlet.MyInvocation.BoundParameters['Verbose'].IsPresent -and $PSCmdlet.MyInvocation.BoundParameters['Verbose'] Write-EventLogEntry -EntryMessage "Starting Portable LCM at {0}" -EntryArguments (Get-TimeStamp) New-Variable -Name 'DSCMachineStatus ' -Scope 'Global' -Value 0 -Force $lcm = Get-LcmConfig if ($lcm.Settings.Status -ne 'Idle' -and -not [string]::IsNullOrEmpty($lcm.Settings.ProcessId -and -not $Force)) { Write-Warning -Message ($LocalizedData.LcmBusy -f $lcm.Settings.ProcessId) return } elseif ($lcm.Settings.Status -ne 'Idle' -and -not [string]::IsNullOrEmpty($lcm.Settings.ProcessId) -and $Force) { Write-Verbose -Message ($LocalizedData.ForceStop) Stop-Lcm -Force } else { $lcm.Settings.Status = 'Busy' $lcm.Settings.ProcessId = $PID $lcm | ConvertTo-Json -Depth 6 | Out-File -FilePath $MofConfigPath } try { $allInstances = @() $lcmResourcesList = @() foreach($configuration in $lcm.Configurations) { $lcmResourcesList += $configuration.Resources $allInstances += Get-MofCimInstances -Path $configuration.MofPath } $graph = @{} foreach ($mofInstance in $allInstances) { $graph[$mofInstance.ResourceId] = $mofInstance.DependsOn } $sorted = Invoke-SortDependencyGraph -Graph $graph $count = 0 foreach ($resourceId in $sorted) { $instance = $allInstances.Where({$_.ResourceId -eq $resourceId}) | Select-Object -First 1 $updateResource = $lcmResourcesList.Where({$_.ResourceID -eq $resourceId}) | Select-Object -First 1 # Test that module is present. If not skip it. if (-not (Test-ModulePresent -ModuleName $instance.ModuleName -ModuleVersion $instance.ModuleVersion)) { Write-Warning -Message ($LocalizedData.ModuleNotPresent -f $instance.ModuleName, $instance.ModuleVersion, $instance.ResourceId) $updateResource.Exception = ($LocalizedData.ModuleNotPresent -f $instance.ModuleName, $instance.ModuleVersion, $instance.ResourceId) continue } $count++ Write-Progress -Activity "$count of $($allInstances.Count), $(($count/$($allInstances.Count)).ToString('P'))" -Status "$($instance.ResourceId)" -PercentComplete ($count/$($allInstances.Count)) # Check for dependencies. $skip = $false if ($null -ne $instance.DependsOn) { foreach ($dependencyId in $instance.DependsOn) { $dependencyInstance = $allInstances.Where({$_.ResourceId -eq $dependencyId}) | Select-Object -First 1 $dependencyInDesiredState = $dependencyInstance.InDesiredState if ($dependencyInDesiredState) { Write-Verbose -Message ($LocalizedData.DependencyInDesiredState -f $resourceId, $dependencyId) } else { $updateResource.Exception = ($LocalizedData.DependencyNotInDesiredState -f $resourceId, $dependencyId) Write-Warning -Message ($LocalizedData.DependencyNotInDesiredState -f $resourceId, $dependencyId) $skip = $true break } } } if ($skip) { continue } try { $resource = Convert-MofInstance -Instance $instance -IncludeProperties } catch { $updateResource.Exception = $_.Exception.Message Write-Warning -Message $_.Exception.Message continue } # Check for cancellation token $cancel = (Get-LcmConfig).Settings.Cancel if ($cancel -eq $true) { break } # Test resource try { $result = Test-MofResource -Resource $resource -Verbose:$verboseSetting $updateResource.LastTest = Get-TimeStamp $updateResource.InDesiredState = $result # Check for cancellation token $cancel = (Get-LcmConfig).Settings.Cancel if ($cancel -eq $true) { break } } catch { $updateResource.Exception = $_.Exception.Message $updateResource.InDesiredState = $false Write-EventLogEntry -EntryMessage "$($LocalizedData.ExternalFunctionError)" -EntryArguments @($updateResource.ResourceId,"Test", $_.Exception.Message) -EntryType Error continue } # Set resource if($resource.Mode -eq 'ApplyAndAutoCorrect' -and -not $result) { try { $result = Set-MofResource -Resource $resource -Verbose:$verboseSetting $updateResource.LastSet = Get-TimeStamp $updateResource.Exception = "" } catch { $updateResource.Exception = $_.Exception.Message $updateResource.InDesiredState = $false } } } # Update LCM status $lcm | ConvertTo-Json -Depth 6 | Out-File -FilePath $MofConfigPath Write-Progress -Completed -Activity 'Completed' if ($global:DSCMachineStatus -eq 1 -and $lcm.Settings.AllowReboot -eq 'true') { Write-Verbose -Message $LocalizedData.Reboot Restart-Computer -Force -Delay 15 } elseif($global:DSCMachineStatus -eq 1) { Write-Warning -Message $LocalizedData.RebootRequiredNotAllowed } else { Write-Verbose -Message $LocalizedData.RebootNotRequired } } finally { Reset-Lcm } Write-EventLogEntry -EntryMessage "Finishing Portable LCM at {0}" -EntryArguments (Get-TimeStamp) } function Get-LcmConfig { $config = Get-Content -Path $MofConfigPath | ConvertFrom-Json -WarningAction 'SilentlyContinue' if (-not (Test-Path -Path $MofConfigPath) -or ($null -eq $config)) { if (-not (Split-Path -Path $MofConfigPath -Parent)) { $null = New-Item -Path (Split-Path -Path $MofConfigPath -Parent) -ItemType 'Directory' } $config = [ordered]@{ Settings = @{ AllowReboot = $true Status = 'Idle' ProcessId = $null Cancel = $false CancelTimeoutInSeconds = 300 } Configurations = @() } $config | ConvertTo-Json | Out-File -FilePath $configPath } return Get-Content -Path $MofConfigPath | ConvertFrom-Json -WarningAction 'SilentlyContinue' } function Remove-DscMofConfig { [CmdletBinding()] param() DynamicParam { $configurations = (Get-LcmConfig).Configurations $attribute = New-Object System.Management.Automation.ParameterAttribute $attribute.Mandatory = $false $attribute.HelpMessage = "Name of the MOF" $attributeCollection = new-object System.Collections.ObjectModel.Collection[System.Attribute] $attributeCollection.Add($attribute) $validateSet = New-Object System.Management.Automation.ValidateSetAttribute($configurations.Name) $attributeCollection.add($validateSet) $param = New-Object System.Management.Automation.RuntimeDefinedParameter('Name', [string], $attributeCollection) $dictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $dictionary.Add('Name', $param) return $dictionary } begin { $Name = $PsBoundParameters['Name'] } process { $config = Get-LcmConfig $config.Configurations = $config.Configurations.Where({$_.Name -ne $Name}) $config | ConvertTo-Json -Depth 6 | Out-File -FilePath $MofConfigPath } } <# .SYNOPSIS Retrieves the current state of the current DSC MOF configuration. .PARAMETER Name Name of the MOF to retrieve the status for. Leaving this null will return status for all configured MOF's. .PARAMETER Full Returns details for all resources contained in the specified MOF. .EXAMPLE Get-DscMofStatus -Name myMofConfig -Detailed This will return the state of every resource in the myMofConfig MOF. .EXAMPLE Get-DscMofStatus This will return only the Name of the MOF(s) in the configuration and whether or not all the resources are in desired state for that MOF. #> function Get-DscMofStatus { [CmdletBinding()] param ( [Parameter()] [switch] $Full ) DynamicParam { $configurations = (Get-LcmConfig).Configurations $attribute = New-Object System.Management.Automation.ParameterAttribute $attribute.Mandatory = $false $attribute.HelpMessage = "Name of the MOF" $attributeCollection = new-object System.Collections.ObjectModel.Collection[System.Attribute] $attributeCollection.Add($attribute) $validateSet = New-Object System.Management.Automation.ValidateSetAttribute($configurations.Name) $attributeCollection.add($validateSet) $param = New-Object System.Management.Automation.RuntimeDefinedParameter('Name', [string], $attributeCollection) $dictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $dictionary.Add('Name', $param) return $dictionary } begin { $Name = $PsBoundParameters['Name'] } process { $overallStatus = @() $configurations = (Get-LcmConfig).Configurations if ($Name) { $configurations = $configurations.Where({$_.Name -eq $Name}) } foreach ($configuration in $configurations) { if (-not $Full) { foreach ($resource in $configuration.Resources) { $properties = [ordered]@{ Name = $resource.ResourceId InDesiredState = $resource.InDesiredState LastError = $resource.Exception } $overallStatus += New-Object -TypeName 'PSObject' -Property $properties } } else { $overallStatus += $configuration } } return $overallStatus } } function Install-DscMofModules { [CmdletBinding(DefaultParameterSetName = 'ByConfiguration')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'ByFile')] [ValidateScript({Test-Path -Path $_})] [string] $Path, [Parameter(ParameterSetName = 'ByFile')] [Parameter(ParameterSetName = 'ByConfiguration')] [ValidateSet('AllUsers', 'CurrentUser')] [string] $Scope = 'CurrentUser' ) if ($PSCmdlet.ParameterSetName -eq 'ByFile') { if (-not [string]::IsNullOrEmpty($Path) -and (Test-Path -Path $Path -PathType 'Container')) { Write-Verbose -Message ($LocalizedData.ContainerDetected -f $Path) $configFiles = (Get-ChildItem -Path $Path -Include "*.mof" -Recurse).FullName } elseif (-not [string]::IsNullOrEmpty($Path)) { $configFiles = $Path } } else { $configFiles = (Get-LcmConfig).Configurations.MofPath } $configResources = @() foreach ($configFile in $configFiles) { $configResources += Get-MofCimInstances -Path $configFile } $moduleGroups = $configResources | Group-Object -Property 'ModuleName', 'ModuleVersion' foreach ($moduleGroup in $moduleGroups) { $group = $moduleGroup.Group $moduleName = $group.ModuleName | Select-Object -First 1 $moduleVersion = $group.ModuleVersion | Select-Object -First 1 if(-not (Test-ModulePresent -ModuleName $moduleName -ModuleVersion $moduleVersion)) { Write-Verbose -Message $($LocalizedData.InstallModule -f $moduleName, $moduleVersion, $Scope) Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Scope $Scope -Verbose:$false -Force } } } <# .SYNOPSIS Publishes MOF file(s) to its internal configuration. .PARAMETER Path Path to a MOF file or folder containing many MOF files. .PARAMETER Mode Mode to apply to MOF file(s): ApplyAndMointor (default) or ApplyAndAutoCorrect .EXAMPLE Publish-DscMofConfig -Path c:\temp\myMof.mof #> function Publish-DscMofConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateScript({Test-Path -Path $_})] [string] $Path, [Parameter()] [ValidateSet('ApplyAndAutoCorrect', 'ApplyAndMonitor')] [string] $Mode = 'ApplyAndMonitor' ) # Read in configuration data $mofConfig = Get-LcmConfig $configurations = $mofConfig.Configurations if (Test-Path -Path $Path -PathType 'Container') { Write-Verbose -Message ($LocalizedData.ContainerDetected -f $Path) $mofFiles = (Get-ChildItem -Path $Path -Include "*.mof" -Recurse).FullName } else { $mofFiles = $Path } foreach ($mofFile in $mofFiles) { $hash = (Get-FileHash -Path $mofFile -Algorithm 'SHA512').Hash $mofFileName = [System.IO.Path]::GetFileName($mofFile) $mofName = [System.IO.Path]::GetFileNameWithoutExtension($mofFile) $mofCopyPath = Join-Path -Path (Split-Path -Path $MofConfigPath -Parent) -ChildPath $mofFileName $existingConfig = $configurations.Where({$_.Name -eq $mofName -and $_.MofPath -eq $mofCopyPath}) | Select-Object -First 1 $properties = [ordered]@{ Name = $mofName Hash = $hash MofPath = $mofCopyPath Mode = $Mode ResourceCount = $null Resources = @() } $mofCopyExists = $false $mofCopyHash = $null if (Test-Path -Path $mofCopyPath) { $mofCopyExists = $true $mofCopyHash = (Get-FileHash -Path $mofCopyPath -Algorithm 'SHA512').Hash } # MOF exists in config and matches all current values - skip it if ((-not $null -eq $existingConfig) -and $existingConfig.Hash -eq $hash -and $mofCopyExists -and $mofCopyHash -eq $hash -and $existingConfig.Mode -eq $Mode) { Write-Verbose -Message ($LocalizedData.MofExists -f $mofName, $hash) continue } # MOF exists in config but is a different mode elseif ((-not $null -eq $existingConfig) -and $existingConfig.Hash -eq $hash -and $mofCopyExists -and $mofCopyHash -eq $hash -and $existingConfig.Mode -ne $Mode ) { $existingConfig.mode = $Mode continue } Write-Verbose -Message ($LocalizedData.CopyMof -f $Path, $mofCopyPath) $null = Copy-Item -Path $mofFiles -Destination $mofCopyPath -Force $mofResources = Import-MofConfig -Path $mofFile -Mode $Mode $properties.ResourceCount = $mofResources.Count $properties.Resources += $mofResources if ($existingConfig.Count -gt 0) { $existingConfig.Hash = $hash $existingConfig.Mode = $Mode } else { $configurations += $properties } } $tempConfig = $mofConfig $tempConfig.Configurations = $configurations $tempConfig | ConvertTo-Json -Depth 6 -WarningAction 'SilentlyContinue' | Out-File -FilePath $MofConfigPath } Initialize-Lcm Export-ModuleMember -Function Initialize-Lcm, Assert-DscMofConfig, Test-DscMofConfig, Get-MofCimInstances, Publish-DscMofConfig, Get-LcmConfig, Get-DscMofStatus, Install-DscMofModules, Remove-DscMofConfig, Get-MofInstanceProperties, Assert-DscCompliance, Stop-Lcm, * |