DscLcm.psm1

<#
    .SYNOPSIS Modifies a DSC Local configuration manager setting
 
    .LINK https://docs.microsoft.com/en-us/powershell/dsc/metaconfig
 
    .PARAMETER CimSession
        The cimsession object or computer name of the target computer to be modified.
 
    .PARAMETER OutputPath
        The output path for mof files to be stored.
 
    .PARAMETER ConfigurationModeFrequencyMins
        How often, in minutes, the current configuration is checked and applied. This property is ignored if the ConfigurationMode property is set to ApplyOnly. The default value is 15.
 
    .PARAMETER RebootNodeIfNeeded
        Specifies whether or not the LCM can reboot the target
 
    .PARAMETER ConfigurationMode
        Specifies how the LCM actually applies the configuration to the target nodes.
 
    .PARAMETER ActionAfterReboot
        Specifies what happens after a reboot during the application of a configuration.
 
    .PARAMETER RefreshMode
        Specifies how the LCM gets configurations.
 
    .PARAMETER CertificateId
        The thumbprint of a certificate used to secure credentials passed in a configuration.
 
    .PARAMETER RefreshFrequencyMins
        The time interval, in minutes, at which the LCM checks a pull service to get updated configurations.
 
    .PARAMETER AllowModuleOverwrite
        Specifies if new configurations are allowed to overwrite old ones using Pull service.
     
    .PARAMETER DebugMode
        Specifies the debug mode for the target
 
    .PARAMETER StatusRetentionTimeInDays
        The number of days the LCM keeps the status of the current configuration.
 
    .PARAMETER DeleteMofWhenDone
        Specifies whether or not to cleanup the resulting meta.mof file
 
    .EXAMPLE This command will set the RebootNodeIfNeeded to 'True' on the target, 'localhost'
        Set-LcmSetting -CimSession localhost -RebootNodeIfNeeded $true
#>

Function Set-LcmSetting
{
    Param
    (
        [Parameter()]
        [psobject]
        $CimSession,
        
        [Parameter()]
        [int]
        $ConfigurationModeFrequencyMins,
        
        [Parameter()]
        [bool]
        $RebootNodeIfNeeded,
        
        [Parameter()]
        [ValidateSet('ApplyOnly','ApplyAndMonitor','ApplyAndAutoCorrect')]
        [string]
        $ConfigurationMode = 'ApplyAndAutoCorrect',
        
        [Parameter()]
        [ValidateSet('ContinueConfiguration','StopConfiguration')]
        [string]
        $ActionAfterReboot,
        
        [Parameter()]
        [ValidateSet('Disabled','Push','Pull')]
        [string]
        $RefreshMode,
        
        [Parameter()]
        [string]
        $CertificateId,
        
        [Parameter()]
        [guid]
        $ConfigurationId,
        
        [Parameter()]
        [int]
        $RefreshFrequencyMins,
        
        [Parameter()]
        [bool]
        $AllowModuleOverwrite,
        
        [Parameter()]
        [ValidateSet('None','ForceModuleImport','All')]
        [string]
        $DebugMode,
        
        [Parameter()]
        [int]
        $StatusRetentionTimeInDays,

        [Parameter()]        
        [string]
        $OutputPath = "C:\Windows\Temp\MofStore",
        
        [Parameter()]
        [boolean]
        $DeleteMofWhenDone = $true
    )

    #Requires -RunAsAdministrator

    if($CimSession)
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -CimSession $CimSession -ErrorAction Stop
        $computerName = Get-ComputerName -CimSession $CimSession

    }
    else
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -ErrorAction Stop
        $computerName = $env:COMPUTERNAME
        $CimSession = $env:COMPUTERNAME
    }

    $null = Test-OutputPath -Path $OutputPath
    $pendingChanges = $PSBoundParameters.Keys.Where({$script:commonParameters -notcontains $_})

    foreach($key in $pendingChanges)
    {
        $currentLcmConfig.$key = $PSBoundParameters[$key]
    }

    $configurations = Initialize-SettingsBlock -Configuration $currentLcmConfig
            
    foreach($partial in $currentLcmConfig.PartialConfigurations)
    {
        $configurations += Initialize-PartialBlock -Configuration $partial
    }
        
    $null = Invoke-LcmConfig -ComputerName $computerName -Configuration $configurations -OutputPath $OutputPath
    Set-DscLocalConfigurationManager -CimSession $CimSession -Path $OutputPath -Force

    if($DeleteMofWhenDone)
    {
        Remove-Item $OutputPath\$computerName.meta.mof -Force -ErrorAction Ignore
    }
}

