305 lines
13 KiB
PowerShell
305 lines
13 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$Provider = "hyperv",
|
|
[string]$ManifestPath = "vm\self-test\install.toml",
|
|
[string]$OutputRoot = "dist\vagrant-self-test",
|
|
[string]$VmName = $(if ($env:COVENANT_VM_NAME) { $env:COVENANT_VM_NAME } else { "covenant-setup-windows" }),
|
|
[string]$GuestUsername = $(if ($env:COVENANT_WINRM_USERNAME) { $env:COVENANT_WINRM_USERNAME } else { "vagrant" }),
|
|
[string]$GuestPassword = $(if ($env:COVENANT_WINRM_PASSWORD) { $env:COVENANT_WINRM_PASSWORD } else { "vagrant" }),
|
|
[switch]$SkipBuild,
|
|
[switch]$SkipVmBoot,
|
|
[switch]$SkipViewer,
|
|
[switch]$HaltAfter,
|
|
[switch]$DestroyAfter
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function Assert-Command {
|
|
param([Parameter(Mandatory)][string]$Name)
|
|
|
|
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
|
throw "Required command not found on PATH: $Name"
|
|
}
|
|
}
|
|
|
|
function Resolve-RepoPath {
|
|
param(
|
|
[Parameter(Mandatory)][string]$RepoRoot,
|
|
[Parameter(Mandatory)][string]$Path
|
|
)
|
|
|
|
if ([System.IO.Path]::IsPathRooted($Path)) {
|
|
return $Path
|
|
}
|
|
|
|
return Join-Path $RepoRoot $Path
|
|
}
|
|
|
|
function Convert-ToSingleQuotedPowerShellLiteral {
|
|
param([Parameter(Mandatory)][string]$Value)
|
|
|
|
return "'" + $Value.Replace("'", "''") + "'"
|
|
}
|
|
|
|
function Invoke-Tool {
|
|
param(
|
|
[Parameter(Mandatory)][string]$FilePath,
|
|
[string[]]$Arguments = @()
|
|
)
|
|
|
|
Write-Host "==> $FilePath $($Arguments -join ' ')"
|
|
Remove-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue
|
|
& $FilePath @Arguments
|
|
$exitCodeVar = Get-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue
|
|
$exitCode = if ($exitCodeVar) { [int]$exitCodeVar.Value } elseif ($?) { 0 } else { 1 }
|
|
if ($exitCode -ne 0) {
|
|
throw "Command failed with exit code ${exitCode}: $FilePath $($Arguments -join ' ')"
|
|
}
|
|
}
|
|
|
|
function Invoke-Vagrant {
|
|
param([string[]]$Arguments)
|
|
|
|
Invoke-Tool -FilePath "vagrant" -Arguments $Arguments
|
|
}
|
|
|
|
function Save-GuestTraceBundle {
|
|
param(
|
|
[Parameter(Mandatory)][string]$GuestTracePath,
|
|
[Parameter(Mandatory)][string]$GuestZipPath,
|
|
[Parameter(Mandatory)][string]$LocalTracePath
|
|
)
|
|
|
|
try {
|
|
New-Item -ItemType Directory -Force -Path $LocalTracePath | Out-Null
|
|
$guestTraceLiteral = Convert-ToSingleQuotedPowerShellLiteral -Value $GuestTracePath
|
|
$guestZipLiteral = Convert-ToSingleQuotedPowerShellLiteral -Value $GuestZipPath
|
|
$command = @(
|
|
"`$tracePath = $guestTraceLiteral"
|
|
"`$zipPath = $guestZipLiteral"
|
|
"if (-not (Test-Path -LiteralPath `$tracePath)) { Write-Output '__COVENANT_TRACE_EMPTY__'; exit 0 }"
|
|
"New-Item -ItemType File -Force -Path (Join-Path `$tracePath '.keep') | Out-Null"
|
|
"Remove-Item -LiteralPath `$zipPath -Force -ErrorAction SilentlyContinue"
|
|
"Compress-Archive -Path (Join-Path `$tracePath '*') -DestinationPath `$zipPath -Force"
|
|
"Write-Output '__COVENANT_TRACE_B64_START__'"
|
|
"[Convert]::ToBase64String([IO.File]::ReadAllBytes(`$zipPath))"
|
|
"Write-Output '__COVENANT_TRACE_B64_END__'"
|
|
) -join "; "
|
|
|
|
$output = Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $command)
|
|
$lines = @($output | ForEach-Object { $_.ToString().Trim() })
|
|
if ($lines -contains "__COVENANT_TRACE_EMPTY__") {
|
|
Write-Warning "Guest trace path did not exist: $GuestTracePath"
|
|
return
|
|
}
|
|
|
|
$start = [Array]::IndexOf($lines, "__COVENANT_TRACE_B64_START__")
|
|
$end = [Array]::IndexOf($lines, "__COVENANT_TRACE_B64_END__")
|
|
if ($start -lt 0 -or $end -le $start) {
|
|
Write-Warning "Guest trace bundle markers were not found in WinRM output."
|
|
return
|
|
}
|
|
|
|
$base64 = (($lines[($start + 1)..($end - 1)]) -join "").Trim()
|
|
if (-not $base64) {
|
|
Write-Warning "Guest trace bundle was empty."
|
|
return
|
|
}
|
|
|
|
$zipPath = Join-Path $LocalTracePath "guest-trace.zip"
|
|
[IO.File]::WriteAllBytes($zipPath, [Convert]::FromBase64String($base64))
|
|
Expand-Archive -LiteralPath $zipPath -DestinationPath $LocalTracePath -Force
|
|
Write-Host "Trace bundle: $LocalTracePath"
|
|
}
|
|
catch {
|
|
Write-Warning "Failed to collect guest trace bundle: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
function Open-HyperVViewer {
|
|
param([Parameter(Mandatory)][string]$VmName)
|
|
|
|
$vmConnect = Join-Path $env:SystemRoot "System32\vmconnect.exe"
|
|
if (-not (Test-Path -LiteralPath $vmConnect)) {
|
|
Write-Warning "Hyper-V viewer not found at $vmConnect"
|
|
return
|
|
}
|
|
|
|
Write-Host "==> $vmConnect localhost $VmName"
|
|
Start-Process -FilePath $vmConnect -ArgumentList @("localhost", $VmName) | Out-Null
|
|
}
|
|
|
|
function Invoke-Process {
|
|
param(
|
|
[Parameter(Mandatory)][string]$FilePath,
|
|
[string[]]$Arguments = @()
|
|
)
|
|
|
|
Write-Host "==> $FilePath $($Arguments -join ' ')"
|
|
$process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -Wait -PassThru
|
|
if ($process.ExitCode -ne 0) {
|
|
throw "Command failed with exit code $($process.ExitCode): $FilePath $($Arguments -join ' ')"
|
|
}
|
|
}
|
|
|
|
function Assert-PackagedInstallerBundle {
|
|
param([Parameter(Mandatory)][string]$InstallerPath)
|
|
|
|
$magic = [Text.Encoding]::ASCII.GetBytes("COVENANT_SETUP_BUNDLE_V1")
|
|
$stream = [IO.File]::Open(
|
|
$InstallerPath,
|
|
[IO.FileMode]::Open,
|
|
[IO.FileAccess]::Read,
|
|
[IO.FileShare]::ReadWrite)
|
|
try {
|
|
if ($stream.Length -lt $magic.Length) {
|
|
throw "Packaged installer is too small to contain the embedded bundle marker: $InstallerPath"
|
|
}
|
|
|
|
$null = $stream.Seek(-1 * $magic.Length, [IO.SeekOrigin]::End)
|
|
$actual = [byte[]]::new($magic.Length)
|
|
$read = $stream.Read($actual, 0, $actual.Length)
|
|
if ($read -ne $magic.Length) {
|
|
throw "Could not read embedded bundle marker from packaged installer: $InstallerPath"
|
|
}
|
|
|
|
for ($i = 0; $i -lt $magic.Length; $i++) {
|
|
if ($actual[$i] -ne $magic[$i]) {
|
|
throw "Packaged installer is missing the embedded bundle marker: $InstallerPath"
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
|
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
$releaseExe = Join-Path $repoRoot "target\release\covenant-setup.exe"
|
|
$payloadRoot = Join-Path $repoRoot "vm\self-test\payload"
|
|
$stagedPayload = Join-Path $payloadRoot "covenant-setup.exe"
|
|
$manifestPathAbs = Resolve-RepoPath -RepoRoot $repoRoot -Path $ManifestPath
|
|
$outputRootAbs = Resolve-RepoPath -RepoRoot $repoRoot -Path $OutputRoot
|
|
$installerPath = Join-Path $outputRootAbs "covenant-setup-installer.exe"
|
|
$resultPath = Join-Path $outputRootAbs "guest-result.json"
|
|
$traceRootAbs = Join-Path $outputRootAbs "trace"
|
|
$guestRoot = "C:\Users\vagrant\AppData\Local\Temp\covenant-setup-smoke"
|
|
$guestInstallerPath = Join-Path $guestRoot "covenant-setup-installer.exe"
|
|
$guestResultPath = Join-Path $guestRoot "guest-result.json"
|
|
$guestTraceRoot = Join-Path $guestRoot "trace"
|
|
$guestTraceZipPath = Join-Path $guestRoot "trace.zip"
|
|
$guestScriptRoot = Join-Path $guestRoot "scripts"
|
|
$guestCommand = "& '$guestScriptRoot\Start-InteractiveSelfInstall.ps1' -InstallerPath '$guestInstallerPath' -ResultPath '$guestResultPath' -ScriptRoot '$guestScriptRoot' -TracePath '$guestTraceRoot'"
|
|
|
|
Assert-Command -Name "cargo"
|
|
Assert-Command -Name "dotnet"
|
|
Assert-Command -Name "vagrant"
|
|
|
|
New-Item -ItemType Directory -Force -Path $payloadRoot | Out-Null
|
|
New-Item -ItemType Directory -Force -Path $outputRootAbs | Out-Null
|
|
Remove-Item -LiteralPath $resultPath -Force -ErrorAction SilentlyContinue
|
|
Remove-Item -LiteralPath $traceRootAbs -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
try {
|
|
if (-not $SkipBuild) {
|
|
Invoke-Tool -FilePath "cargo" -Arguments @("build", "--release")
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $releaseExe)) {
|
|
throw "Release binary not found at $releaseExe"
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $manifestPathAbs)) {
|
|
throw "Self-test manifest not found at $manifestPathAbs"
|
|
}
|
|
|
|
Copy-Item -LiteralPath $releaseExe -Destination $stagedPayload -Force
|
|
Invoke-Process -FilePath $releaseExe -Arguments @("package", $manifestPathAbs, "--output", $outputRootAbs)
|
|
|
|
if (-not (Test-Path -LiteralPath $installerPath)) {
|
|
throw "Packaged installer not found at $installerPath"
|
|
}
|
|
Assert-PackagedInstallerBundle -InstallerPath $installerPath
|
|
|
|
if (-not $SkipVmBoot) {
|
|
Invoke-Vagrant -Arguments @("up", "--provider", $Provider)
|
|
|
|
$guestUsernameLiteral = Convert-ToSingleQuotedPowerShellLiteral -Value $GuestUsername
|
|
$guestPasswordLiteral = Convert-ToSingleQuotedPowerShellLiteral -Value $GuestPassword
|
|
$enableAutoLogonCommand = @(
|
|
'$winlogon = ''HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'''
|
|
"New-ItemProperty -Path `$winlogon -Name 'AutoAdminLogon' -PropertyType String -Value '1' -Force | Out-Null"
|
|
"New-ItemProperty -Path `$winlogon -Name 'ForceAutoLogon' -PropertyType String -Value '1' -Force | Out-Null"
|
|
"New-ItemProperty -Path `$winlogon -Name 'DefaultUserName' -PropertyType String -Value $guestUsernameLiteral -Force | Out-Null"
|
|
"New-ItemProperty -Path `$winlogon -Name 'DefaultPassword' -PropertyType String -Value $guestPasswordLiteral -Force | Out-Null"
|
|
"New-ItemProperty -Path `$winlogon -Name 'DefaultDomainName' -PropertyType String -Value `$env:COMPUTERNAME -Force | Out-Null"
|
|
) -join "; "
|
|
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $enableAutoLogonCommand)
|
|
Invoke-Vagrant -Arguments @("reload")
|
|
}
|
|
|
|
if ($Provider -ieq "hyperv" -and -not $SkipViewer) {
|
|
Open-HyperVViewer -VmName $VmName
|
|
}
|
|
|
|
$waitForShellCommand = "for (`$i = 0; `$i -lt 90; `$i++) { if (Get-Process -Name explorer -ErrorAction SilentlyContinue) { exit 0 }; Start-Sleep -Seconds 2 }; Write-Error 'Explorer shell did not start in time.'; exit 1"
|
|
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $waitForShellCommand)
|
|
|
|
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", "New-Item -ItemType Directory -Force -Path '$guestRoot' | Out-Null; New-Item -ItemType Directory -Force -Path '$guestScriptRoot' | Out-Null")
|
|
$clearGuestTraceCommand = @(
|
|
"try {"
|
|
"if (Test-Path -LiteralPath '$guestTraceRoot') { Remove-Item -LiteralPath '$guestTraceRoot' -Recurse -Force -ErrorAction SilentlyContinue }"
|
|
"if (Test-Path -LiteralPath '$guestTraceZipPath') { Remove-Item -LiteralPath '$guestTraceZipPath' -Force -ErrorAction SilentlyContinue }"
|
|
"} catch { Write-Warning `$_.Exception.Message }"
|
|
"exit 0"
|
|
) -join "; "
|
|
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $clearGuestTraceCommand)
|
|
Invoke-Vagrant -Arguments @("upload", $installerPath, $guestInstallerPath)
|
|
Invoke-Vagrant -Arguments @("upload", (Join-Path $repoRoot "scripts\windows-vm\Invoke-InteractiveInstaller.ps1"), (Join-Path $guestScriptRoot "Invoke-InteractiveInstaller.ps1"))
|
|
Invoke-Vagrant -Arguments @("upload", (Join-Path $repoRoot "scripts\windows-vm\Start-InteractiveSelfInstall.ps1"), (Join-Path $guestScriptRoot "Start-InteractiveSelfInstall.ps1"))
|
|
|
|
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $guestCommand)
|
|
|
|
$resultJson = Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", "Get-Content -LiteralPath '$guestResultPath' -Raw")
|
|
Set-Content -LiteralPath $resultPath -Value $resultJson -Encoding UTF8
|
|
|
|
if (-not (Test-Path -LiteralPath $resultPath)) {
|
|
throw "Guest smoke-test result file was not written: $resultPath"
|
|
}
|
|
|
|
$result = $resultJson | ConvertFrom-Json
|
|
if (-not $result.success) {
|
|
throw "Guest reported a failed smoke test: $($result.error)"
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Smoke test passed."
|
|
Write-Host "Installer: $installerPath"
|
|
Write-Host "Result JSON: $resultPath"
|
|
Write-Host "Trace: $traceRootAbs"
|
|
Write-Host "InstallRoot: $($result.installRoot) (installed, then removed)"
|
|
Write-Host "Uninstall: exit $($result.uninstallExitCode), verified=$($result.uninstallVerified)"
|
|
}
|
|
finally {
|
|
Save-GuestTraceBundle -GuestTracePath $guestTraceRoot -GuestZipPath $guestTraceZipPath -LocalTracePath $traceRootAbs
|
|
|
|
if ($DestroyAfter) {
|
|
try {
|
|
Invoke-Vagrant -Arguments @("destroy", "-f")
|
|
}
|
|
catch {
|
|
Write-Warning "Failed to destroy VM after smoke test: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
elseif ($HaltAfter) {
|
|
try {
|
|
Invoke-Vagrant -Arguments @("halt")
|
|
}
|
|
catch {
|
|
Write-Warning "Failed to halt VM after smoke test: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
}
|