176 lines
14 KiB
PowerShell
176 lines
14 KiB
PowerShell
$ErrorActionPreference='Stop'
|
|
$repo=Split-Path -Parent $PSScriptRoot
|
|
. (Join-Path $repo 'scripts\Project.Common.ps1')
|
|
Assert-PowerShell51
|
|
. (Join-Path $repo 'scripts\Configuration.Common.ps1')
|
|
. (Join-Path $repo 'scripts\Builder.Workflow.ps1')
|
|
function Assert-KmsTest { param([bool]$Condition,[string]$Message) if (-not $Condition) { throw $Message } }
|
|
function Assert-KmsFailure {
|
|
param([scriptblock]$Action)
|
|
$failed=$false
|
|
try { & $Action | Out-Null } catch { $failed=$true }
|
|
Assert-KmsTest $failed 'Expected KMS validation failure.'
|
|
}
|
|
foreach ($case in @(
|
|
@{Address='';Host='';Port=1688},
|
|
@{Address='kms-default.cangshui.net';Host='kms-default.cangshui.net';Port=1688},
|
|
@{Address=' kms.example.com:1689 ';Host='kms.example.com';Port=1689},
|
|
@{Address='192.0.2.10';Host='192.0.2.10';Port=1688},
|
|
@{Address='[2001:db8::10]:1689';Host='2001:db8::10';Port=1689},
|
|
@{Address='[2001:db8::10]';Host='2001:db8::10';Port=1688},
|
|
@{Address='2001:db8::10';Host='2001:db8::10';Port=1688}
|
|
)) {
|
|
$endpoint=ConvertTo-PveKmsEndpoint -Address $case.Address
|
|
Assert-KmsTest ($endpoint.Address -ceq $case.Address.Trim() -and $endpoint.Host -ceq $case.Host -and $endpoint.Port -eq $case.Port) 'KMS host or port parsed incorrectly.'
|
|
}
|
|
foreach ($address in @('https://kms.example.com','kms.example.com/path','kms.example.com:0','kms.example.com:65536','kms.example.com:999999999999','kms.example.com:abc','kms host','[kms.example.com]:1688',"kms.example.com`nother",'kms.example.com;whoami')) {
|
|
Assert-KmsFailure { ConvertTo-PveKmsEndpoint -Address $address }
|
|
}
|
|
& {
|
|
$script:kmsAnswers=New-Object 'System.Collections.Generic.Queue[string]'
|
|
function Read-Host {
|
|
param($Prompt)
|
|
if (-not $script:kmsAnswers.Count) { throw 'KMS test input exhausted.' }
|
|
$script:kmsAnswers.Dequeue()
|
|
}
|
|
function Write-Host { param($Object,$ForegroundColor) }
|
|
$script:kmsAnswers.Enqueue('')
|
|
Assert-KmsTest ((Read-BuilderKmsServer).Address -ceq 'kms-default.cangshui.net') 'Default KMS choice changed.'
|
|
foreach ($answer in @('2','')) { $script:kmsAnswers.Enqueue($answer) }
|
|
Assert-KmsTest ((Read-BuilderKmsServer).Address -ceq '') 'Explicit blank was replaced by the default server.'
|
|
foreach ($answer in @('2','https://invalid',' kms.example.com:1689 ')) { $script:kmsAnswers.Enqueue($answer) }
|
|
Assert-KmsTest ((Read-BuilderKmsServer).Address -ceq 'kms.example.com:1689') 'Manual KMS input did not retry or trim.'
|
|
$script:kmsAnswers.Enqueue('Q')
|
|
Assert-KmsFailure { Read-BuilderKmsServer }
|
|
foreach ($answer in @('2','Q')) { $script:kmsAnswers.Enqueue($answer) }
|
|
Assert-KmsFailure { Read-BuilderKmsServer }
|
|
}
|
|
$fixture=Join-Path $repo ('local\kms settings tests '+[guid]::NewGuid().ToString('N').Substring(0,8))
|
|
foreach ($directory in @('scripts','config','resources','local','assets\iso','assets\custom-resources','assets\output','LocalScripts')) { New-Item -ItemType Directory -Path (Join-Path $fixture $directory) -Force | Out-Null }
|
|
foreach ($file in @('New-ImageConfig.ps1','scripts\Project.Common.ps1','scripts\Configuration.Common.ps1','scripts\Package.Validation.ps1','scripts\ProductKeys.Common.ps1','scripts\Software.Selection.ps1','config\Kms.ClientKeys.psd1','config\image.example.ini','resources\Apply-KmsSettings.ps1','resources\Activate-Windows.ps1')) {
|
|
Copy-Item -LiteralPath (Join-Path $repo $file) -Destination (Join-Path $fixture $file)
|
|
}
|
|
[IO.File]::WriteAllText((Join-Path $fixture 'local\settings.json'),(@{ProjectRoot=$fixture;AssetsRoot=(Join-Path $fixture 'assets')}|ConvertTo-Json))
|
|
foreach ($file in @('assets\iso\Windows.iso','assets\iso\virtio-win.iso','assets\custom-resources\virtio-win-guest-tools.exe')) { [IO.File]::WriteAllText((Join-Path $fixture $file),'Fixture; never used to build an image.') }
|
|
$configurations=@()
|
|
foreach ($address in @($null,'kms.example.com:1689','')) {
|
|
$parameters=@{WimPath=(Join-Path $fixture 'assets\iso\Windows.iso');ProductKeyMode='Kms';OutputPath=(Join-Path $fixture ('assets\output\image-'+$configurations.Count+'.qcow2'));ConfigPath=(Join-Path $fixture ('local\image-'+$configurations.Count+'.ini'));SkipChrome=$true;SkipVSCode=$true}
|
|
if ($null -ne $address) { $parameters.KmsServer=$address }
|
|
$result=& (Join-Path $fixture 'New-ImageConfig.ps1') @parameters
|
|
$selection=Get-GuestSoftwareSelection -ResourceRoot $result.StagedResources
|
|
$expected=$address
|
|
if ($null -eq $expected) { $expected='kms-default.cangshui.net' }
|
|
Assert-KmsTest ($selection.KmsServer -ceq $expected -and $result.KmsServer -ceq $expected) 'KMS address did not survive configuration staging.'
|
|
Assert-KmsTest (Test-Path -LiteralPath (Join-Path $result.StagedResources 'Apply-KmsSettings.ps1')) 'Guest KMS script was not staged.'
|
|
Assert-KmsTest (Test-Path -LiteralPath (Join-Path $result.StagedResources 'Activate-Windows.ps1')) 'Guest activation script was not staged.'
|
|
$configurations+=@($result)
|
|
}
|
|
# Preserve old configurations without silently adding a public KMS endpoint.
|
|
$legacy=@{SchemaVersion=1;InstallChrome=$false;InstallVSCode=$false}
|
|
$selectionPath=Join-Path $fixture 'resources\software-selection.json'
|
|
[IO.File]::WriteAllText($selectionPath,($legacy|ConvertTo-Json))
|
|
Assert-KmsTest (-not (Get-GuestSoftwareSelection -ResourceRoot (Join-Path $fixture 'resources')).KmsServer) 'Legacy resources acquired a KMS server.'
|
|
$legacy.KmsServer=$false
|
|
[IO.File]::WriteAllText($selectionPath,($legacy|ConvertTo-Json))
|
|
Assert-KmsFailure { Get-GuestSoftwareSelection -ResourceRoot (Join-Path $fixture 'resources') }
|
|
$legacy.KmsServer='https://invalid'
|
|
[IO.File]::WriteAllText($selectionPath,($legacy|ConvertTo-Json))
|
|
Assert-KmsFailure { Get-GuestSoftwareSelection -ResourceRoot (Join-Path $fixture 'resources') }
|
|
|
|
$guestResources=Join-Path $fixture 'resources'
|
|
$localScripts=Join-Path $fixture 'LocalScripts'
|
|
Copy-Item -LiteralPath (Join-Path $repo 'scripts\Software.Selection.ps1') -Destination (Join-Path $guestResources 'Software.Selection.ps1')
|
|
foreach ($name in @('Apply-GuestNetworkSettings.ps1','Install-ManagedAdministrator.ps1')) { [IO.File]::WriteAllText((Join-Path $guestResources $name),'# Other guest actions are intentionally omitted in this fixture.') }
|
|
$hook=[IO.File]::ReadAllText((Join-Path $repo 'hooks\RunAfterCloudbaseInitInstall.ps1'))
|
|
$hook=$hook.Replace('C:\UnattendResources\CustomResources',$guestResources)
|
|
$hook=$hook.Replace('C:\ProgramData\PveImageBuilder',(Join-Path $fixture 'Diagnostics'))
|
|
$hook=$hook.Replace("Join-Path `$env:ProgramFiles 'Cloudbase Solutions\Cloudbase-Init\LocalScripts'",("'"+$localScripts+"'"))
|
|
$hook=$hook.Replace('exit 0','return 0').Replace('exit 1','return 1')
|
|
$guestHook=[scriptblock]::Create($hook)
|
|
& {
|
|
$global:PveKmsSettingsTest=@{Calls=(New-Object 'System.Collections.Generic.List[object]');ReturnCode=0;ReturnMode='Object';ApplySettings=$true;Host='';Port=0}
|
|
function Write-Host { param($Object,$ForegroundColor) }
|
|
function Test-Path {
|
|
param($LiteralPath,$PathType)
|
|
if ($LiteralPath -eq 'C:\UnattendResources\config.ini') { return $true }
|
|
Microsoft.PowerShell.Management\Test-Path -LiteralPath $LiteralPath
|
|
}
|
|
function Get-Service { param($Name,$ErrorAction) [PSCustomObject]@{Name=$Name} }
|
|
function Set-Service { param($Name,$StartupType) }
|
|
function Get-CimInstance {
|
|
param($ClassName,$ErrorAction)
|
|
Assert-KmsTest ($ClassName -eq 'SoftwareLicensingService') 'Unexpected guest CIM query.'
|
|
[PSCustomObject]@{ClassName=$ClassName;KeyManagementServiceMachine=$global:PveKmsSettingsTest.Host;KeyManagementServicePort=$global:PveKmsSettingsTest.Port}
|
|
}
|
|
function Invoke-CimMethod {
|
|
param($InputObject,$MethodName,$Arguments,$ErrorAction)
|
|
Assert-KmsTest ($MethodName -in @('SetKeyManagementServiceMachine','SetKeyManagementServicePort')) 'An activation or unexpected licensing method was called.'
|
|
$global:PveKmsSettingsTest.Calls.Add([PSCustomObject]@{Method=$MethodName;Arguments=$Arguments})
|
|
if ($global:PveKmsSettingsTest.ApplySettings -and $global:PveKmsSettingsTest.ReturnCode -eq 0) {
|
|
if ($MethodName -eq 'SetKeyManagementServiceMachine') { $global:PveKmsSettingsTest.Host=$Arguments.MachineName }
|
|
else { $global:PveKmsSettingsTest.Port=$Arguments.PortNumber }
|
|
}
|
|
switch ($global:PveKmsSettingsTest.ReturnMode) {
|
|
'Object' { [PSCustomObject]@{ReturnValue=$global:PveKmsSettingsTest.ReturnCode} }
|
|
'Scalar' { [uint32]$global:PveKmsSettingsTest.ReturnCode }
|
|
'Missing' { [PSCustomObject]@{} }
|
|
'NullValue' { [PSCustomObject]@{ReturnValue=$null} }
|
|
'Null' { $null }
|
|
}
|
|
}
|
|
# Blank and legacy choices must not call Windows licensing or create startup scripts.
|
|
foreach ($selection in @(@{SchemaVersion=1;InstallChrome=$false;InstallVSCode=$false;KmsServer=''},@{SchemaVersion=1;InstallChrome=$false;InstallVSCode=$false})) {
|
|
[IO.File]::WriteAllText($selectionPath,($selection|ConvertTo-Json))
|
|
Assert-KmsTest ((& $guestHook) -eq 0 -and $global:PveKmsSettingsTest.Calls.Count -eq 0) 'Blank/legacy choice configured KMS.'
|
|
Assert-KmsTest (-not (Test-Path -LiteralPath (Join-Path $localScripts '20-KmsSettings.ps1'))) 'Blank/legacy choice created a KMS startup script.'
|
|
Assert-KmsTest (-not (Test-Path -LiteralPath (Join-Path $localScripts '30-WindowsActivation.ps1'))) 'Blank/legacy choice created an activation startup script.'
|
|
}
|
|
foreach ($result in $configurations[0..1]) {
|
|
Copy-Item -LiteralPath (Join-Path $result.StagedResources 'software-selection.json') -Destination $selectionPath -Force
|
|
$global:PveKmsSettingsTest.Calls.Clear()
|
|
Assert-KmsTest ((& $guestHook) -eq 0) 'Guest hook failed to configure KMS.'
|
|
Assert-KmsTest ((Get-FileHash -LiteralPath (Join-Path $localScripts '30-WindowsActivation.ps1')).Hash -eq
|
|
(Get-FileHash -LiteralPath (Join-Path $guestResources 'Activate-Windows.ps1')).Hash) 'The first-boot activation script was not copied intact.'
|
|
$endpoint=ConvertTo-PveKmsEndpoint -Address $result.KmsServer
|
|
Assert-KmsTest ($global:PveKmsSettingsTest.Calls.Count -eq 2 -and $global:PveKmsSettingsTest.Calls[0].Arguments.MachineName -eq $endpoint.Host -and $global:PveKmsSettingsTest.Calls[1].Arguments.PortNumber -eq $endpoint.Port) 'Guest licensing received the wrong host or port.'
|
|
$global:PveKmsSettingsTest.Calls.Clear()
|
|
& (Join-Path $localScripts '20-KmsSettings.ps1')
|
|
Assert-KmsTest ($global:PveKmsSettingsTest.Calls.Count -eq 2 -and $global:PveKmsSettingsTest.Calls[0].Arguments.MachineName -eq $endpoint.Host) 'First-boot script did not reapply the configured KMS host.'
|
|
}
|
|
foreach ($returnMode in @('Null','Missing','NullValue','Scalar')) {
|
|
$global:PveKmsSettingsTest.ReturnMode=$returnMode
|
|
$global:PveKmsSettingsTest.Host='';$global:PveKmsSettingsTest.Port=0
|
|
& (Join-Path $localScripts '20-KmsSettings.ps1')
|
|
Assert-KmsTest ($global:PveKmsSettingsTest.Host -eq 'kms.example.com' -and $global:PveKmsSettingsTest.Port -eq 1689) 'A successful licensing method with no ReturnValue was rejected.'
|
|
}
|
|
$global:PveKmsSettingsTest.ReturnMode='Null'
|
|
$global:PveKmsSettingsTest.ApplySettings=$false
|
|
$global:PveKmsSettingsTest.Host='';$global:PveKmsSettingsTest.Port=0
|
|
Assert-KmsFailure { & (Join-Path $localScripts '20-KmsSettings.ps1') }
|
|
$global:PveKmsSettingsTest.ApplySettings=$true
|
|
$global:PveKmsSettingsTest.ReturnMode='Scalar'
|
|
$global:PveKmsSettingsTest.ReturnCode=5
|
|
Assert-KmsFailure { & (Join-Path $localScripts '20-KmsSettings.ps1') }
|
|
$global:PveKmsSettingsTest.ReturnMode='Object'
|
|
Assert-KmsFailure { & (Join-Path $localScripts '20-KmsSettings.ps1') }
|
|
Assert-KmsTest ((& $guestHook 2>$null) -eq 1) 'Guest hook ignored a licensing configuration failure.'
|
|
$failureReport=Get-Content -Raw -LiteralPath (Join-Path $fixture 'Diagnostics\post-cloudbase-init-error.json') | ConvertFrom-Json
|
|
Assert-KmsTest ($failureReport.Stage -eq 'ApplyKmsSettings' -and $failureReport.Message -match 'SetKeyManagementServiceMachine returned 5' -and $failureReport.ScriptStackTrace) 'Guest hook did not preserve the original licensing failure and stage.'
|
|
$global:PveKmsSettingsTest.ReturnCode=0
|
|
[IO.File]::WriteAllText((Join-Path $localScripts 'kms-settings.json'),(@{SchemaVersion=1;Host='https://invalid';Port=1688}|ConvertTo-Json))
|
|
$global:PveKmsSettingsTest.Calls.Clear()
|
|
Assert-KmsFailure { & (Join-Path $localScripts '20-KmsSettings.ps1') }
|
|
Assert-KmsTest ($global:PveKmsSettingsTest.Calls.Count -eq 0) 'Invalid guest settings reached Windows licensing.'
|
|
$activationTarget=Join-Path $localScripts '30-WindowsActivation.ps1'
|
|
$originalActivation=[IO.File]::ReadAllText($activationTarget)
|
|
try {
|
|
[IO.File]::WriteAllText($activationTarget,'# Pre-existing user script must be preserved.')
|
|
Assert-KmsTest ((& $guestHook 2>$null) -eq 1) 'Guest hook overwrote a different activation LocalScript.'
|
|
Assert-KmsTest ([IO.File]::ReadAllText($activationTarget) -eq '# Pre-existing user script must be preserved.') 'A pre-existing activation script was modified.'
|
|
$failureReport=Get-Content -Raw -LiteralPath (Join-Path $fixture 'Diagnostics\post-cloudbase-init-error.json') | ConvertFrom-Json
|
|
Assert-KmsTest ($failureReport.Stage -eq 'StageWindowsActivation' -and $failureReport.Message -match 'refusing to overwrite') 'Activation staging failure was not diagnosed.'
|
|
}
|
|
finally { [IO.File]::WriteAllText($activationTarget,$originalActivation,[Text.UTF8Encoding]::new($false)) }
|
|
}
|
|
[PSCustomObject]@{TwoOptionMenu='passed';DefaultCustomBlank='passed';InputValidation='passed';IndependentConfigurations=$configurations.Count;GuestAndFirstBoot='passed';CimReturnShapes='passed';ReadbackVerification='passed';FailureHandling='passed';LicensingCalls='mocked; no activation request'}
|