<#
    .SYNOPSIS Resets a DSC Local configuration manager to a blank state
 
    .LINK https://docs.microsoft.com/en-us/powershell/dsc/metaconfig
     
    .PARAMETER CimSession
        The cimsession object or computer name of the target computer to be modified.
 
    .PARAMETER OutputPath
        The output path for mof files to be stored.
 
    .PARAMETER DeleteMofWhenDone
        Specifies whether or not to cleanup the resulting meta.mof file
 
    .EXAMPLE This command will reset the lcm on the target, 'localhost'
        Reset-LcmConfiguration -CimSession localhost
#>

Function Reset-LcmConfiguration
{
    [CmdletBinding()]
    Param
    (
        [Parameter()]
        [psobject]
        $CimSession,

        [string]
        $OutputPath = "C:\Windows\Temp\MofStore",

        [Parameter()]
        [boolean]
        $DeleteMofWhenDone = $true
    )

    #Requires -RunAsAdministrator
    
    $selectProperties = 'RebootNodeIfNeeded','ConfigurationMode','RefreshMode','ActionAfterReboot'
    
    if($CimSession)
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -CimSession $CimSession -ErrorAction Stop | Select-Object -Property $selectProperties
        $computerName = Get-ComputerName -CimSession $CimSession

    }
    else
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -ErrorAction Stop | Select-Object -Property $selectProperties
        $computerName = $env:COMPUTERNAME
        $CimSession = $env:COMPUTERNAME
    }

    $null = Test-OutputPath -Path $OutputPath
    $configurations = Initialize-SettingsBlock -Configuration $currentLcmConfig

    $null = Invoke-LcmConfig -ComputerName $computerName -Configuration $configurations -OutputPath $OutputPath
    Set-DscLocalConfigurationManager -CimSession $CimSession -Path $OutputPath -Force

    foreach($stage in @('Current','Previous','Pending'))
    {
        Remove-DscConfigurationDocument -CimSession $CimSession -Stage $stage -Force
    }

    if($DeleteMofWhenDone)
    {
        Remove-Item $OutputPath\$computerName.meta.mof -Force -ErrorAction Ignore
    }
}

<#
    .SYNOPSIS Removes a partial configuration by name from a target local configuration manager
 
    .LINK https://docs.microsoft.com/en-us/powershell/dsc/metaconfig
     
    .PARAMETER CimSession
        The cimsession object or computer name of the target computer to be modified.
 
    .PARAMETER OutputPath
        The output path for mof files to be stored.
 
    .PARAMETER ConfigurationSource
        An array of names of configuration servers, previously defined in ConfigurationRepositoryWeb and ConfigurationRepositoryShare blocks, where the partial configuration is pulled from.
     
    .PARAMETER Description
        Text used to describe the partial configuration.
             
    .PARAMETER ExclusiveResources
        An array of resources exclusive to this partial configuration.
 
    .PARAMETER RefreshMode
        Specifies what happens after a reboot during the application of a configuration.
 
    .PARAMETER ResourceModuleSource
        An array of the names of resource servers from which to download required resources for this partial configuration
 
    .PARAMETER DeleteMofWhenDone
        Specifies whether or not to cleanup the resulting meta.mof file
 
    .EXAMPLE This command will remove the partial configuration 'test partial' on the target, 'localhost'
        Remove-LcmPartialConfiguration -CimSession localhost -PartialName 'test partial'
#>

Function Remove-LcmPartialConfiguration
{
    [CmdletBinding()]
    Param
    (    
        [Parameter()]
        [psobject]
        $CimSession,
        
        [Parameter(Mandatory = $true)]
        [string]
        $PartialName,

        [Parameter()]
        [string]
        $OutputPath = "C:\Windows\Temp\MofStore",

        [Parameter()]
        [boolean]
        $DeleteMofWhenDone = $true
    )

    #Requires -RunAsAdministrator

    if($CimSession)
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -CimSession $CimSession -ErrorAction Stop
        $computerName = Get-ComputerName -CimSession $CimSession

    }
    else
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -ErrorAction Stop
        $computerName = $env:COMPUTERNAME
        $CimSession = $env:COMPUTERNAME
    }
    
    $null = Test-OutputPath -Path $OutputPath
    $partialConfiguration = $currentLcmConfig.PartialConfigurations.Where({$($_.ResourceId.Replace('[PartialConfiguration]','')) -eq $PartialName})

    if(-not $partialConfiguration)
    {
        throw "Invalid partial name, $PartialName."
    }

    $configurations = Initialize-SettingsBlock -Configuration $currentLcmConfig
            
    foreach($partial in $currentLcmConfig.PartialConfigurations)
    {
        if($partial.ResourceId.Replace('[PartialConfiguration]','') -ne $PartialName)
        {
            if($partial.DependsOn -contains "[PartialConfiguration]$PartialName")
            {
                $partial.DependsOn = $partial.DependsOn.Where({$_ -ne "[PartialConfiguration]$PartialName"})
            }

            $configurations += Initialize-PartialBlock -Configuration $partial
        }
    }

    $null = Invoke-LcmConfig -ComputerName $computerName -Configuration $configurations -OutputPath $OutputPath
    Set-DscLocalConfigurationManager -CimSession $CimSession -Path $OutputPath -Force

    if($DeleteMofWhenDone)
    {
        Remove-Item $OutputPath\$computerName.meta.mof -Force -ErrorAction Ignore
    }
}

