Carbon.DSC.psm1
# Copyright WebMD Health Services # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License #Requires -Version 5.1 Set-StrictMode -Version 'Latest' # Functions should use $moduleRoot as the relative root from which to find # things. A published module has its function appended to this file, while a # module in development has its functions in the Functions directory. $script:moduleRoot = $PSScriptRoot $psModulesRoot = $script:moduleRoot Import-Module -Name (Join-Path -Path $psModulesRoot -ChildPath 'Carbon.Cryptography' -Resolve) ` -Function @('Get-CCertificate') ` -Verbose:$false Import-Module -Name (Join-Path -Path $psModulesRoot -ChildPath 'Carbon.Core' -Resolve) ` -Function @('Resolve-CFullPath') ` -Verbose:$false # Store each of your module's functions in its own file in the Functions # directory. On the build server, your module's functions will be appended to # this file, so only dot-source files that exist on the file system. This allows # developers to work on a module without having to build it first. Grab all the # functions that are in their own files. $functionsPath = Join-Path -Path $script:moduleRoot -ChildPath 'Functions\*.ps1' if( (Test-Path -Path $functionsPath) ) { foreach( $functionPath in (Get-Item $functionsPath) ) { . $functionPath.FullName } } function Clear-CDscLocalResourceCache { <# .SYNOPSIS Clears the local DSC resource cache. .DESCRIPTION DSC caches resources. This is painful when developing, since you're constantly updating your resources. This function allows you to clear the DSC resource cache on the local computer. What this function really does, is kill the DSC host process running DSC. `Clear-CDscLocalResourceCache` is new in Carbon 2.0. .EXAMPLE Clear-CDscLocalResourceCache #> [CmdletBinding()] param( ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState Get-CimInstance -Class 'msft_providers' | Where-Object {$_.provider -like 'dsccore'} | Select-Object -ExpandProperty HostProcessIdentifier | ForEach-Object { Get-Process -ID $_ } | Stop-Process -Force } function Clear-CMofAuthoringMetadata { <# .SYNOPSIS Removes authoring metadata from .mof files. .DESCRIPTION Everytime PowerShell generates a .mof file, it includes authoring metadata: who created the file, on what computer, and at what date/time. This means a .mof file's checksum will change everytime a new one is generated, even if the configuration in that file didn't change. This makes it hard to know when a configuration in a .mof file has truly changed, and makes its change history noisy. This function strips/removes all authoring metadata from a .mof file. When given a path to a file, all authoring metadata is removed from that file. When given the path to a directory, removes authoring metadata from all `*.mof` files in that directory. Essentially, these blocks from each .mof file: /* @TargetNode='********' @GeneratedBy=******** @GenerationDate=08/19/2014 13:29:15 @GenerationHost=******** */ /* ...snip... */ instance of OMI_ConfigurationDocument { Version="1.0.0"; Author="********; GenerationDate="08/19/2014 13:29:15"; GenerationHost="********"; }; Would be changed to: /* @TargetNode='JSWEB01L-WHS-08' */ /* ...snip... */ instance of OMI_ConfigurationDocument { Version="1.0.0"; }; `Clear-CMofAuthoringMetadata` is new in Carbon 2.0. .EXAMPLE Clear-CMofAuthoringMetadata -Path 'C:\Projects\DSC\localhost.mof' Demonstrates how to clear the authoring data from a specific file. .EXAMPLE Clear-CMofAuthoringMetadata -Path 'C:\Projects\DSC' Demonstrates how to clear the authoring data from all .mof files in a specific directory. #> [CmdletBinding(SupportsShouldProcess)] param( # The path to the file/directory whose .mof files should be operated on. [Parameter(Mandatory)] [String] $Path ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $tempDirName = "CDsc+Clear-CMofAuthoringMetadata+$([IO.Path]::GetRandomFileName())" $tempDir = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath $tempDirName New-Item -Path $tempDir -ItemType 'Directory' -WhatIf:$false foreach( $item in (Get-ChildItem -Path $Path -Filter '*.mof') ) { Write-Verbose ('Clearing authoring metadata from ''{0}''.' -f $item.FullName) $tempItem = Copy-Item -Path $item.FullName -Destination $tempDir -PassThru -WhatIf:$false $inComment = $false $inAuthoringComment = $false $inConfigBlock = $false; Get-Content -Path $tempItem | Where-Object { $line = $_ if( $line -like '/`**' ) { if( $line -like '*`*/' ) { return $true } $inComment = $true return $true } if( $inComment ) { if( $line -like '*`*/' ) { $inComment = $false $inAuthoringComment = $false return $true } if( $line -like '@TargetNode=*' ) { $inAuthoringComment = $true return $true } if( $inAuthoringComment ) { return ( $line -notmatch '^@(GeneratedBy|Generation(Host|Date))' ) } return $true } if( $line -eq 'instance of OMI_ConfigurationDocument' ) { $inConfigBlock = $true return $true } if( $inConfigBlock ) { if( $line -like '};' ) { $inConfigBlock = $false; return $true } return ($line -notmatch '(Author|(Generation(Date|Host)))='); } return $true } | Set-Content -Path $item.FullName } } function Copy-CDscResource { <# .SYNOPSIS Copies DSC resources. .DESCRIPTION This function copies a DSC resource or a directory of DSC resources to a DSC pull server share/website. All files under `$Path` are copied. DSC requires all files have a checksum file (e.g. `localhost.mof.checksum`), which this function generates for you (in a temporary location). Only new files, or files whose checksums have changed, are copied. You can force all files to be copied with the `Force` switch. `Copy-CDscResource` is new in Carbon 2.0. .EXAMPLE Copy-CDscResource -Path 'localhost.mof' -Destination '\\dscserver\DscResources' Demonstrates how to copy a single resource to a resources SMB share. `localhost.mof` will only be copied if its checksum is different than what is in `\\dscserver\DscResources`. .EXAMPLE Copy-CDscResource -Path 'C:\Projects\DscResources' -Destination '\\dscserver\DscResources' Demonstrates how to copy a directory of resources. Only files in the directory are copied. Every file in the source must have a `.checksum` file. Only files whose checksums are different between source and destination will be copied. .EXAMPLE Copy-CDscResource -Path 'C:\Projects\DscResources' -Destination '\\dscserver\DscResources' -Recurse Demonstrates how to recursively copy files. .EXAMPLE Copy-CDscResource -Path 'C:\Projects\DscResources' -Destination '\\dscserver\DscResources' -Force Demonstrates how to copy all files, even if their `.checksum` files are the same. .EXAMPLE Copy-CDscResource -Path 'C:\Projects\DscResources' -Destination '\\dscserver\DscResources' -PassThru Demonstrates how to get `System.IO.FileInfo` objects for all resources copied to the destination. If all files are up-to-date, nothing is copied, and no objects are returned. #> [CmdletBinding()] [OutputType([IO.FileInfo])] param( # The path to the DSC resource to copy. If a directory is given, all files in that directory are copied. # Wildcards supported. [Parameter(Mandatory)] [String] $Path, # The directory where the resources should be copied. [Parameter(Mandatory)] [String] $Destination, # Recursively copy files from the source directory. [switch] $Recurse, # Returns `IO.FileInfo` objects for each item copied to `Destination`. [switch] $PassThru, # Copy resources, even if they are the same on the destination server. [switch] $Force ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $tempDirName = "CDsc+Copy-CDscResource+$([IO.Path]::GetRandomFileName())" $tempDir = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath $tempDirName New-Item -Path $tempDir -ItemType 'Directory' | Out-Null try { foreach( $item in (Get-ChildItem -Path $Path -Exclude '*.checksum') ) { $destinationPath = Join-Path -Path $Destination -ChildPath $item.Name if( $item.PSIsContainer ) { if( $Recurse ) { if( -not (Test-Path -Path $destinationPath -PathType Container) ) { New-Item -Path $destinationPath -ItemType 'Directory' | Out-Null } Copy-CDscResource -Path $item.FullName -Destination $destinationPath -Recurse -Force:$Force -PassThru:$PassThru } continue } $sourceChecksumPath = '{0}.checksum' -f $item.Name $sourceChecksumPath = Join-Path -Path $tempDir -ChildPath $sourceChecksumPath $sourceChecksum = Get-FileHash -Path $item.FullName | Select-Object -ExpandProperty 'Hash' # hash files can't have any newline characters, so we can't use Set-Content [IO.File]::WriteAllText($sourceChecksumPath, $sourceChecksum) $destinationChecksum = '' $destinationChecksumPath = '{0}.checksum' -f $destinationPath if( (Test-Path -Path $destinationChecksumPath -PathType Leaf) ) { $destinationChecksum = Get-Content -TotalCount 1 -Path $destinationChecksumPath } if( $Force -or -not (Test-Path -Path $destinationPath -PathType Leaf) -or ($sourceChecksum -ne $destinationChecksum) ) { Copy-Item -Path $item -Destination $Destination -PassThru:$PassThru Copy-Item -Path $sourceChecksumPath -Destination $Destination -PassThru:$PassThru } else { Write-Verbose ('File ''{0}'' already up-to-date.' -f $destinationPath) } } } finally { Remove-Item -Path $tempDir -Recurse -Force -ErrorAction Ignore } } function Get-CDscError { <# .SYNOPSIS Gets DSC errors from a computer's event log. .DESCRIPTION The DSC Local Configuration Manager (LCM) writes any errors it encounters to the `Microsoft-Windows-DSC/Operational` event log, in addition to some error messages that report that encountered an error. This function gets just the important error log messages, skipping the superflous ones that won't help you track down where the problem is. By default, errors on the local computer are returned. You can return errors from another computer via the `ComputerName` parameter. You can filter the results further with the `StartTime` and `EndTime` parameters. `StartTime` will return entries after the given time. `EndTime` will return entries before the given time. If no items are found, nothing is returned. It can take several seconds for event log entries to get written to the log, so you might not get results back. If you want to wait for entries to come back, use the `-Wait` switch. You can control how long to wait (in seconds) via the `WaitTimeoutSeconds` parameter. The default is 10 seconds. When getting errors on a remote computer, that computer must have Remote Event Log Management firewall rules enabled. To enable them, run Get-CFirewallRule -Name '*Remove Event Log Management*' | ForEach-Object { netsh advfirewall firewall set rule name= $_.Name new enable=yes } `Get-CDscError` is new in Carbon 2.0. .OUTPUTS System.Diagnostics.Eventing.Reader.EventLogRecord .LINK Write-CDscError .EXAMPLE Get-CDscWinEvent Demonstrates how to get all the DSC errors from the local computer. .EXAMPLE Get-CDscError -ComputerName 10.1.2.3 Demonstrates how to get all the DSC errors from a specific computer. .EXAMPLE Get-CDscError -StartTime '8/1/2014 0:00' Demonstrates how to get errors that occurred *after* a given time. .EXAMPLE Get-CDscError -EndTime '8/30/2014 11:59:59' Demonstrates how to get errors that occurred *before* a given time. .EXAMPLE Get-CDscError -StartTime '8/1/2014 2:58 PM' -Wait -WaitTimeoutSeconds 5 Demonstrates how to wait for entries that match the specified criteria to appear in the event log. It can take several seconds between the time a log entry is written to when you can read it. #> [CmdletBinding(DefaultParameterSetName='NoWait')] [OutputType([Diagnostics.Eventing.Reader.EventLogRecord])] param( [string[]] # The computer whose DSC errors to return. $ComputerName, [DateTime] # Get errors that occurred after this date/time. $StartTime, [DateTime] # Get errors that occurred before this date/time. $EndTime, [Parameter(Mandatory, ParameterSetName='Wait')] [switch] # Wait for entries to appear, as it can sometimes take several seconds for entries to get written to the event log. $Wait, [Parameter(ParameterSetName='Wait')] [uint32] # The time to wait for entries to appear before giving up. Default is 10 seconds. There is no way to wait an infinite amount of time. $WaitTimeoutSeconds = 10 ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState Get-CDscWinEvent @PSBoundParameters -ID 4103 -Level ([Diagnostics.Eventing.Reader.StandardEventLevel]::Error) } function Get-CDscWinEvent { <# .SYNOPSIS Gets events from the DSC Windows event log. .DESCRIPTION Thie `Get-CDscWinEvent` function gets log entries from the `Microsoft-Windows-DSC/Operational` event log, where the Local Configuration Manager writes events. By default, entries on the local computer are returned. You can return entries from another computer via the `ComputerName` parameter. You can filter the results further with the `ID`, `Level`, `StartTime` and `EndTime` parameters. `ID` will get events with the specific ID. `Level` will get events at the specified level. `StartTime` will return entries after the given time. `EndTime` will return entries before the given time. If no items are found, nothing is returned. It can take several seconds for event log entries to get written to the log, so you might not get results back. If you want to wait for entries to come back, use the `-Wait` switch. You can control how long to wait (in seconds) via the `WaitTimeoutSeconds` parameter. The default is 10 seconds. When getting errors on a remote computer, that computer must have Remote Event Log Management firewall rules enabled. To enable them, run Get-CFirewallRule -Name '*Remove Event Log Management*' | ForEach-Object { netsh advfirewall firewall set rule name= $_.Name new enable=yes } `Get-CDscWinEvent` is new in Carbon 2.0. .OUTPUTS System.Diagnostics.Eventing.Reader.EventLogRecord .LINK Write-CDscError .LINK Get-CDscWinEvent .EXAMPLE Get-CDscWinEvent Demonstrates how to get all the DSC errors from the local computer. .EXAMPLE Get-CDscWinEvent -ComputerName 10.1.2.3 Demonstrates how to get all the DSC errors from a specific computer. .EXAMPLE Get-CDscWinEvent -StartTime '8/1/2014 0:00' Demonstrates how to get errors that occurred *after* a given time. .EXAMPLE Get-CDscWinEvent -EndTime '8/30/2014 11:59:59' Demonstrates how to get errors that occurred *before* a given time. .EXAMPLE Get-CDscWinEvent -StartTime '8/1/2014 2:58 PM' -Wait -WaitTimeoutSeconds 5 Demonstrates how to wait for entries that match the specified criteria to appear in the event log. It can take several seconds between the time a log entry is written to when you can read it. .EXAMPLE Get-CDscWinEvent -Level ([Diagnostics.Eventing.Reader.StandardEventLevel]::Error) Demonstrates how to get events at a specific level, in this case, only error level entries will be returned. .EXAMPLE Get-CDscWinEvent -ID 4103 Demonstrates how to get events with a specific ID, in this case `4103`. #> [CmdletBinding(DefaultParameterSetName='NoWait')] [OutputType([Diagnostics.Eventing.Reader.EventLogRecord])] param( [string[]] # The computer whose DSC errors to return. $ComputerName, [int] # The event ID. Only events with this ID will be returned. $ID, [int] # The level. Only events at this level will be returned. $Level, [DateTime] # Get errors that occurred after this date/time. $StartTime, [DateTime] # Get errors that occurred before this date/time. $EndTime, [Parameter(Mandatory, ParameterSetName='Wait')] [switch] # Wait for entries to appear, as it can sometimes take several seconds for entries to get written to the event log. $Wait, [Parameter(ParameterSetName='Wait')] [uint32] # The time to wait for entries to appear before giving up. Default is 10 seconds. There is no way to wait an infinite amount of time. $WaitTimeoutSeconds = 10 ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $filter = @{ LogName = 'Microsoft-Windows-DSC/Operational'; } if( $ID ) { $filter['ID'] = $ID } if( $Level ) { $filter['Level'] = $Level } if( $StartTime ) { $filter['StartTime'] = $StartTime } if( $EndTime ) { $filter['EndTime'] = $EndTime } function Invoke-GetWinEvent { param( [String] $ComputerName ) Set-StrictMode -Version 'Latest' $startedAt = Get-Date $computerNameParam = @{ } if( $ComputerName ) { $computerNameParam['ComputerName'] = $ComputerName } try { $events = @() while( -not ($events = Get-WinEvent @computerNameParam -FilterHashtable $filter -ErrorAction Ignore -Verbose:$false) ) { if( $PSCmdlet.ParameterSetName -ne 'Wait' ) { break } Start-Sleep -Milliseconds 100 [timespan]$duration = (Get-Date) - $startedAt if( $duration.TotalSeconds -gt $WaitTimeoutSeconds ) { break } } return $events } catch { if( $_.Exception.Message -eq 'The RPC server is unavailable' ) { Write-Error -Message ("Unable to connect to '{0}': it looks like Remote Event Log Management isn't running or is blocked by the computer's firewall. To allow this traffic through the firewall, run the following command on '{0}':`n`tGet-FirewallRule -Name '*Remove Event Log Management*' |`n`t`t ForEach-Object {{ netsh advfirewall firewall set rule name= `$_.Name new enable=yes }}." -f $ComputerName) } else { Write-Error -Exception $_.Exception } } } if( $ComputerName ) { $ComputerName = $ComputerName | Where-Object { # Get just the computers that exist. if( (Test-Connection -ComputerName $ComputerName -Quiet) ) { return $true } else { Write-Error -Message ('Computer ''{0}'' not found.' -f $ComputerName) return $false } } if( -not $ComputerName ) { return } $ComputerName | ForEach-Object { Invoke-GetWinEvent -ComputerName $_ } } else { Invoke-GetWinEvent } } # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. function Initialize-CLcm { <# .SYNOPSIS Configures a computer's DSC Local Configuration Manager (LCM). .DESCRIPTION The Local Configuration Manager (LCM) is the Windows PowerShell Desired State Configuration (DSC) engine. It runs on all target computers, and it is responsible for calling the configuration resources that are included in a DSC configuration script. It can be configured to receive changes (i.e. `Push` mode) or pull and apply changes its own changes (i.e. `Pull` mode). ## Push Mode Push mode is simplest. The LCM only applies configurations that are pushed to it via `Start-DscConfiguration`. It is expected that all resources needed by the LCM are installed and available on the computer. To use `Push` mode, use the `Push` switch. ## Pull Mode ***NOTE: You can't use `Initialize-CLcm` to put the local configuration manager in pull mode on Windows 2016 or later.*** In order to get a computer to pulls its configuration automatically, you need to configure its LCM so it knows where and how to find its DSC pull server. The pull server holds all the resources and modules needed by the computer's configuration. The LCM can pull from two sources: a DSC website (the web download manager) or an SMB files hare (the file download manager). To use the web download manager, specify the URL to the website with the `ServerUrl` parameter. To use the file download manager, specify the path to the resources with the `SourcePath` parameter. This path can be an SMB share path or a local (on the LCM's computer) file system path. No matter where the LCM pulls its configuration from, you're responsible for putting all modules, resources, and .mof files at that location. The most frequently the LCM will *download* new configuration is every 15 minutes. This is the minimum interval. The refresh interval is set via the `RefreshIntervalMinutes` parameter. The LCM will only *apply* a configuration on one of the refreshes. At most, it will apply configuration every 2nd refresh (i.e. every other refresh). You can control the frequency when configuration is applied via the `ConfigurationFrequency` parameter. For example, if `RefreshIntervalMinutes` is set to `30`, and `ConfigurationFrequency` is set to 4, then configuration will be downloaded every 30 minutes, and applied every two hours (i.e. `30 * 4 = 120` minutes). The `ConfigurationMode` parameter controls *how* the LCM applies its configuration. It supports three values: * `ApplyOnly`: Configuration is applied once and isn't applied again until a new configuration is detected. If the computer's configuration drifts, no action is taken. * `ApplyAndMonitor`: The same as `ApplyOnly`, but if the configuration drifts, it is reported in event logs. * `ApplyAndAutoCorrect`: The same as `ApplyOnly`, and when the configuratio drifts, the discrepency is reported in event logs, and the LCM attempts to correct the configuration drift. When credentials are needed on the target computer, the DSC system encrypts those credentials with a public key when generating the configuration. Those credentials are then decrypted on the target computer, using the corresponding private key. A computer can't run its configuration until the private key is installed. Use the `CertFile` and `CertPassword` parameters to specify the path to the certificate containing the private key and the private key's password, respectively. This function will use Carbon's `Install-CCertificate` function to upload the certificate to the target computer and install it in the proper Windows certificate store. To generate a public/private key pair, use `New-CRsaKeyPair`. Returns an object representing the computer's updated LCM settings. See [Windows PowerShell Desired State Configuration Local Configuration Manager](http://technet.microsoft.com/en-us/library/dn249922.aspx) for more information. This function is not available in 32-bit PowerShell 4 processes on 64-bit operating systems. `Initialize-CLcm` is new in Carbon 2.0. You cannot use `Initialize-CLcm .LINK New-CRsaKeyPair .LINK Start-CDscPullConfiguration .LINK Install-CCertificate .LINK http://technet.microsoft.com/en-us/library/dn249922.aspx .EXAMPLE Initialize-CLcm -Push -ComputerName '1.2.3.4' Demonstrates how to configure an LCM to use push mode. .EXAMPLE Initialize-CLcm -ConfigurationID 'fc2ffe50-13cd-4cd2-9942-d25ac66d6c13' -ComputerName '10.1.2.3' -ServerUrl 'https://10.4.5.6/PSDSCPullServer.dsc' Demonstrates the minimum needed to configure a computer (in this case, `10.1.2.3`) to pull its configuration from a DSC web server. You can't do this on Windows 2016 or later. .EXAMPLE Initialize-CLcm -ConfigurationID 'fc2ffe50-13cd-4cd2-9942-d25ac66d6c13' -ComputerName '10.1.2.3' -SourcePath '\\10.4.5.6\DSCResources' Demonstrates the minimum needed to configure a computer (in this case, `10.1.2.3`) to pull its configuration from an SMB file share. You can't do this on Windows 2016 or later. .EXAMPLE Initialize-CLcm -CertFile 'D:\Projects\Resources\PrivateKey.pfx' -CertPassword $secureStringPassword -ConfigurationID 'fc2ffe50-13cd-4cd2-9942-d25ac66d6c13' -ComputerName '10.1.2.3' -SourcePath '\\10.4.5.6\DSCResources' Demonstrates how to upload the private key certificate on to the targer computer(s). .EXAMPLE Initialize-CLcm -RefreshIntervalMinutes 25 -ConfigurationFrequency 3 -ConfigurationID 'fc2ffe50-13cd-4cd2-9942-d25ac66d6c13' -ComputerName '10.1.2.3' -SourcePath '\\10.4.5.6\DSCResources' Demonstrates how to use the `RefreshIntervalMinutes` and `ConfigurationFrequency` parameters to control when the LCM downloads new configuration and applies that configuration. In this case, new configuration is downloaded every 25 minutes, and apllied every 75 minutes (`RefreshIntervalMinutes * ConfigurationFrequency`). #> [CmdletBinding(SupportsShouldProcess)] param( # Configures the LCM to receive its configuration via pushes using `Start-DscConfiguration`. [Parameter(Mandatory, ParameterSetName='Push')] [switch] $Push, # Configures the LCM to pull its configuration from a DSC website using the web download manager [Parameter(Mandatory, ParameterSetName='PullWebDownloadManager')] [String] $ServerUrl, # When using the web download manager, allow the `ServerUrl` to use an unsecured, http connection when # contacting the DSC web pull server. [Parameter(ParameterSetName='PullWebDownloadManager')] [switch] $AllowUnsecureConnection, # Configures the LCM to pull its configuration from an SMB share or directory. This is the path to the SMB share # where resources can be found. Local paths are also allowed, e.g. `C:\DscResources`. [Parameter(Mandatory, ParameterSetName='PullFileDownloadManager')] [String] $SourcePath, # The GUID that identifies what configuration to pull to the computer. The Local Configuration Manager will look # for a '$Guid.mof' file to pull. [Parameter(Mandatory, ParameterSetName='PullWebDownloadManager')] [Parameter(Mandatory, ParameterSetName='PullFileDownloadManager')] [Guid] $ConfigurationID, # Specifies how the Local Configuration Manager applies configuration to the target computer(s). It supports # three values: `ApplyOnly`, `ApplyAndMonitor`, or `ApplyAndAutoCorrect`. [Parameter(Mandatory, ParameterSetName='PullWebDownloadManager')] [Parameter(Mandatory, ParameterSetName='PullFileDownloadManager')] [ValidateSet('ApplyOnly','ApplyAndMonitor','ApplyAndAutoCorrect')] [String] $ConfigurationMode, [Parameter(Mandatory)] [String[]] # The computer(s) whose Local Configuration Manager to configure. $ComputerName, [pscredential] # The credentials to use when connecting to the target computer(s). $Credential, # Controls whether new configurations downloaded from the configuration server are allowed to overwrite the old # ones on the target computer(s). [Parameter(ParameterSetName='PullWebDownloadManager')] [Parameter(ParameterSetName='PullFileDownloadManager')] [switch] $AllowModuleOverwrite, # The thumbprint of the certificate to use to decrypt secrets. If `CertFile` is given, this parameter is ignored # in favor of the certificate in `CertFile`. [Alias('Thumbprint')] [String] $CertificateID = $null, # The path to the certificate containing the private key to use when decrypting credentials. The certificate # will be uploaded and installed for you. [String] $CertFile, # The password for the certificate specified by `CertFile`. It can be a `string` or a `SecureString`. [securestring] $CertPassword, # Reboot the target computer(s) if needed. [Alias('RebootNodeIfNeeded')] [switch] $RebootIfNeeded, # The interval (in minutes) at which the target computer(s) will contact the pull server to *download* its # current configuration. The default (and minimum) interval is 15 minutes. [Parameter(ParameterSetName='PullWebDownloadManager')] [Parameter(ParameterSetName='PullFileDownloadManager')] [ValidateRange(30,[Int32]::MaxValue)] [Alias('RefreshFrequencyMinutes')] [int] $RefreshIntervalMinutes = 30, # The frequency (in number of `RefreshIntervalMinutes`) at which the target computer will run/implement its # current configuration. The default (and minimum) frequency is 2 refresh intervals. This value is multiplied by # the `RefreshIntervalMinutes` parameter to calculate the interval in minutes that the configuration is applied. [Parameter(ParameterSetName='PullWebDownloadManager')] [Parameter(ParameterSetName='PullFileDownloadManager')] [ValidateRange(1,([int]([Int32]::MaxValue)))] [int] $ConfigurationFrequency = 1, # The credentials the Local Configuration Manager should use when contacting the pull server. [Parameter(ParameterSetName='PullWebDownloadManager')] [Parameter(ParameterSetName='PullFileDownloadManager')] [pscredential] $LcmCredential ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if( $PSCmdlet.ParameterSetName -match '^Pull(File|Web)DownloadManager' ) { if( [Environment]::OSVersion.Version.Major -ge 10 ) { Write-Error -Message ('Initialize-CLcm can''t configure the local configuration manager to use the file or web download manager on Windows Server 2016 or later.') return } } $thumbprint = $null if( $CertificateID ) { $thumbprint = $CertificateID } $privateKey = $null if( $CertFile ) { $CertFile = Resolve-CFullPath -Path $CertFile if( -not (Test-Path -Path $CertFile -PathType Leaf) ) { Write-Error ('Certificate file ''{0}'' not found.' -f $CertFile) return } $privateKey = Get-CCertificate -Path $CertFile -Password $CertPassword if( -not $privateKey ) { return } if( -not $privateKey.HasPrivateKey ) { Write-Error ('Certificate file ''{0}'' does not have a private key.' -f $CertFile) return } $thumbprint = $privateKey.Thumbprint } $credentialParam = @{ } if( $Credential ) { $credentialParam.Credential = $Credential } $ComputerName = $ComputerName | Where-Object { if( Test-Connection -ComputerName $_ -Quiet ) { return $true } Write-Error ('Computer ''{0}'' not found or is unreachable.' -f $_) return $false } if( -not $ComputerName ) { return } # Upload the private key, if one was given. if( $privateKey ) { $session = New-PSSession -ComputerName $ComputerName @credentialParam if( -not $session ) { return } try { Install-CCertificate -Session $session ` -Path $CertFile ` -Password $CertPassword ` -StoreLocation ([Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) ` -StoreName ([Security.Cryptography.X509Certificates.StoreName]::My) | Out-Null } finally { Remove-PSSession -Session $session -WhatIf:$false } } $sessions = New-CimSession -ComputerName $ComputerName @credentialParam try { $originalWhatIf = $WhatIfPreference $WhatIfPreference = $false configuration Lcm { Set-StrictMode -Off $configID = $null if( $ConfigurationID ) { $configID = $ConfigurationID.ToString() } node $AllNodes.NodeName { if( $Node.RefreshMode -eq 'Push' ) { LocalConfigurationManager { CertificateID = $thumbprint; RebootNodeIfNeeded = $RebootIfNeeded; RefreshMode = 'Push'; } } else { if( $Node.RefreshMode -like '*FileDownloadManager' ) { $downloadManagerName = 'DscFileDownloadManager' $customData = @{ SourcePath = $SourcePath } } else { $downloadManagerName = 'WebDownloadManager' $customData = @{ ServerUrl = $ServerUrl; AllowUnsecureConnection = $AllowUnsecureConnection.ToString(); } } LocalConfigurationManager { AllowModuleOverwrite = $AllowModuleOverwrite; CertificateID = $thumbprint; ConfigurationID = $configID; ConfigurationMode = $ConfigurationMode; ConfigurationModeFrequencyMins = $RefreshIntervalMinutes * $ConfigurationFrequency; Credential = $LcmCredential; DownloadManagerCustomData = $customData; DownloadManagerName = $downloadManagerName; RebootNodeIfNeeded = $RebootIfNeeded; RefreshFrequencyMins = $RefreshIntervalMinutes; RefreshMode = 'Pull' } } } } $WhatIfPreference = $originalWhatIf $randomFileName = [IO.Path]::GetRandomFileName() $tempDir = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath "Carbon.DSC+Initialize-CLcm+${randomFileName}" New-Item -Path $tempDir -ItemType 'Directory' -WhatIf:$false | Out-Null try { [object[]]$allNodes = $ComputerName | ForEach-Object { @{ NodeName = $_; PSDscAllowPlainTextPassword = $true; RefreshMode = $PSCmdlet.ParameterSetName } } $configData = @{ AllNodes = $allNodes } $whatIfParam = @{ } if( (Get-Command -Name 'Lcm').Parameters.ContainsKey('WhatIf') ) { $whatIfParam['WhatIf'] = $false } & Lcm -OutputPath $tempDir @whatIfParam -ConfigurationData $configData | Out-Null Set-DscLocalConfigurationManager -ComputerName $ComputerName -Path $tempDir @credentialParam Get-DscLocalConfigurationManager -CimSession $sessions } finally { Remove-Item -Path $tempDir -Recurse -Force -WhatIf:$false -ErrorAction Ignore } } finally { Remove-CimSession -CimSession $sessions -WhatIf:$false } } function Start-CDscPullConfiguration { <# .SYNOPSIS Performs a configuration check on a computer that is using DSC's Pull refresh mode. .DESCRIPTION The most frequently a computer's LCM will download new configuration is every 15 minutes; the most frequently it will apply it is every 30 minutes. This function contacts a computer's LCM and tells it to apply and download its configuration immediately. If a computer's LCM isn't configured to pull its configuration, an error is written, and nothing happens. If a configuration check fails, the errors are retrieved from the computer's event log and written out as errors. The `Remote Event Log Management` firewall rules must be enabled on the computer for this to work. If they aren't, you'll see an error explaining this. The `Get-CDscError` help topic shows how to enable these firewall rules. Sometimes, the LCM does a really crappy job of updating to the latest version of a module. `Start-CDscPullConfiguration` will delete modules on the target computers. Specify the names of the modules to delete with the `ModuleName` parameter. Make sure you only delete modules that will get installed by the LCM. Only modules installed in the `$env:ProgramFiles\WindowsPowerShell\Modules` directory are removed. `Start-CDscPullConfiguration` is new in Carbon 2.0. .LINK Get-CDscError .LINK Initialize-CLcm .LINK Get-CDscWinEvent .EXAMPLE Start-CDscPullConfiguration -ComputerName '10.1.2.3','10.4.5.6' Demonstrates how to immedately download and apply a computer from its pull server. .EXAMPLE Start-CDscPullConfiguration -ComputerName '10.1.2.3' -Credential (Get-Credential domain\username) Demonstrates how to use custom credentials to contact the remote server. .EXAMPLE Start-CDscPullConfiguration -CimSession $session Demonstrates how to use one or more CIM sessions to invoke a configuration check. .EXAMPLE Start-CDscPullConfiguration -ComputerName 'example.com' -ModuleName 'Carbon' Demonstrates how to delete modules on the target computers, because sometimes the LCM does a really crappy job of it. #> [CmdletBinding(DefaultParameterSetName='WithCredentials')] param( [Parameter(Mandatory, ParameterSetName='WithCredentials')] [string[]] # The credential to use when connecting to the target computer. $ComputerName, [Parameter(ParameterSetName='WithCredentials')] [pscredential] # The credentials to use when connecting to the computers. $Credential, [Parameter(ParameterSetName='WithCimSession')] [Microsoft.Management.Infrastructure.CimSession[]] $CimSession, [string[]] # Any modules that should be removed from the target computer's PSModulePath (since the LCM does a *really* crappy job of removing them). $ModuleName ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $credentialParam = @{ } if( $PSCmdlet.ParameterSetName -eq 'WithCredentials' ) { if( $Credential ) { $credentialParam.Credential = $Credential } $CimSession = New-CimSession -ComputerName $ComputerName @credentialParam if( -not $CimSession ) { return } } $CimSession = Get-DscLocalConfigurationManager -CimSession $CimSession | ForEach-Object { if( $_.RefreshMode -ne 'Pull' ) { Write-Error ('The Local Configuration Manager on ''{0}'' is not in Pull mode (current RefreshMode is ''{1}'').' -f $_.PSComputerName,$_.RefreshMode) return } foreach( $session in $CimSession ) { if( $session.ComputerName -eq $_.PSComputerName ) { return $session } } } if( -not $CimSession ) { return } # Get rid of any _tmp directories you might find out there. Invoke-Command -ComputerName $CimSession.ComputerName @credentialParam -ScriptBlock { $modulesRoot = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules' Get-ChildItem -Path $modulesRoot -Filter '*_tmp' -Directory | Remove-Item -Recurse } if( $ModuleName ) { # Now, get rid of any modules we know will need to get updated Invoke-Command -ComputerName $CimSession.ComputerName @credentialParam -ScriptBlock { param( [string[]] $ModuleName ) $dscProcessID = Get-CCimInstance -Class 'msft_providers' | Where-Object {$_.provider -like 'dsccore'} | Select-Object -ExpandProperty HostProcessIdentifier Stop-Process -Id $dscProcessID -Force $modulesRoot = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules' Get-ChildItem -Path $modulesRoot -Directory | Where-Object { $ModuleName -contains $_.Name } | Remove-Item -Recurse } -ArgumentList (,$ModuleName) } # Getting the date/time on the remote computers so we can get errors later. $win32OS = Get-CimInstance -CimSession $CimSession -ClassName 'Win32_OperatingSystem' $results = Invoke-CimMethod -CimSession $CimSession ` -Namespace 'root/microsoft/windows/desiredstateconfiguration' ` -Class 'MSFT_DscLocalConfigurationManager' ` -MethodName 'PerformRequiredConfigurationChecks' ` -Arguments @{ 'Flags' = [uint32]1 } $successfulComputers = $results | Where-Object { $_ -and $_.ReturnValue -eq 0 } | Select-Object -ExpandProperty 'PSComputerName' $CimSession | Where-Object { $successfulComputers -notcontains $_.ComputerName } | ForEach-Object { $session = $_ $startedAt= $win32OS | Where-Object { $_.PSComputerName -eq $session.ComputerName } | Select-Object -ExpandProperty 'LocalDateTime' Get-CDscError -ComputerName $session.ComputerName -StartTime $startedAt -Wait } | Write-CDscError } function Test-CDscTargetResource { <# .SYNOPSIS Tests that all the properties on a resource and object are the same. .DESCRIPTION DSC expects a resource's `Test-TargetResource` function to return `$false` if an object needs to be updated. Usually, you compare the current state of a resource with the desired state, and return `$false` if anything doesn't match. This function takes in a hashtable of the current resource's state (what's returned by `Get-TargetResource`) and compares it to the desired state (the values passed to `Test-TargetResource`). If any property in the target resource is different than the desired resource, a list of stale resources is written to the verbose stream and `$false` is returned. Here's a quick example: return Test-TargetResource -TargetResource (Get-TargetResource -Name 'fubar') -DesiredResource $PSBoundParameters -Target ('my resource ''fubar''') If you want to exclude properties from the evaluation, just remove them from the hashtable returned by `Get-TargetResource`: $resource = Get-TargetResource -Name 'fubar' $resource.Remove( 'PropertyThatDoesNotMatter' ) return Test-TargetResource -TargetResource $resource -DesiredResource $PSBoundParameters -Target ('my resource ''fubar''') `Test-CDscTargetResource` is new in Carbon 2.0. .OUTPUTS System.Boolean. .EXAMPLE Test-TargetResource -TargetResource (Get-TargetResource -Name 'fubar') -DesiredResource $PSBoundParameters -Target ('my resource ''fubar''') Demonstrates how to test that all the properties on a DSC resource are the same was what's desired. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory=$true)] [hashtable] # The current state of the resource. $TargetResource, [Parameter(Mandatory=$true)] [hashtable] # The desired state of the resource. Properties not in this hashtable are skipped. Usually you'll pass `PSBoundParameters` from your `Test-TargetResource` function. $DesiredResource, [Parameter(Mandatory=$true)] [String] # The a description of the target object being tested. Output in verbose messages. $Target ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $notEqualProperties = $TargetResource.Keys | Where-Object { $_ -ne 'Ensure' } | Where-Object { $DesiredResource.ContainsKey( $_ ) } | Where-Object { $desiredObj = $DesiredResource[$_] $targetObj = $TargetResource[$_] if( $desiredobj -eq $null -or $targetObj -eq $null ) { return ($desiredObj -ne $targetObj) } if( -not $desiredObj.GetType().IsArray -or -not $targetObj.GetType().IsArray ) { return ($desiredObj -ne $targetObj) } if( $desiredObj.Length -ne $targetObj.Length ) { return $true } $desiredObj | Where-Object { $targetObj -notcontains $_ } } if( $notEqualProperties ) { Write-Verbose ('{0} has stale properties: ''{1}''' -f $Target,($notEqualProperties -join ''',''')) return $false } return $true } function Use-CallerPreference { <# .SYNOPSIS Sets the PowerShell preference variables in a module's function based on the callers preferences. .DESCRIPTION Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module. When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller: * ErrorAction * Debug * Confirm * InformationAction * Verbose * WarningAction * WhatIf This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function. This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d). There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get fixed. .LINK about_Preference_Variables .LINK about_CommonParameters .LINK https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d .LINK http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/ .EXAMPLE Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Demonstrates how to set the caller's common parameter preference variables in a module function. #> [CmdletBinding()] param ( [Parameter(Mandatory)] #[Management.Automation.PSScriptCmdlet] # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` # attribute. $Cmdlet, [Parameter(Mandatory)] # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the # `[CmdletBinding()]` attribute. # # Used to set variables in its callers' scope, even if that caller is in a different script module. [Management.Automation.SessionState]$SessionState ) Set-StrictMode -Version 'Latest' # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken # from about_CommonParameters). $commonPreferences = @{ 'ErrorActionPreference' = 'ErrorAction'; 'DebugPreference' = 'Debug'; 'ConfirmPreference' = 'Confirm'; 'InformationPreference' = 'InformationAction'; 'VerbosePreference' = 'Verbose'; 'WarningPreference' = 'WarningAction'; 'WhatIfPreference' = 'WhatIf'; } foreach( $prefName in $commonPreferences.Keys ) { $parameterName = $commonPreferences[$prefName] # Don't do anything if the parameter was passed in. if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) ) { continue } $variable = $Cmdlet.SessionState.PSVariable.Get($prefName) # Don't do anything if caller didn't use a common parameter. if( -not $variable ) { continue } if( $SessionState -eq $ExecutionContext.SessionState ) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } function Write-CDscError { <# .SYNOPSIS Writes DSC errors out as errors. .DESCRIPTION The Local Configuration Manager (LCM) applies configuration in a separate process space as a background service which writes its errors to the `Microsoft-Windows-DSC/Operational` event log. This function is intended to be used with `Get-CDscError`, and will write errors returned by that function as PowerShell errors. `Write-CDscError` is new in Carbon 2.0. .OUTPUTS System.Diagnostics.Eventing.Reader.EventLogRecord .LINK Get-CDscError .EXAMPLE Get-CDscError | Write-CDscError Demonstrates how `Write-CDscError` is intended to be used. `Get-CDscError` gets the appropriate event objects that `Write-CDscError` writes out. #> [CmdletBinding()] [OutputType([Diagnostics.Eventing.Reader.EventLogRecord])] param( [Parameter(Mandatory, ValueFromPipeline=$true)] [Diagnostics.Eventing.Reader.EventLogRecord[]] # The error record to write out as an error. $EventLogRecord, [switch] # Return the event log record after writing an error. $PassThru ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState foreach( $record in $EventLogRecord ) { [string[]]$property = $record.Properties | Select-Object -ExpandProperty Value $message = $property[-1] Write-Error -Message ('[{0}] [{1}] [{2}] {3}' -f $record.TimeCreated,$record.MachineName,($property[0..($property.Count - 2)] -join '] ['),$message) if( $PassThru ) { return $record } } } } |