Private/Get-WinClatConfigurationPlan.ps1
|
Function Get-WinClatConfigurationPlan { <# .SYNOPSIS Reads the current configuration of every computer and plans a global configuration change for each (pass 1). .DESCRIPTION The read pass shared by Set-WinClatConfiguration, Import-WinClatConfiguration, and Reset-WinClatConfiguration (DESIGN.md, sections 7.3, 7.7, and 7.8). It runs the Get-WinClatConfiguration script with store=both on every computer, then on the calling computer: - Returns WinClat.SetResult failure rows, one per requested setting, for computers that are unreachable, lack the feature, or could not be read. Their Status and Message carry the outcome; no error is written (DESIGN.md, section 4.6). - With -OnlyChanged, drops each requested setting whose persistent value already matches; those settings are reported as Unchanged. - Refuses a change that would leave both Pref64FromRA and Pref64FromDns disabled in the active store, with Failed rows. - Returns one WinClat.ConfigurationPlan object per remaining computer with the settings to write, the unchanged settings, the reported settings (-Report), the active and persistent rows, and the ShouldProcess action text, which names policy-controlled settings and warns when AutoEnable or Permit is disabled. Confirmation is left to the public function, because the -AutoEnable Disabled prompt depends on its $ConfirmPreference. WinClat.ConfigurationPlan is internal and is never returned to the caller. .PARAMETER ComputerName The target computers, as supplied by the caller. .PARAMETER Requested The settings requested for every computer, as a hashtable of setting name to [bool]. .PARAMETER RequestByComputer Settings requested per computer, keyed by computer name. A computer listed here uses its own settings instead of -Requested. .PARAMETER OnlyChanged Plan only the settings whose persistent value differs from the request. .PARAMETER Report Settings that are not requested but are reported with their current values, as a hashtable of setting name to the message for their row. Used by Reset-WinClatConfiguration for Permit when -Permit is not given (DESIGN.md, section 7.8). A setting that is also requested is not reported. .PARAMETER Credential Credentials used to connect to remote computers. .PARAMETER ThrottleLimit Maximum number of concurrent remote connections. .PARAMETER Cmdlet The $PSCmdlet of the public function, passed on to Invoke-WinClatRemote. .INPUTS None. This function does not accept pipeline input. .OUTPUTS WinClat.SetResult for computers that cannot be changed, and WinClat.ConfigurationPlan for the others. .EXAMPLE Get-WinClatConfigurationPlan -ComputerName 'CLIENT1' -Requested @{ Permit = $true } -Cmdlet $PSCmdlet Returns the plan for permitting CLAT on CLIENT1. .LINK https://learn.microsoft.com/windows-server/administration/windows-commands/netsh .NOTES Version: 1.0.0 Creation Date: October 28, 2026 Last Updated: October 28, 2026 Author: Richard Hicks Organization: Richard M. Hicks Consulting, Inc. Contact: rich@richardhicks.com Website: https://www.richardhicks.com/ #> [CmdletBinding()] [OutputType('WinClat.SetResult', 'WinClat.ConfigurationPlan')] Param ( # Blank names and comments are dropped by Invoke-WinClatRemote (DESIGN.md, section 4.2). [Parameter(Mandatory)] [AllowEmptyString()] [AllowEmptyCollection()] [string[]]$ComputerName, [hashtable]$Requested = @{}, [hashtable]$RequestByComputer = @{}, [switch]$OnlyChanged, [hashtable]$Report = @{}, [PSCredential]$Credential, [int]$ThrottleLimit = 32, [System.Management.Automation.PSCmdlet]$Cmdlet ) If (-not $Cmdlet) { $Cmdlet = $PSCmdlet } $SettingOrder = @('Permit', 'Pref64FromDns', 'Pref64FromRA', 'AutoEnable') # A computer listed in -RequestByComputer uses its own settings; every other computer uses -Requested. $Requests = @{ Default = $Requested; ByComputer = $RequestByComputer } $GetRequest = { Param ($Name) If ($Requests.ByComputer.ContainsKey($Name)) { Return $Requests.ByComputer[$Name] } Return $Requests.Default } # One row per requested setting for a computer where nothing was changed. $NewFailure = { Param ($Name, $Status, $Message) $Request = & $GetRequest $Name ForEach ($Setting in $SettingOrder) { If ($Request.ContainsKey($Setting)) { ConvertTo-WinClatSetResult -ComputerName $Name -Status $Status -Setting $Setting -RequestedValue $Request[$Setting] -Message $Message } } } $RemoteParameters = @{ ComputerName = $ComputerName ScriptBlock = Get-WinClatConfigurationScript ArgumentList = @('both') ThrottleLimit = $ThrottleLimit NewFailureObject = $NewFailure Cmdlet = $Cmdlet } If ($Credential) { $RemoteParameters['Credential'] = $Credential } # Read every computer. Failures write their errors here. $Current = [ordered]@{} ForEach ($Row in @(Invoke-WinClatRemote @RemoteParameters)) { $Key = [string]$Row.ComputerName If (-not $Current.Contains($Key)) { $Current[$Key] = New-Object -TypeName 'System.Collections.Generic.List[object]' } $Current[$Key].Add($Row) } ForEach ($Key in $Current.Keys) { $Rows = $Current[$Key].ToArray() $Request = & $GetRequest $Key # Unreachable or failed computers already have SetResult rows from the failure builder. $SetRows = @($Rows | Where-Object -FilterScript { $_.PSObject.TypeNames[0] -eq 'WinClat.SetResult' }) If ($SetRows.Count -gt 0) { ForEach ($SetRow in $SetRows) { $SetRow } Continue } $Failure = @($Rows | Where-Object -FilterScript { $_.Status -ne 'Success' })[0] If ($Failure) { & $NewFailure $Key $Failure.Status $Failure.Message Continue } $Active = @($Rows | Where-Object -FilterScript { $_.Store -eq 'Active' })[0] $Persistent = @($Rows | Where-Object -FilterScript { $_.Store -eq 'Persistent' })[0] If (-not $Active -or ($OnlyChanged -and -not $Persistent)) { $Message = 'The current configuration could not be read, so no change was made.' & $NewFailure $Key 'Failed' $Message Continue } # A plain hashtable, so ContainsKey works on the target whether it arrives deserialized or in a local runspace. $Change = @{} $Unchanged = @{} $Reported = @{} ForEach ($Setting in $SettingOrder) { If (-not $Request.ContainsKey($Setting)) { If ($Report.ContainsKey($Setting)) { $Reported[$Setting] = $Report[$Setting] } Continue } If ($OnlyChanged -and $Persistent.$Setting -eq $Request[$Setting]) { $Unchanged[$Setting] = $Request[$Setting] } Else { $Change[$Setting] = $Request[$Setting] } } # Refuse a change that leaves both prefix discovery methods disabled (DESIGN.md, section 7.3). $FromRA = $Active.Pref64FromRA -eq $true $FromDns = $Active.Pref64FromDns -eq $true If ($Change.ContainsKey('Pref64FromRA')) { $FromRA = $Change['Pref64FromRA'] } If ($Change.ContainsKey('Pref64FromDns')) { $FromDns = $Change['Pref64FromDns'] } If ($Change.Count -gt 0 -and -not $FromRA -and -not $FromDns) { $Message = 'The change was refused because it would leave both Pref64FromRA and Pref64FromDns disabled, and CLAT would never activate. Keep at least one prefix discovery method enabled.' & $NewFailure $Key 'Failed' $Message Continue } $Action = $null $DisablesAutoEnable = $Change.ContainsKey('AutoEnable') -and -not $Change['AutoEnable'] $DisablesPermit = $Change.ContainsKey('Permit') -and -not $Change['Permit'] If ($Change.Count -gt 0) { $Changes = ForEach ($Setting in $SettingOrder) { If ($Change.ContainsKey($Setting)) { # The words the caller typed, as in Set-WinClatConfiguration -Permit Enabled (DESIGN.md, section 7.3). If ($Change[$Setting]) { "$Setting = Enabled" } Else { "$Setting = Disabled" } } } $Action = "Set WinCLAT global configuration: $(@($Changes) -join ', ')." If ($DisablesAutoEnable) { $Action += ' Disabling AutoEnable disables CLAT on every interface where it was automatically enabled.' } If ($DisablesPermit) { $Action += ' Disabling Permit disables CLAT on every interface where it was automatically enabled, and no instance can be enabled while it is disabled; a manually enabled instance that is already running keeps translating.' } $Controlled = @(@($Active.PolicyControlled -split ',\s*') | Where-Object -FilterScript { $_ -and $Change.ContainsKey($_) }) If ($Controlled.Count -gt 0) { $Action += " Group Policy controls $($Controlled -join ', '): the local value will be written but does not take effect while the policy applies." } } [PSCustomObject]@{ PSTypeName = 'WinClat.ConfigurationPlan' ComputerName = $Key Change = $Change Unchanged = $Unchanged Reported = $Reported Active = $Active Persistent = $Persistent Action = $Action DisablesAutoEnable = $DisablesAutoEnable DisablesPermit = $DisablesPermit } } } |