<#
    .SYNOPSIS Adds a partial configuration by name from a target local configuration manager
 
    .LINK https://docs.microsoft.com/en-us/powershell/dsc/metaconfig
     
    .PARAMETER CimSession
        The cimsession object or computer name of the target computer to be modified.
 
    .PARAMETER OutputPath
        The output path for mof files to be stored.
 
    .PARAMETER PartialName
        The name of the partial configuration to add to the target
 
    .PARAMETER ConfigurationSource
        An array of names of configuration servers, previously defined in ConfigurationRepositoryWeb and ConfigurationRepositoryShare blocks, where the partial configuration is pulled from.
     
    .PARAMETER Description
        Text used to describe the partial configuration.
             
    .PARAMETER ExclusiveResources
        An array of resources exclusive to this partial configuration.
 
    .PARAMETER RefreshMode
        Specifies how the LCM gets configurations. The possible values are "Disabled", "Push", and "Pull".
 
    .PARAMETER ResourceModuleSource
        An array of the names of resource servers from which to download required resources for this partial configuration
 
    .PARAMETER DeleteMofWhenDone
        Specifies whether or not to cleanup the resulting meta.mof file
 
    .EXAMPLE This command will add the partial configuration 'test partial' on the target, 'localhost'
        Add-LcmPartialConfiguration -CimSession localhost -PartialName 'test partial'
#>

Function Add-LcmPartialConfiguration
{
    [CmdletBinding()]
    Param
    (    
        [Parameter()]
        [psobject]
        $CimSession,
        
        [Parameter(Mandatory = $true)]
        [string]
        $PartialName,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Disabled','Push','Pull')]
        [string]
        $RefreshMode,
        
        [Parameter()]
        [string[]]
        $ConfigurationSource,
        
        [Parameter()]
        [System.Collections.Generic.List[string]]
        $DependsOn,
        
        [Parameter()]
        [string]
        $Description,
        
        [Parameter()]
        [string[]]
        $ExclusiveResources,
        
        [Parameter()]
        [string[]]
        $ResourceModuleSource,

        [Parameter()]
        [string]
        $OutputPath = "C:\Windows\Temp\MofStore",

        [Parameter()]
        [boolean]
        $DeleteMofWhenDone = $true
    )
    
    #Requires -RunAsAdministrator

    if($CimSession)
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -CimSession $CimSession -ErrorAction Stop
        $computerName = Get-ComputerName -CimSession $CimSession

    }
    else
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -ErrorAction Stop
        $computerName = $env:COMPUTERNAME
        $CimSession = $env:COMPUTERNAME
    }
    
    $null = Test-OutputPath -Path $OutputPath
    $existingPartials = $currentLcmConfig.PartialConfigurations.ResourceId
        
    if($existingPartials -and $existingPartials.Replace('[PartialConfiguration]','') -contains $PartialName)
    {
        $partialExists = $true
        Write-Warning "Partial configuration $PartialName already exists on computer $ComputerName"
    }
        
    $pendingChanges = $PSBoundParameters.Keys.Where({$script:commonParameters -notcontains $_})

    $hashChanges = @{}
    foreach($pendingChange in $pendingChanges)
    {
        if($pendingChange -eq 'PartialName')
        {
            $hashChanges.Add("ResourceId", "[PartialConfiguration]$($PSBoundParameters[$pendingChange])")
        }
        elseif($pendingChange -eq 'DependsOn')
        {
            $hashChanges.Add($pendingChange, "[PartialConfiguration]$($PSBoundParameters[$pendingChange])")
        }
        else
        {
            $hashChanges.Add($pendingChange, $PSBoundParameters[$pendingChange])
        }
    }

    $configurations = Initialize-SettingsBlock -Configuration $currentLcmConfig
            
    foreach($partial in $currentLcmConfig.PartialConfigurations)
    {
        $configurations += Initialize-PartialBlock -Configuration $partial
    }

    if(-not $partialExists)
    {
        $configurations += Initialize-PartialBlock -Configuration $(New-Object -TypeName PsObject -Property $hashChanges)
    }

    $null = Invoke-LcmConfig -ComputerName $computerName -Configuration $configurations -OutputPath $OutputPath
    Set-DscLocalConfigurationManager -CimSession $CimSession -Path $OutputPath -Force

    if($DeleteMofWhenDone)
    {
        Remove-Item $OutputPath\$computerName.meta.mof -Force -ErrorAction Ignore
    }
}

