Runtime/Invoke-OSDStageWindowsFeatures.ps1
|
[CmdletBinding()] param( [string]$ConfigPath = (Join-Path $PSScriptRoot 'config.json') ) $ErrorActionPreference = 'Stop' function Assert-OSDStageWindowsFeatureConfiguration { param([Parameter(Mandatory)]$Configuration) if ([int]$Configuration.SchemaVersion -ne 2) { throw 'Unsupported Windows Features configuration schema.' } $id = [guid]::Empty if (-not [guid]::TryParse([string]$Configuration.DeploymentId,[ref]$id)) { throw 'Windows Features configuration has no valid DeploymentId.' } if ([string]::IsNullOrWhiteSpace([string]$Configuration.ResultPath)) { throw 'Windows Features configuration has no ResultPath.' } $operations = @($Configuration.Operations) if ($operations.Count -eq 0) { throw 'Windows Features configuration contains no operations.' } $seen = @{} foreach($operation in $operations) { $name = [string]$operation.Name $action = [string]$operation.Action if ($action -notin @('Enable','Disable')) { throw "Unsupported Windows Features action '$action'." } if ([string]::IsNullOrWhiteSpace($name) -or $name.Length -gt 256 -or $name -match '[\x00-\x1F]') { throw 'Windows feature names must be non-empty, at most 256 characters, and contain no control characters.' } $key = $name.ToLowerInvariant() if ($seen.ContainsKey($key)) { throw "Duplicate Windows feature operation '$name'." } $seen[$key] = $true } } function Invoke-OSDStageWindowsFeatureOperation { param([Parameter(Mandatory)]$Operation) $name = [string]$Operation.Name $action = [string]$Operation.Action $before = Get-WindowsOptionalFeature -Online -FeatureName $name -ErrorAction Stop $stateBefore = [string]$before.State $restartNeeded = $false if ($action -eq 'Enable') { if ($stateBefore -in @('Enabled','EnablePending')) { return [pscustomobject]@{ Name=$name;Action=$action;Status='Succeeded';Outcome='AlreadyEnabled' StateBefore=$stateBefore;StateAfter=$stateBefore RestartNeeded=($stateBefore -eq 'EnablePending');Error=$null } } $result = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -ErrorAction Stop if ($result -and $result.PSObject.Properties.Name -contains 'RestartNeeded') { $restartNeeded=[bool]$result.RestartNeeded } } else { if ($stateBefore -in @('Disabled','DisablePending')) { return [pscustomobject]@{ Name=$name;Action=$action;Status='Succeeded';Outcome='AlreadyDisabled' StateBefore=$stateBefore;StateAfter=$stateBefore RestartNeeded=($stateBefore -eq 'DisablePending');Error=$null } } $result = Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart -ErrorAction Stop if ($result -and $result.PSObject.Properties.Name -contains 'RestartNeeded') { $restartNeeded=[bool]$result.RestartNeeded } } $after = Get-WindowsOptionalFeature -Online -FeatureName $name -ErrorAction Stop $stateAfter = [string]$after.State if ($stateAfter -in @('EnablePending','DisablePending')) { $restartNeeded=$true } [pscustomobject]@{ Name=$name;Action=$action;Status='Succeeded' Outcome=if($action -eq 'Enable'){'Enabled'}else{'Disabled'} StateBefore=$stateBefore;StateAfter=$stateAfter RestartNeeded=[bool]$restartNeeded;Error=$null } } try { if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) { throw "Windows Features configuration not found: $ConfigPath" } $configuration = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop Assert-OSDStageWindowsFeatureConfiguration -Configuration $configuration $items = [System.Collections.Generic.List[object]]::new() foreach($operation in @($configuration.Operations)) { try { $items.Add((Invoke-OSDStageWindowsFeatureOperation -Operation $operation)) } catch { $items.Add([pscustomobject]@{ Name=[string]$operation.Name;Action=[string]$operation.Action Status='Failed';Outcome='Failed';StateBefore=$null;StateAfter=$null RestartNeeded=$false;Error=$_.Exception.Message }) } } $failed=@($items | Where-Object Status -eq 'Failed').Count $restart=@($items | Where-Object RestartNeeded).Count -gt 0 $record=[ordered]@{ SchemaVersion=2 DeploymentId=[string]$configuration.DeploymentId Status=if($failed -gt 0){'Failed'}else{'Succeeded'} OperationCount=$items.Count SuccessfulOperationCount=@($items | Where-Object Status -eq 'Succeeded').Count FailedOperationCount=$failed RestartNeeded=[bool]$restart Items=@($items) Error=if($failed -gt 0){"$failed Windows feature operation(s) failed."}else{$null} } $resultPath=[Environment]::ExpandEnvironmentVariables([string]$configuration.ResultPath) New-Item -ItemType Directory -Path (Split-Path $resultPath -Parent) -Force | Out-Null $record | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $resultPath -Encoding UTF8 if ($failed -gt 0) { exit 1 } if ($restart) { exit 3010 } exit 0 } catch { try { if ($configuration -and $configuration.ResultPath) { $resultPath=[Environment]::ExpandEnvironmentVariables([string]$configuration.ResultPath) New-Item -ItemType Directory -Path (Split-Path $resultPath -Parent) -Force | Out-Null [ordered]@{ SchemaVersion=2;DeploymentId=[string]$configuration.DeploymentId;Status='Failed' OperationCount=0;SuccessfulOperationCount=0;FailedOperationCount=0;RestartNeeded=$false Items=@();Error=$_.Exception.Message } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $resultPath -Encoding UTF8 } } catch {} Write-Error $_.Exception.Message exit 1 } |