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
            }
        }
    }
}