<#
    .SYNOPSIS Modifies a DSC Local configuration manager setting
 
    .LINK https://docs.microsoft.com/en-us/powershell/dsc/metaconfig
 
    .PARAMETER CimSession
        The cimsession object or computer name of the target computer to be modified.
 
    .PARAMETER OutputPath
        The output path for mof files to be stored.
 
    .PARAMETER PartialName
        The name of the partial configuration to modify on the target.
 
    .PARAMETER ConfigurationSource
        An array of names of configuration servers, previously defined in ConfigurationRepositoryWeb and ConfigurationRepositoryShare blocks, where the partial configuration is pulled from.
     
    .PARAMETER Description
        Text used to describe the partial configuration.
             
    .PARAMETER ExclusiveResources
        An array of resources exclusive to this partial configuration.
 
    .PARAMETER RefreshMode
        Specifies how the LCM gets configurations. The possible values are "Disabled", "Push", and "Pull".
 
    .PARAMETER ResourceModuleSource
        An array of the names of resource servers from which to download required resources for this partial configuration
 
    .PARAMETER DeleteMofWhenDone
        Specifies whether or not to cleanup the resulting meta.mof file
 
    .EXAMPLE This command will set the Description to 'Test partial description' on the partial, 'Test partial' on the target, 'localhost'
        Set-LcmPartialConfiguration -CimSession localhost -PartialName 'Test partial' -Description 'Test partial description'
 
#>

Function Set-LcmPartialConfiguration
{
    Param
    (
        [Parameter()]
        [psobject]
        $CimSession,

        [Parameter()]
        [string]
        $PartialName,

        [Parameter()]
        [string[]]
        $ConfigurationSource,
        
        [Parameter()]
        [System.Collections.Generic.List[string]]
        $DependsOn,
        
        [Parameter()]
        [string]
        $Description,
        
        [Parameter()]
        [string[]]
        $ExclusiveResources,
        
        [Parameter()]
        [ValidateSet('Disabled','Push','Pull')]
        [string]
        $RefreshMode,
        
        [Parameter()]
        [string[]]
        $ResourceModuleSource,

        [Parameter()]
        [string]
        $OutputPath = "$env:windir\Temp\MofStore",

        [Parameter()]
        [boolean]
        $DeleteMofWhenDone = $true
    )

    #Requires -RunAsAdministrator

    if($CimSession)
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -CimSession $CimSession -ErrorAction Stop
        $computerName = Get-ComputerName -CimSession $CimSession

    }
    else
    {
        $currentLcmConfig = Get-DscLocalConfigurationManager -ErrorAction Stop
        $computerName = $env:COMPUTERNAME
        $CimSession = $env:COMPUTERNAME
    }
    
    $null = Test-OutputPath -Path $OutputPath
    $partialConfiguration = $currentLcmConfig.PartialConfigurations.Where({$($_.ResourceId.Replace('[PartialConfiguration]','')) -eq $PartialName})

    if(-not $partialConfiguration)
    {
        throw "Invalid partial name, $PartialName."
    }
        
    $pendingChanges = $PSBoundParameters.Keys.Where({$script:commonParameters -notcontains $_})
    $configurations = Initialize-SettingsBlock -Configuration $currentLcmConfig
    foreach($partial in $currentLcmConfig.PartialConfigurations)
    {
        if($partial.ResourceId -eq $partialConfiguration.ResourceId)
        {
            $hashChanges = @{}
            foreach($pendingChange in $pendingChanges)
            {
                if($pendingChange -eq 'PartialName')
                {
                    $hashChanges.Add("ResourceId", "[PartialConfiguration]$($PSBoundParameters[$pendingChange])")
                }
                elseif($pendingChange -eq 'DependsOn')
                {
                    $hashChanges.Add($pendingChange, "[PartialConfiguration]$($PSBoundParameters[$pendingChange])")
                }
                else
                {
                    $hashChanges.Add($pendingChange, $PSBoundParameters[$pendingChange])
                }
            }

            $unmodifiedProperties = ($partial | Get-Member -MemberType Property).Name | Where-Object {$hashChanges.Keys -notcontains $_}
            foreach($existingProperty in $unmodifiedProperties)
            {
                if($partial.$existingProperty)
                {
                    $hashChanges.Add($existingProperty, $partial.$existingProperty)
                }
            }
            
            $configurations += Initialize-PartialBlock -Configuration $(New-Object -TypeName PsObject -Property $hashChanges)
        }
        else
        {
            $configurations += Initialize-PartialBlock -Configuration $partial
        }
    }
        
    $null = Invoke-LcmConfig -ComputerName $ComputerName -Configuration $configurations -OutputPath $OutputPath
    Set-DscLocalConfigurationManager -CimSession $CimSession -Path $OutputPath -Force

    if($DeleteMofWhenDone)
    {
        Remove-Item $OutputPath\$computerName.meta.mof -Force -ErrorAction Ignore
    }
}

Function Invoke-LcmConfig
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $ComputerName,

        [Parameter(Mandatory = $true)]
        [string]
        $Configuration,

        [Parameter(Mandatory = $true)]
        [string]
        $OutputPath
    )

    [DSCLocalConfigurationManager()]
    Configuration LcmConfig
    {
        Node $ComputerName
        {
            Invoke-Command -ScriptBlock $ExecutionContext.InvokeCommand.NewScriptBlock($Configuration)
        }
    }

    LcmConfig -OutputPath $OutputPath
}

Function Get-ComputerName
{
    Param
    (
        [Parameter(Mandatory = $true)]
        [psobject]
        $CimSession
    )

    if($CimSession -is [Microsoft.Management.Infrastructure.CimSession])
    {
        return $CimSession.ComputerName
    }
    else
    {
        return $CimSession
    }
}

Function Test-OutputPath
{
    Param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    if(-not (Test-Path $OutputPath))
    {
        New-Item -Path $OutputPath -ItemType Directory
    }
}

Function Initialize-PartialBlock
{
    Param
    (
        [Parameter()]
        [psobject]
        $Configuration
    )

    $PartialConfigurationParameters = @(
        'ConfigurationSource',
        'DependsOn',
        'Description',
        'ExclusiveResources',
        'RefreshMode',
        'ResourceModuleSource'
    )

    $output += "PartialConfiguration $($Configuration.ResourceId.Split("]")[1])`n"
    $output += "{`n"
        
    foreach($partialProperty in $PartialConfigurationParameters)
    {
        if($($Configuration.$partialProperty))
        {
            $output += " $partialProperty = '$($Configuration.$partialProperty -join "','")'`n"
        }
    }
    
    $output += "}`n"

    return $output
}

Function Initialize-SettingsBlock
{
    Param
    (
        [Parameter(Mandatory = $true)]
        [psobject]$Configuration
    )

    $LcmSettingParameters = @(
        'ConfigurationModeFrequencyMins',
        'RebootNodeIfNeeded',
        'ConfigurationMode',
        'ActionAfterReboot',
        'RefreshMode',
        'CertificateId',
        'ConfigurationId',
        'RefreshFrequencyMins',
        'AllowModuleOverwrite',
        'DebugMode',
        'StatusRetentionTimeInDays'
    )
    
    $output = "Settings`n"
    $output += "{`n"

    foreach($property in $LcmSettingParameters)
    {
        if($($Configuration.$property) -eq $true -or $($Configuration.$property) -eq $false)
        {
            $output += " $property = $" + "$($Configuration.$property)`n"
        }
        elseif($Configuration.$property)
        {
            $output += " $property = '$($Configuration.$property)'`n"
        }
    }
    
    $output += "}`n"
    
    return $output
}

$script:commonParameters = [System.Management.Automation.Cmdlet]::CommonParameters + [System.Management.Automation.Cmdlet]::OptionalCommonParameters
$script:commonParameters += 'CimSession', 'OutputPath','DeleteMofWhenDone'

Export-ModuleMember -Function @(
    'Set-LcmSetting',
    'Reset-LcmConfiguration',
    'Remove-LcmPartialConfiguration',
    'Add-LcmPartialConfiguration',
    'Set-LcmPartialConfiguration'
)