fix(template): §7.4 e2e fixes + autologin + validation scripts
Fix emerged during §7.4 e2e test run (2026-05-10): - Deploy: set UsoSvc to Manual explicitly (Server 2025 default = Automatic) - Setup cleanup: re-apply Set-Service Disabled on wuauserv/UsoSvc after DISM StartComponentCleanup (WaaSMedicSvc resets StartType during component store cleanup) - Deploy + Setup: add Administrator autologin (AutoAdminLogon, DefaultUserName, DefaultPassword, DefaultDomainName) in post-install.ps1; Assert-Step 5c validates it - Add Validate-DeployState.ps1: standalone host-side check of all Deploy-set state - Add Validate-SetupState.ps1: standalone host-side check of full post-Setup state - Mark §7.4, §7.1 and §7.2 e2e validation items as complete in TODO.md - Update docs: WINDOWS-TEMPLATE-SETUP.md (arch table, validation scripts section, autologin in confine Deploy/Setup); TEST-7.4-e2e.md checklist marked done Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -632,7 +632,7 @@ File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
|
||||
- [x] Setup Step 4b (UAC) → Assert-Step only
|
||||
- [x] Setup Step 5b (Explorer LaunchTo) → Assert-Step only
|
||||
- [x] Setup Step 5c (Server Manager / DisableCAD / OOBE) → Assert-Step only
|
||||
- [ ] E2e validation — vedi §7.4
|
||||
- [x] E2e validation — completata §7.4 (2026-05-10)
|
||||
|
||||
### 7.2 [P1] WU lifecycle — strategia B (Deploy: GPO + Manual; Setup: lifecycle)
|
||||
File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
|
||||
@@ -647,7 +647,7 @@ avvia WU via SchTask SYSTEM, poi Step 6b disabilita servizi permanentemente post
|
||||
- [x] Setup Step 6 Step A: già rimuove `DisableWindowsUpdateAccess` + `NoAutoUpdate` prima del COM API — nessuna modifica necessaria
|
||||
- [x] Setup Step 6 Step D: già fa `Set-Service start=Manual` + `Start-Service` — idempotente con servizi già Manual
|
||||
- [x] Setup Step 6b: già disabilita wuauserv/UsoSvc + GPO keys — nessuna modifica necessaria
|
||||
- [ ] E2e validation — vedi §7.4
|
||||
- [x] E2e validation — completata §7.4 (2026-05-10)
|
||||
|
||||
### 7.3 [P2] Update header docs di entrambi gli script
|
||||
|
||||
@@ -655,12 +655,18 @@ avvia WU via SchTask SYSTEM, poi Step 6b disabilita servizi permanentemente post
|
||||
- [x] [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1) `.DESCRIPTION`: Steps 1/3/4b/5b/5c marcati "validation-only — set by Deploy"; step ordering rationale aggiornato
|
||||
- [x] [docs/WINDOWS-TEMPLATE-SETUP.md](docs/WINDOWS-TEMPLATE-SETUP.md): architettura "tre script", VMX path `CI-WinBuild.vmx`, Fase C step list + razionale + tabella validazioni aggiornati
|
||||
|
||||
### 7.4 [P2] Test e2e refactor
|
||||
### 7.4 [P2] Test e2e refactor — COMPLETATO 2026-05-10
|
||||
|
||||
- [ ] Deploy stand-alone su VM test → `Get-NetFirewallProfile` tutti `Enabled=False`, `Get-Service wuauserv | Select StartType` → `Manual`, `NoAutoUpdate=1` GPO presente
|
||||
- [ ] Prepare dopo Deploy con `-SkipWindowsUpdate` → tutti `Assert-Step` passano `[OK]` senza eseguire Set (log mostra solo validation output)
|
||||
- [ ] Prepare senza `-SkipWindowsUpdate` → WU gira via SchTask SYSTEM, exit code 0/2/3, Step 6b disabilita wuauserv/UsoSvc (`StartType=Disabled`)
|
||||
- [ ] Final gate del guest passa su tutti e 7 i check
|
||||
- [x] Deploy stand-alone su VM test → `Get-NetFirewallProfile` tutti `Enabled=False`, `Get-Service wuauserv | Select StartType` → `Manual`, `NoAutoUpdate=1` GPO presente
|
||||
- [x] Prepare dopo Deploy con `-SkipWindowsUpdate` → tutti `Assert-Step` passano `[OK]` senza eseguire Set (log mostra solo validation output)
|
||||
- [x] Prepare senza `-SkipWindowsUpdate` → WU (0 update trovati, ResultCode=0), Step 6b disabilita wuauserv/UsoSvc (`StartType=Disabled`)
|
||||
- [x] Final gate del guest passa su tutti e 7 i check; snapshot `BaseClean` preso
|
||||
|
||||
**Fix emersi durante §7.4** (applicati):
|
||||
- Deploy: `UsoSvc` impostato a `Manual` esplicitamente (default Server 2025 = Automatic)
|
||||
- Setup cleanup: `Set-Service -StartupType Disabled` ri-applicato su wuauserv/UsoSvc dopo DISM `StartComponentCleanup` (DISM/WaaSMedicSvc può resettare StartType)
|
||||
- Deploy + Setup: autologin Administrator (`AutoAdminLogon=1`) aggiunto in post-install.ps1 e validato in Assert-Step 5c
|
||||
- Nuovi script: `template/Validate-DeployState.ps1`, `template/Validate-SetupState.ps1`
|
||||
|
||||
### 7.5 [P3] Rinomina Setup → Setup-CITools (opzionale, futuro)
|
||||
Una volta che Setup fa solo build customization, nome `Setup-WinBuild2025` è impreciso —
|
||||
|
||||
+13
-5
@@ -153,9 +153,17 @@ Per promuovere a produzione (rimpiazza template esistente):
|
||||
|
||||
---
|
||||
|
||||
## Checklist §7.4
|
||||
## Checklist §7.4 — COMPLETATA 2026-05-10
|
||||
|
||||
- [ ] Step 1: Deploy exit 0, snapshot `PostInstall` presente
|
||||
- [ ] Step 2: Firewall=False, wuauserv=Manual, NoAutoUpdate=1, MaxMemory≥2048, UAC=0
|
||||
- [ ] Step 3: Prepare `-SkipWindowsUpdate` exit 0, nessun Set in Step 1/3/4b/5b/5c
|
||||
- [ ] Step 4: Prepare senza `-Skip` exit 0, wuauserv=Disabled post-Step 6b
|
||||
- [x] Step 1: Deploy exit 0, snapshot `PostInstall` presente
|
||||
- [x] Step 2: Firewall=False, wuauserv=Manual, NoAutoUpdate=1, MaxMemory≥2048, UAC=0
|
||||
(UsoSvc era Automatic — fix applicato in Deploy; corretto manualmente sulla VM test)
|
||||
- [x] Step 3: Prepare `-SkipWindowsUpdate` exit 0, nessun Set in Step 1/3/4b/5b/5c
|
||||
- [x] Step 4: Prepare senza `-Skip` exit 0 (WU: 0 update, ResultCode=0), wuauserv=Disabled post-Step 6b
|
||||
- [x] Snapshot `BaseClean` preso con successo
|
||||
|
||||
**Fix applicati durante il test**:
|
||||
- Deploy: `UsoSvc` esplicitamente a `Manual` (era omesso, default Server 2025 = Automatic)
|
||||
- Setup cleanup: ri-applica `Disabled` su wuauserv/UsoSvc dopo DISM (WaaSMedicSvc resets StartType)
|
||||
- Deploy + Setup 5c: autologin Administrator (`AutoAdminLogon=1`, `DefaultUserName`, `DefaultPassword`, `DefaultDomainName`)
|
||||
- Nuovi: `Validate-DeployState.ps1`, `Validate-SetupState.ps1`
|
||||
|
||||
@@ -16,9 +16,11 @@ credenziali archiviate, ISO Windows Server presente).
|
||||
| `template/Deploy-WinBuild2025.ps1` | **Host** | Opzionale: crea VM e installa Windows unattended da ISO |
|
||||
| `template/Prepare-WinBuild2025.ps1` | **Host** | Orchestratore: copia + esegue Setup nel guest via WinRM |
|
||||
| `template/Setup-WinBuild2025.ps1` | **Dentro VM** | CI toolchain (user, .NET, Python, VS) + validazione hardening Deploy |
|
||||
| `template/Validate-DeployState.ps1` | **Host** | Valida stato post-Deploy prima di eseguire Prepare |
|
||||
| `template/Validate-SetupState.ps1` | **Host** | Valida stato post-Setup (Deploy + Setup) prima dello snapshot |
|
||||
|
||||
**Confine Deploy / Setup** (refactor §7, 2026-05-10):
|
||||
- `Deploy-WinBuild2025.ps1` si occupa dell'**OS hardening** (Firewall, WinRM, UAC, Defender, UX tweaks, WU GPO locks). Se usato, Setup valida questi come Assert-Step senza re-applicarli.
|
||||
- `Deploy-WinBuild2025.ps1` si occupa dell'**OS hardening** (Firewall, WinRM, UAC, Defender, UX tweaks, autologin Administrator, WU GPO locks). Se usato, Setup valida questi come Assert-Step senza re-applicarli.
|
||||
- `Setup-WinBuild2025.ps1` si occupa della **CI customization** (utente `ci_build`, cartelle `C:\CI`, toolchain .NET/Python/VS, Windows Update lifecycle, cleanup, final gate).
|
||||
|
||||
`Prepare-WinBuild2025.ps1` apre una WinRM session, copia `Setup-WinBuild2025.ps1` in
|
||||
@@ -161,7 +163,7 @@ Step 4b UAC: validation only — Deploy set EnableLUA=0 + TokenFilterPolicy
|
||||
Step 5 CI dirs: C:\CI\{build, output, scripts} ── C:\CI deve esistere prima di WU
|
||||
Step 5b Explorer LaunchTo=1: validation only — Deploy set HKCU + Default hive [Assert-Step]
|
||||
Step 5c CI UX hardening: validation only — Deploy set lock screen, SM policy/task,
|
||||
DisableCAD, OOBE, telemetry [Assert-Step]
|
||||
DisableCAD, OOBE, telemetry, autologin Administrator [Assert-Step]
|
||||
Step 6 Windows Update via Scheduled Task SYSTEM ── clears Deploy GPO locks, runs WU
|
||||
Step 6b Windows Update permanently disabled (post-update lockdown)
|
||||
Step 7 .NET SDK install (channel via dotnet-install.ps1)
|
||||
@@ -196,7 +198,7 @@ Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover
|
||||
| 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
|
||||
| 5 | Operativo | Ogni dir Test-Path -PathType Container |
|
||||
| 5b | Assert | HKCU LaunchTo=1 |
|
||||
| 5c | Assert | NoLockScreen=1, SM policy DoNotOpenAtLogon=1, SM task Disabled, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0 |
|
||||
| 5c | Assert | NoLockScreen=1, SM policy DoNotOpenAtLogon=1, SM task Disabled, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0, AutoAdminLogon=1 + DefaultUserName=Administrator |
|
||||
| 6 | Operativo | ResultCode ∈ {0,2,3}, no reboot required (else exit 3010) |
|
||||
| 6b | Operativo | wuauserv StartType=Disabled, NoAutoUpdate=1, DisableWindowsUpdateAccess=1 |
|
||||
| 7 | Operativo | `dotnet --version` channel match, machine PATH contiene `C:\dotnet` |
|
||||
@@ -220,6 +222,35 @@ Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover
|
||||
|
||||
---
|
||||
|
||||
## Validation scripts standalone
|
||||
|
||||
Due script per validare il guest in punti specifici del flusso, indipendentemente da
|
||||
Prepare. Utili per debug, re-verifica dopo modifiche manuali, e CI automatizzata.
|
||||
|
||||
### Dopo Deploy — prima di Prepare
|
||||
|
||||
```powershell
|
||||
.\template\Validate-DeployState.ps1 -VMIPAddress 192.168.79.129
|
||||
```
|
||||
|
||||
Controlla: Firewall off, Defender GPO, WinRM (Running/Automatic/AllowUnencrypted/Basic/MaxMem),
|
||||
UAC off, Explorer LaunchTo, NoLockScreen, Server Manager (policy+HKLM+task+HKCU),
|
||||
DisableCAD, OOBE, AllowTelemetry, **AutoAdminLogon=1**, WU GPO, wuauserv/UsoSvc=Manual.
|
||||
|
||||
### Dopo Prepare — prima di snapshot
|
||||
|
||||
```powershell
|
||||
.\template\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129
|
||||
```
|
||||
|
||||
Tutti i check Deploy + ci_build (exists/enabled/admin/PasswordNeverExpires),
|
||||
`C:\CI\{build,output,scripts}`, wuauserv/UsoSvc=Disabled, WU GPO,
|
||||
.NET SDK channel match, Python version match, MSBuild present, no leftover installer files.
|
||||
|
||||
Exit 0 = tutto OK. Exit 1 = almeno un check fallito (output indica quale).
|
||||
|
||||
---
|
||||
|
||||
## Fase E — Verifica finale
|
||||
|
||||
Dall'host:
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
via IMAPI2FS COM (built-in Windows, zero deps)
|
||||
Step 4 — Create the VMDK (80 GB single-file thin) via vmware-vdiskmanager
|
||||
Step 5 — Generate the .vmx (UEFI no SecureBoot, NVMe disk, e1000e NIC,
|
||||
three CD-ROMs: Win install + autounattend + VMware Tools)
|
||||
Step 6 — Power on the VM (vmrun start nogui)
|
||||
two CD-ROMs: Win install + autounattend; Tools bundled inside autounattend)
|
||||
Step 6 — Power on the VM (vmrun start gui/nogui — async to avoid Workstation hang)
|
||||
Step 7 — Wait for the install_complete.flag file inside the guest
|
||||
(created by post-install.ps1 once Tools install finishes)
|
||||
Step 8 — Graceful soft-stop, swap NIC e1000e → vmxnet3 in the .vmx
|
||||
Step 8 — Graceful soft-stop, remove CD-ROM controller + USB from VMX, swap NIC e1000e → vmxnet3
|
||||
Step 9 — Power on, wait for guest IP via vmrun (vmxnet3 verified)
|
||||
Step 10 — Take snapshot named $SnapshotName
|
||||
Final — Print summary (VM path, IP, snapshot name)
|
||||
@@ -49,6 +49,9 @@
|
||||
- Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive),
|
||||
DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed;
|
||||
Setup-WinBuild2025 Step 5b/5c validate all of these
|
||||
- Taskbar search bar hidden, Task View button hidden
|
||||
- Windows Terminal set as default terminal (DelegationConsole/Terminal HKCU + Default hive)
|
||||
- Azure Arc auto-start, scheduled tasks, and himds service disabled
|
||||
- VMware Tools installed (Complete) from Tools ISO
|
||||
- install_complete.flag dropped in C:\Windows\Temp
|
||||
|
||||
@@ -56,7 +59,8 @@
|
||||
Path to the Windows Server 2025 install ISO.
|
||||
|
||||
.PARAMETER ToolsISO
|
||||
Path to the VMware Tools ISO (windows.iso).
|
||||
Path to the VMware Tools ISO (windows.iso). Contents are extracted and bundled
|
||||
inside the autounattend ISO at build time (Step 3); not mounted as a separate CD-ROM.
|
||||
|
||||
.PARAMETER VMXPath
|
||||
Full path of the .vmx file to create. The folder is created if missing.
|
||||
@@ -144,7 +148,7 @@ param(
|
||||
# ── Hardware ──
|
||||
[int] $DiskSizeGB = 80,
|
||||
[int] $MemoryMB = 6144,
|
||||
[int] $VCPUCount = 2,
|
||||
[int] $VCPUCount = 4,
|
||||
[int] $CoresPerSocket = 2,
|
||||
|
||||
# ── Locale ──
|
||||
@@ -165,7 +169,14 @@ param(
|
||||
|
||||
# When set, vmrun powers on the VM with the Workstation GUI visible.
|
||||
# Default: headless (nogui) for unattended pipelines.
|
||||
[switch] $ShowGui
|
||||
[switch] $ShowGui,
|
||||
|
||||
# Skip straight to this step number. All derived paths are still computed;
|
||||
# artifacts from skipped steps must already exist on disk.
|
||||
# Steps: 1=preflight 2=render XML 3=autounattend ISO 4=VMDK+noPromptISO
|
||||
# 5=VMX 6=power on 7=wait flag 8=stop+NIC 9=power on vmxnet3 10=snapshot
|
||||
[ValidateRange(1,10)]
|
||||
[int] $StartFromStep = 1
|
||||
)
|
||||
|
||||
$vmrunUiMode = if ($ShowGui) { 'gui' } else { 'nogui' }
|
||||
@@ -178,8 +189,13 @@ $ErrorActionPreference = 'Stop'
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
param([string] $Message, [int] $Step = 0)
|
||||
$skipped = $Step -gt 0 -and $script:StartFromStep -gt $Step
|
||||
if ($skipped) {
|
||||
Write-Host "`n=== $Message === [SKIP]" -ForegroundColor DarkGray
|
||||
} else {
|
||||
Write-Host "`n=== $Message ===" -ForegroundColor Cyan
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-Step {
|
||||
@@ -350,8 +366,14 @@ function New-WinIsoNoPrompt {
|
||||
|
||||
function Invoke-Vmrun {
|
||||
param([Parameter(Mandatory)] [string[]] $Arguments,
|
||||
[switch] $IgnoreErrors)
|
||||
[switch] $IgnoreErrors,
|
||||
[switch] $Async)
|
||||
$vmrun = Join-Path $VMwareWorkstationDir 'vmrun.exe'
|
||||
if ($Async) {
|
||||
# vmrun start can block indefinitely on some Workstation versions; fire async.
|
||||
Start-Process -FilePath $vmrun -ArgumentList $Arguments -NoNewWindow
|
||||
return
|
||||
}
|
||||
$stdout = & $vmrun @Arguments 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
if ($code -ne 0 -and -not $IgnoreErrors) {
|
||||
@@ -377,15 +399,80 @@ function Set-VmxKey {
|
||||
Set-Content -LiteralPath $VmxPath -Value $newLines -Encoding ASCII
|
||||
}
|
||||
|
||||
function Remove-VmxLines {
|
||||
param([Parameter(Mandatory)] [string] $VmxPath,
|
||||
[Parameter(Mandatory)] [string] $KeyPrefix)
|
||||
$pat = "^\s*$([regex]::Escape($KeyPrefix))"
|
||||
$lines = Get-Content -LiteralPath $VmxPath | Where-Object { $_ -notmatch $pat }
|
||||
Set-Content -LiteralPath $VmxPath -Value $lines -Encoding ASCII
|
||||
}
|
||||
|
||||
function Test-VmInList {
|
||||
param([Parameter(Mandatory)] [string] $VmxPath)
|
||||
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
|
||||
$forward = $VmxPath -replace '\\', '/'
|
||||
$back = $VmxPath -replace '/', '\'
|
||||
return ($list -match [regex]::Escape($forward)) -or ($list -match [regex]::Escape($back))
|
||||
}
|
||||
|
||||
function Invoke-VmrunBounded {
|
||||
# Run vmrun synchronously but kill the vmrun process after $TimeoutSeconds.
|
||||
# Useful for 'stop soft': vmrun blocks until guest shuts down, but the ACPI
|
||||
# signal is sent within seconds — killing vmrun doesn't cancel the guest shutdown.
|
||||
param([Parameter(Mandatory)] [string[]] $Arguments,
|
||||
[int] $TimeoutSeconds = 30)
|
||||
$exe = Join-Path $VMwareWorkstationDir 'vmrun.exe'
|
||||
$proc = Start-Process -FilePath $exe -ArgumentList $Arguments -NoNewWindow -PassThru
|
||||
$null = $proc.WaitForExit($TimeoutSeconds * 1000)
|
||||
if (-not $proc.HasExited) { $proc.Kill() }
|
||||
}
|
||||
|
||||
function Stop-Vm {
|
||||
param([Parameter(Mandatory)] [string] $VmxPath,
|
||||
[int] $SoftTimeoutMinutes = 5)
|
||||
# Send soft-stop (ACPI); vmrun may block until guest finishes — kill vmrun after 30 s.
|
||||
# Guest OS continues shutdown independently once the signal is received.
|
||||
Invoke-VmrunBounded -Arguments @('-T','ws','stop',$VmxPath,'soft') -TimeoutSeconds 30
|
||||
$deadline = (Get-Date).AddMinutes($SoftTimeoutMinutes)
|
||||
do {
|
||||
Start-Sleep -Seconds 5
|
||||
} while ((Get-Date) -lt $deadline -and (Test-VmInList -VmxPath $VmxPath))
|
||||
if (Test-VmInList -VmxPath $VmxPath) {
|
||||
Write-Host ' soft stop timed out — hard stop'
|
||||
Invoke-VmrunBounded -Arguments @('-T','ws','stop',$VmxPath,'hard') -TimeoutSeconds 15
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Pre-flight validation
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 1: pre-flight validation'
|
||||
|
||||
# ── Derived paths — always computed regardless of -StartFromStep ─────────────
|
||||
$vmrunPath = Join-Path $VMwareWorkstationDir 'vmrun.exe'
|
||||
$vdiskMgr = Join-Path $VMwareWorkstationDir 'vmware-vdiskmanager.exe'
|
||||
$scriptRoot = Split-Path -Parent $PSCommandPath
|
||||
$xmlTemplate = Join-Path $scriptRoot 'autounattend.template.xml'
|
||||
$vmDir = Split-Path -Parent $VMXPath
|
||||
$vmdkName = [IO.Path]::GetFileNameWithoutExtension($VMXPath) + '.vmdk'
|
||||
$vmdkPath = Join-Path $vmDir $vmdkName
|
||||
$autounattendIso = Join-Path $vmDir 'autounattend.iso'
|
||||
$stagingDir = Join-Path $vmDir '_autounattend_staging'
|
||||
$guestOS = 'windows2019srvnext-64'
|
||||
$guestIP = $null # set in Step 9; pre-init for strict-mode safety
|
||||
if ([string]::IsNullOrWhiteSpace($WinISONoPromptPath)) {
|
||||
$srcDir = Split-Path -Parent $WinISO
|
||||
$srcBase = [IO.Path]::GetFileNameWithoutExtension($WinISO)
|
||||
$WinISONoPromptPath = Join-Path $srcDir "$srcBase.patched.iso"
|
||||
}
|
||||
if ($StartFromStep -gt 1) {
|
||||
Write-Host " [SKIP] Steps 1..$($StartFromStep - 1) — starting from Step $StartFromStep" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: pre-flight validation
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 1: pre-flight validation' -Step 1
|
||||
if ($StartFromStep -le 1) {
|
||||
|
||||
Assert-Step 'PreFlight' "vmrun.exe found at $vmrunPath" { Test-Path $vmrunPath }
|
||||
Assert-Step 'PreFlight' "vmware-vdiskmanager.exe found at $vdiskMgr" { Test-Path $vdiskMgr }
|
||||
@@ -395,26 +482,21 @@ Assert-Step 'PreFlight' "autounattend template exists: $xmlTemplate" { Test-Pat
|
||||
Assert-Step 'PreFlight' "ComputerName valid (1-15 chars, no spaces)" {
|
||||
$ComputerName -match '^[A-Za-z0-9-]{1,15}$'
|
||||
}
|
||||
|
||||
$vmDir = Split-Path -Parent $VMXPath
|
||||
if (-not (Test-Path $vmDir)) {
|
||||
New-Item -ItemType Directory -Path $vmDir -Force | Out-Null
|
||||
}
|
||||
Assert-Step 'PreFlight' "VM directory ready: $vmDir" { Test-Path $vmDir }
|
||||
|
||||
if (Test-Path $VMXPath) {
|
||||
throw "[PreFlight] $VMXPath already exists. Remove the existing VM folder before re-running."
|
||||
}
|
||||
|
||||
$vmdkName = [IO.Path]::GetFileNameWithoutExtension($VMXPath) + '.vmdk'
|
||||
$vmdkPath = Join-Path $vmDir $vmdkName
|
||||
$autounattendIso = Join-Path $vmDir 'autounattend.iso'
|
||||
$stagingDir = Join-Path $vmDir '_autounattend_staging'
|
||||
} # end Step 1
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: render autounattend.xml + post-install.ps1
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 2: render autounattend.xml + post-install.ps1'
|
||||
Write-Step 'Step 2: render autounattend.xml + post-install.ps1' -Step 2
|
||||
if ($StartFromStep -le 2) {
|
||||
|
||||
if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
|
||||
@@ -513,14 +595,15 @@ sc.exe stop DiagTrack | Out-Null
|
||||
L 'Telemetry disabled'
|
||||
|
||||
# ── Windows Update GPO locks — services left Manual (§7.2 Strategy B) ───
|
||||
# Services remain at Windows default (Manual/on-demand). Setup-WinBuild2025
|
||||
# Step 6 invokes the WU COM API via a SYSTEM scheduled task; it clears these
|
||||
# GPO keys first (Step A), applies updates, then permanently disables services
|
||||
# in Step 6b (post-update lockdown). These keys block background auto-start
|
||||
# between Deploy and Setup without breaking the on-demand COM API.
|
||||
# Set both WU services to Manual so they can be invoked on-demand by Setup
|
||||
# Step 6 (WU COM API via SYSTEM task) but won't auto-start between Deploy and Setup.
|
||||
# UsoSvc defaults to Automatic on Windows Server 2025; must be explicit here.
|
||||
# GPO keys below also block background triggers. Step 6b permanently disables both.
|
||||
Set-Service -Name wuauserv -StartupType Manual -ErrorAction SilentlyContinue
|
||||
Set-Service -Name UsoSvc -StartupType Manual -ErrorAction SilentlyContinue
|
||||
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' /v DisableWindowsUpdateAccess /t REG_DWORD /d 1 /f | Out-Null
|
||||
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' /v NoAutoUpdate /t REG_DWORD /d 1 /f | Out-Null
|
||||
L 'Windows Update GPO locks set (services remain Manual; Setup Step 6b permanently disables)'
|
||||
L 'Windows Update GPO locks set + wuauserv/UsoSvc Manual (Setup Step 6b permanently disables)'
|
||||
|
||||
# ── Firewall: all profiles disabled + allow RDP + ICMP + WinRM HTTPS ─────
|
||||
# Disabling all profiles removes any block on subsequent WinRM config and WU
|
||||
@@ -576,9 +659,15 @@ reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManage
|
||||
schtasks /Change /TN '\Microsoft\Windows\Server Manager\ServerManager' /Disable 2>&1 | Out-Null
|
||||
# DisableCAD in Winlogon (belt-and-suspenders with Policies\System key set above)
|
||||
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DisableCAD /t REG_DWORD /d 1 /f | Out-Null
|
||||
# Autologin — Administrator auto-signs in after every boot (CI template convenience).
|
||||
# Password stored plaintext in registry; acceptable for isolated VMnet8 NAT lab VM.
|
||||
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v AutoAdminLogon /t REG_SZ /d 1 /f | Out-Null
|
||||
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultUserName /t REG_SZ /d Administrator /f | Out-Null
|
||||
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultPassword /t REG_SZ /d "$AdminPassword" /f | Out-Null
|
||||
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultDomainName /t REG_SZ /d '.' /f | Out-Null
|
||||
# OOBE non-policy key (supplements the Policies\Windows\OOBE key set above)
|
||||
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null
|
||||
L 'Lock screen, SM GPO policy + task, CAD Winlogon, OOBE non-policy key set'
|
||||
L 'Lock screen, SM GPO policy + task, CAD Winlogon, autologin Administrator, OOBE non-policy key set'
|
||||
|
||||
# ── IE Enhanced Security Configuration off (Admin) ───────────────────────
|
||||
# Long-known CI papercut: ESC blocks every URL in admin sessions.
|
||||
@@ -587,6 +676,13 @@ reg add 'HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37E
|
||||
Stop-Process -Name iexplore -ErrorAction SilentlyContinue
|
||||
L 'IE ESC disabled'
|
||||
|
||||
# ── Remove drive letter from EFI System Partition ────────────────────────
|
||||
# Windows setup sometimes assigns 'S:' to the ESP making it visible in Explorer.
|
||||
Get-Partition | Where-Object { `$_.DriveLetter -eq 'S' } | ForEach-Object {
|
||||
`$_ | Remove-PartitionAccessPath -AccessPath 'S:\' -ErrorAction SilentlyContinue
|
||||
}
|
||||
L 'EFI partition drive letter removed (if any)'
|
||||
|
||||
# ── Hibernate off (frees ~RAM-size on C:, no need on VMs) ────────────────
|
||||
powercfg /h off 2>&1 | Out-Null
|
||||
# Power plan High Performance — never sleep, never throttle CPU
|
||||
@@ -621,11 +717,51 @@ foreach (`$task in @(
|
||||
}
|
||||
L 'CEIP / feedback scheduled tasks disabled'
|
||||
|
||||
# ── Azure Arc auto-start / tray icon off ────────────────────────────────
|
||||
# Server 2025 ships with Azure Arc integration enabled; suppress for CI VMs.
|
||||
foreach (`$arcTask in @(
|
||||
'\Microsoft\Azure Arc\Onboarding\EnableAzureArcSetup',
|
||||
'\Microsoft\Azure Arc\Onboarding\ArcScan',
|
||||
'\Microsoft\AzureArcSeamlessOnboarding\ArcSetup'
|
||||
)) {
|
||||
schtasks /Change /TN `$arcTask /Disable 2>&1 | Out-Null
|
||||
}
|
||||
foreach (`$arcSvc in 'himds','ArcProxyAgent') {
|
||||
`$s = Get-Service -Name `$arcSvc -ErrorAction SilentlyContinue
|
||||
if (`$s) {
|
||||
sc.exe config `$arcSvc start= disabled | Out-Null
|
||||
sc.exe stop `$arcSvc 2>&1 | Out-Null
|
||||
}
|
||||
}
|
||||
reg add 'HKLM\SOFTWARE\Microsoft\AzureArc' /v OnboardingState /t REG_DWORD /d 4 /f 2>&1 | Out-Null
|
||||
reg delete 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' /v AzureArcSetup /f 2>&1 | Out-Null
|
||||
L 'Azure Arc auto-start disabled'
|
||||
|
||||
# ── Explorer UX: show file extensions + open to This PC ──────────────────
|
||||
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v HideFileExt /t REG_DWORD /d 0 /f | Out-Null
|
||||
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v LaunchTo /t REG_DWORD /d 1 /f | Out-Null
|
||||
L 'Explorer UX tweaks applied (HKCU)'
|
||||
|
||||
# ── Taskbar / terminal UX tweaks ─────────────────────────────────────────
|
||||
# SearchboxTaskbarMode=0 hide search bar
|
||||
# ShowTaskViewButton=0 hide Task View button
|
||||
# DelegationConsole/Terminal Windows Terminal as default terminal
|
||||
# LayoutModification.xml pre-pin Windows Terminal for new user profiles
|
||||
|
||||
# HKCU (SYSTEM context at FirstLogon)
|
||||
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Search' /v SearchboxTaskbarMode /t REG_DWORD /d 0 /f | Out-Null
|
||||
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v ShowTaskViewButton /t REG_DWORD /d 0 /f | Out-Null
|
||||
reg add 'HKCU\Console\%Startup' /v DelegationConsole /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
|
||||
reg add 'HKCU\Console\%Startup' /v DelegationTerminal /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
|
||||
# Default hive — Administrator + any future user inherits on first interactive logon
|
||||
reg load HKU\DefaultUser2 'C:\Users\Default\NTUSER.DAT' 2>&1 | Out-Null
|
||||
reg add 'HKU\DefaultUser2\Software\Microsoft\Windows\CurrentVersion\Search' /v SearchboxTaskbarMode /t REG_DWORD /d 0 /f | Out-Null
|
||||
reg add 'HKU\DefaultUser2\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v ShowTaskViewButton /t REG_DWORD /d 0 /f | Out-Null
|
||||
reg add 'HKU\DefaultUser2\Console\%Startup' /v DelegationConsole /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
|
||||
reg add 'HKU\DefaultUser2\Console\%Startup' /v DelegationTerminal /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
|
||||
reg unload HKU\DefaultUser2 2>&1 | Out-Null
|
||||
L 'Taskbar: search hidden, task view hidden, WT default terminal'
|
||||
|
||||
# ── Wait for VMware Tools install to settle (kicked off at top) ──────────
|
||||
# Wrapper returned immediately while msiexec runs async. Poll until:
|
||||
# (a) no setup/setup64/vminst wrapper process (excludes system msiexec /V)
|
||||
@@ -664,9 +800,11 @@ if (`$toolsExe) {
|
||||
New-Item -ItemType File -Path 'C:\Windows\Temp\install_complete.flag' -Force | Out-Null
|
||||
L 'install_complete.flag dropped'
|
||||
|
||||
# ── Reboot to apply Tools/UAC/services state cleanly ──────────────────────
|
||||
L 'rebooting in 15s'
|
||||
shutdown /r /t 15 /f /c "post-install complete"
|
||||
# ── Shut down (not restart) so host Step 8 can swap NIC and snapshot ──────
|
||||
# Delay 60s: host polls every 30s, needs one cycle after flag appears.
|
||||
# Step 8 sends vmrun stop soft on top of this — Windows handles both.
|
||||
L 'shutting down in 60s'
|
||||
shutdown /s /t 60 /f /c "post-install complete"
|
||||
"@
|
||||
$psOut = Join-Path $stagingDir 'post-install.ps1'
|
||||
Set-Content -LiteralPath $psOut -Value $postInstall -Encoding UTF8
|
||||
@@ -690,10 +828,13 @@ Assert-Step 'Render' 'Tools setup bundled in staging\Tools' {
|
||||
(Test-Path (Join-Path $toolsStage 'setup64.exe'))
|
||||
}
|
||||
|
||||
} # end Step 2
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: build autounattend ISO
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 3: build autounattend ISO via IMAPI2FS'
|
||||
Write-Step 'Step 3: build autounattend ISO via IMAPI2FS' -Step 3
|
||||
if ($StartFromStep -le 3) {
|
||||
|
||||
New-IsoFromFolder -SourceFolder $stagingDir -IsoPath $autounattendIso -VolumeLabel 'AUTOUNATTEND'
|
||||
Assert-Step 'IsoBuild' "autounattend.iso created at $autounattendIso" { Test-Path $autounattendIso }
|
||||
@@ -708,10 +849,13 @@ if (Test-Path $stagingDir) {
|
||||
Write-Host " [warn] staging dir not fully cleaned, leaving in place: $stagingDir" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
} # end Step 3
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 4: create VMDK
|
||||
# Step 4: create VMDK + no-prompt Windows ISO
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step "Step 4: create $DiskSizeGB GB VMDK (single-file growable)"
|
||||
Write-Step "Step 4: create $DiskSizeGB GB VMDK (single-file growable)" -Step 4
|
||||
if ($StartFromStep -le 4) {
|
||||
|
||||
# -t 0 = single growable file (thin); -a lsilogic affects descriptor only,
|
||||
# the disk attaches to the NVMe controller defined in the VMX.
|
||||
@@ -723,13 +867,7 @@ Assert-Step 'Vmdk' "VMDK file present: $vmdkPath" { Test-Path $vmdkPath }
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 4b: rebuild Windows ISO with EFI no-prompt boot image
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 4b: build no-prompt Windows ISO (skips "Press any key")'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($WinISONoPromptPath)) {
|
||||
$srcDir = Split-Path -Parent $WinISO
|
||||
$srcBase = [IO.Path]::GetFileNameWithoutExtension($WinISO)
|
||||
$WinISONoPromptPath = Join-Path $srcDir "$srcBase.patched.iso"
|
||||
}
|
||||
Write-Step 'Step 4b: build no-prompt Windows ISO (skips "Press any key")' -Step 4
|
||||
|
||||
$srcMtime = (Get-Item $WinISO).LastWriteTime
|
||||
$rebuild = $true
|
||||
@@ -747,21 +885,24 @@ if ($rebuild) {
|
||||
Assert-Step 'NoPromptIso' "no-prompt ISO present: $WinISONoPromptPath" { Test-Path $WinISONoPromptPath }
|
||||
Assert-Step 'NoPromptIso' 'no-prompt ISO non-empty (>500 MB)' { (Get-Item $WinISONoPromptPath).Length -gt 500MB }
|
||||
|
||||
} # end Step 4
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 5: generate VMX
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 5: generate VMX'
|
||||
Write-Step 'Step 5: generate VMX' -Step 5
|
||||
if ($StartFromStep -le 5) {
|
||||
|
||||
# guestOS:
|
||||
# "windows2019srvnext-64" is the generic forward-compat ID for new Windows
|
||||
# Server releases on Workstation. Workstation 17.6+ accepts it for Server 2025.
|
||||
# If your Workstation version supports the explicit ID, change it here.
|
||||
$guestOS = 'windows2019srvnext-64'
|
||||
# ($guestOS is set unconditionally in the derived-paths block above.)
|
||||
|
||||
# CD-ROM ordering (sata):
|
||||
# sata0:0 = Windows install ISO (bootable) ← EFI picks this on first boot
|
||||
# sata0:1 = autounattend ISO (non-bootable, scanned by Setup)
|
||||
# sata0:2 = VMware Tools ISO (Tools installer source)
|
||||
# sata0:1 = autounattend ISO (non-bootable, scanned by Setup; Tools bundled inside)
|
||||
# Both disconnected in Step 8 after install — snapshot/clone boots clean with no ISOs.
|
||||
$vmxLines = @(
|
||||
'.encoding = "windows-1252"',
|
||||
'config.version = "8"',
|
||||
@@ -804,7 +945,7 @@ $vmxLines = @(
|
||||
"nvme0:0.fileName = `"$vmdkName`"",
|
||||
'nvme0:0.deviceType = "disk"',
|
||||
'',
|
||||
'# Three SATA CD-ROMs: install / autounattend / Tools',
|
||||
'# Two SATA CD-ROMs: install ISO + autounattend ISO (Tools bundled in autounattend)',
|
||||
'sata0.present = "TRUE"',
|
||||
'sata0.pciSlotNumber = "32"',
|
||||
'sata0:0.present = "TRUE"',
|
||||
@@ -815,10 +956,6 @@ $vmxLines = @(
|
||||
'sata0:1.deviceType = "cdrom-image"',
|
||||
"sata0:1.fileName = `"$autounattendIso`"",
|
||||
'sata0:1.startConnected = "TRUE"',
|
||||
'sata0:2.present = "TRUE"',
|
||||
'sata0:2.deviceType = "cdrom-image"',
|
||||
"sata0:2.fileName = `"$ToolsISO`"",
|
||||
'sata0:2.startConnected = "TRUE"',
|
||||
'',
|
||||
'# NIC: e1000e for install (broad compat); switched to vmxnet3 after Tools',
|
||||
'ethernet0.present = "TRUE"',
|
||||
@@ -852,19 +989,23 @@ $vmxLines = @(
|
||||
Set-Content -LiteralPath $VMXPath -Value $vmxLines -Encoding ASCII
|
||||
Assert-Step 'Vmx' "VMX written: $VMXPath" { Test-Path $VMXPath }
|
||||
|
||||
} # end Step 5
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 6: power on VM
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 6: power on VM (vmrun start nogui)'
|
||||
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null
|
||||
Start-Sleep -Seconds 5
|
||||
$running = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
|
||||
Assert-Step 'PowerOn' 'VM appears in vmrun list' { $running -match [regex]::Escape($VMXPath) }
|
||||
Write-Step "Step 6: power on VM (vmrun start $vmrunUiMode)" -Step 6
|
||||
if ($StartFromStep -le 6) {
|
||||
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
|
||||
Start-Sleep -Seconds 10
|
||||
Assert-Step 'PowerOn' 'VM appears in vmrun list' { Test-VmInList -VmxPath $VMXPath }
|
||||
} # end Step 6
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 7: wait for install_complete.flag inside guest
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step "Step 7: wait for guest install_complete.flag (max ${GuestPollMaxMinutes} min)"
|
||||
Write-Step "Step 7: wait for guest install_complete.flag (max ${GuestPollMaxMinutes} min)" -Step 7
|
||||
if ($StartFromStep -le 7) {
|
||||
|
||||
$deadline = (Get-Date).AddMinutes($GuestPollMaxMinutes)
|
||||
$flagPath = 'C:\Windows\Temp\install_complete.flag'
|
||||
@@ -883,22 +1024,24 @@ while ((Get-Date) -lt $deadline) {
|
||||
}
|
||||
Assert-Step 'GuestReady' "install_complete.flag observed in guest" { $flagSeen }
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 8: graceful soft-stop, swap NIC e1000e → vmxnet3
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 8: stop VM softly, swap NIC to vmxnet3'
|
||||
} # end Step 7
|
||||
|
||||
Invoke-Vmrun -Arguments @('-T','ws','stop',$VMXPath,'soft') -IgnoreErrors | Out-Null
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 8: stop VM, disconnect ISOs, swap NIC e1000e → vmxnet3
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 8: stop VM, disconnect ISOs, swap NIC to vmxnet3' -Step 8
|
||||
if ($StartFromStep -le 8) {
|
||||
|
||||
# Wait for VM to drop out of running list (max 3 min).
|
||||
$stopDeadline = (Get-Date).AddMinutes(3)
|
||||
do {
|
||||
Start-Sleep -Seconds 5
|
||||
$still = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
|
||||
} while ((Get-Date) -lt $stopDeadline -and ($still -match [regex]::Escape($VMXPath)))
|
||||
Assert-Step 'Stop' 'VM no longer in vmrun list' {
|
||||
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
|
||||
-not ($list -match [regex]::Escape($VMXPath))
|
||||
Stop-Vm -VmxPath $VMXPath
|
||||
Assert-Step 'Stop' 'VM no longer in vmrun list' { -not (Test-VmInList -VmxPath $VMXPath) }
|
||||
|
||||
# Remove CD-ROM controller and USB from VMX — not needed post-install.
|
||||
foreach ($prefix in 'sata0','usb','ehci','usb_xhci') {
|
||||
Remove-VmxLines -VmxPath $VMXPath -KeyPrefix $prefix
|
||||
}
|
||||
Assert-Step 'CleanVmx' 'CD-ROM and USB entries removed from VMX' {
|
||||
$vmx = Get-Content -LiteralPath $VMXPath -Raw
|
||||
($vmx -notmatch '(?m)^\s*sata0') -and ($vmx -notmatch '(?m)^\s*usb')
|
||||
}
|
||||
|
||||
Set-VmxKey -VmxPath $VMXPath -Key 'ethernet0.virtualDev' -Value 'vmxnet3'
|
||||
@@ -906,12 +1049,16 @@ Assert-Step 'NicSwap' 'ethernet0.virtualDev = vmxnet3' {
|
||||
(Get-Content -LiteralPath $VMXPath) -match '^\s*ethernet0\.virtualDev\s*=\s*"vmxnet3"\s*$'
|
||||
}
|
||||
|
||||
} # end Step 8
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 9: power on, verify vmxnet3 + DHCP IP
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step 'Step 9: power on with vmxnet3, verify DHCP IP'
|
||||
Write-Step 'Step 9: power on with vmxnet3, verify DHCP IP' -Step 9
|
||||
if ($StartFromStep -le 9) {
|
||||
|
||||
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null
|
||||
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
|
||||
Start-Sleep -Seconds 10
|
||||
$ipDeadline = (Get-Date).AddMinutes(10)
|
||||
$guestIP = $null
|
||||
do {
|
||||
@@ -921,27 +1068,23 @@ do {
|
||||
} while ((Get-Date) -lt $ipDeadline -and -not ($guestIP -match '^\d+\.\d+\.\d+\.\d+'))
|
||||
Assert-Step 'IPCheck' 'Guest IP obtained on vmxnet3' { $guestIP -match '^\d+\.\d+\.\d+\.\d+' }
|
||||
|
||||
} # end Step 9
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 10: shutdown → snapshot (cold) → power on
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
Write-Step "Step 10: graceful shutdown, snapshot '$SnapshotName' (cold), power on"
|
||||
Write-Step "Step 10: graceful shutdown, snapshot '$SnapshotName' (cold), power on" -Step 10
|
||||
if ($StartFromStep -le 10) {
|
||||
|
||||
Invoke-Vmrun -Arguments @('-T','ws','stop',$VMXPath,'soft') -IgnoreErrors | Out-Null
|
||||
$stopDeadline = (Get-Date).AddMinutes(3)
|
||||
do {
|
||||
Start-Sleep -Seconds 5
|
||||
$running = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
|
||||
} while ((Get-Date) -lt $stopDeadline -and ($running -match [regex]::Escape($VMXPath)))
|
||||
Assert-Step 'Snapshot' 'VM powered off before snapshot' {
|
||||
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
|
||||
-not ($list -match [regex]::Escape($VMXPath))
|
||||
}
|
||||
Stop-Vm -VmxPath $VMXPath
|
||||
Assert-Step 'Snapshot' 'VM powered off before snapshot' { -not (Test-VmInList -VmxPath $VMXPath) }
|
||||
|
||||
Invoke-Vmrun -Arguments @('-T','ws','snapshot',$VMXPath,$SnapshotName) | Out-Null
|
||||
$snaps = Invoke-Vmrun -Arguments @('-T','ws','listSnapshots',$VMXPath)
|
||||
Assert-Step 'Snapshot' "Snapshot '$SnapshotName' present" { $snaps -match [regex]::Escape($SnapshotName) }
|
||||
|
||||
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null
|
||||
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
|
||||
Start-Sleep -Seconds 10
|
||||
$ipDeadline = (Get-Date).AddMinutes(5)
|
||||
do {
|
||||
Start-Sleep -Seconds 10
|
||||
@@ -949,6 +1092,8 @@ do {
|
||||
} while ((Get-Date) -lt $ipDeadline -and -not ($guestIP -match '^\d+\.\d+\.\d+\.\d+'))
|
||||
Assert-Step 'Snapshot' 'VM back online after snapshot' { $guestIP -match '^\d+\.\d+\.\d+\.\d+' }
|
||||
|
||||
} # end Step 10
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Final summary
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -437,7 +437,17 @@ try {
|
||||
Write-Host "[Prepare] Output from guest:"
|
||||
Write-Host "------------------------------------------------------------"
|
||||
|
||||
$result = $null
|
||||
try {
|
||||
$result = Invoke-GuestSetup -Session $session -ScriptPath $guestScriptPath -ScriptArgs $setupArgs
|
||||
} catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
|
||||
# VM rebooted mid-update (WU triggered OS restart before Setup could return 3010).
|
||||
# Treat as reboot-required: close dead session, fall through to the reboot-wait block.
|
||||
Write-Host "[Prepare] WinRM transport error — VM rebooted mid-update. Treating as reboot-required..."
|
||||
Remove-PSSession $session -ErrorAction SilentlyContinue
|
||||
$session = $null
|
||||
$result = 3010
|
||||
}
|
||||
|
||||
Write-Host "------------------------------------------------------------"
|
||||
Write-Host "[Prepare] Iteration $iter exit code: $result"
|
||||
@@ -448,18 +458,19 @@ try {
|
||||
throw "Setup-WinBuild2025.ps1 exited with code $result"
|
||||
}
|
||||
|
||||
# exit 3010 → WU applied updates needing reboot. Reboot guest, wait,
|
||||
# reopen session, loop. -SkipWindowsUpdate must NOT be set on retry —
|
||||
# we want the next iteration to drain remaining updates.
|
||||
# exit 3010 (or transport error treated as 3010) → WU needs reboot.
|
||||
# Reboot guest if session still alive, wait for WinRM, reopen session, loop.
|
||||
if ($iter -eq $maxWuIterations) {
|
||||
throw "Windows Update did not converge after $maxWuIterations iterations (still returning 3010)"
|
||||
}
|
||||
|
||||
Write-Host "[Prepare] WU applied patches → reboot required. Rebooting guest..."
|
||||
if ($session) {
|
||||
try {
|
||||
Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force } -ErrorAction SilentlyContinue
|
||||
} catch { }
|
||||
Remove-PSSession $session -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 30 # let WinRM tear down before polling
|
||||
Write-Host "[Prepare] Waiting for guest WinRM to come back (max $rebootDeadlineMin min)..."
|
||||
|
||||
@@ -360,6 +360,10 @@ Assert-Step 'CIUX' 'OOBE policy DisablePrivacyExperience=1' {
|
||||
Assert-Step 'CIUX' 'DataCollection AllowTelemetry=0' {
|
||||
(Get-ItemProperty -Path $dcKey -Name AllowTelemetry -ErrorAction Stop).AllowTelemetry -eq 0
|
||||
}
|
||||
Assert-Step 'CIUX' 'autologin Administrator enabled (AutoAdminLogon=1)' {
|
||||
$wl = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -ErrorAction Stop
|
||||
$wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator'
|
||||
}
|
||||
|
||||
# ── Step 6: Windows Update ────────────────────────────────────────────────────
|
||||
# Firewall + Defender already off: WU downloads and installs run without scanning.
|
||||
@@ -919,6 +923,12 @@ Write-Host "DISM exited (code $($dism.ExitCode))."
|
||||
|
||||
Write-Host "Disk cleanup complete."
|
||||
|
||||
# DISM StartComponentCleanup / WaaSMedicSvc may reset service StartType during cleanup.
|
||||
# Re-enforce Disabled on both WU services after DISM completes.
|
||||
foreach ($svc in 'wuauserv', 'UsoSvc') {
|
||||
Set-Service -Name $svc -StartupType Disabled -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Validation — leftover artifacts from this run should be gone
|
||||
# Note: do NOT assert SoftwareDistribution\Download empty.
|
||||
# WaaSMedicSvc (Windows Update Medic Service) is tamper-protected — cannot be stopped —
|
||||
@@ -929,7 +939,7 @@ $sdFiles = Get-ChildItem 'C:\Windows\SoftwareDistribution\Download' -Recurse -Fi
|
||||
$sdMB = [math]::Round(($sdFiles | Measure-Object -Property Length -Sum).Sum / 1MB, 1)
|
||||
Write-Host " SoftwareDistribution\Download: $($sdFiles.Count) file(s), ${sdMB} MB remaining (WaaSMedicSvc best-effort, WU is disabled)."
|
||||
|
||||
Assert-Step 'Cleanup' 'wuauserv StartType still Disabled after cache clear' {
|
||||
Assert-Step 'Cleanup' 'wuauserv StartType Disabled after cache clear' {
|
||||
(Get-Service wuauserv -ErrorAction Stop).StartType -eq 'Disabled'
|
||||
}
|
||||
} # end if (-not $SkipCleanup)
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates that Deploy-WinBuild2025.ps1 has correctly configured a guest VM.
|
||||
|
||||
.DESCRIPTION
|
||||
Connects to the VM via WinRM (HTTP/Basic) and runs all checks that
|
||||
Setup-WinBuild2025.ps1 Steps 1/2/3/4b/5b/5c assert as validation-only.
|
||||
Use this after Deploy completes and before running Prepare/Setup.
|
||||
|
||||
.PARAMETER VMIPAddress
|
||||
IP address of the guest VM on VMnet8 (e.g. 192.168.79.129).
|
||||
|
||||
.PARAMETER AdminUsername
|
||||
Admin account inside the VM (default: Administrator).
|
||||
|
||||
.PARAMETER AdminPassword
|
||||
Password for the admin account. Prompted if omitted.
|
||||
|
||||
.EXAMPLE
|
||||
.\Validate-DeployState.ps1 -VMIPAddress 192.168.79.129
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $VMIPAddress,
|
||||
[string] $AdminUsername = 'Administrator',
|
||||
[string] $AdminPassword
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not $AdminPassword) {
|
||||
$secure = Read-Host "Password for $AdminUsername@$VMIPAddress" -AsSecureString
|
||||
$AdminPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
||||
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
|
||||
}
|
||||
|
||||
$cred = New-Object System.Management.Automation.PSCredential(
|
||||
$AdminUsername,
|
||||
(ConvertTo-SecureString $AdminPassword -AsPlainText -Force)
|
||||
)
|
||||
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
|
||||
|
||||
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
|
||||
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
|
||||
|
||||
try {
|
||||
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
|
||||
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
|
||||
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
|
||||
Set-Item WSMan:\localhost\Client\TrustedHosts -Value ($cur ? "$cur,$VMIPAddress" : $VMIPAddress) -Force
|
||||
}
|
||||
|
||||
Write-Host "`nValidating Deploy state on $VMIPAddress..." -ForegroundColor Cyan
|
||||
|
||||
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
|
||||
-Authentication Basic -SessionOption $so -ScriptBlock {
|
||||
|
||||
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
|
||||
function Chk {
|
||||
param([string]$Name, [scriptblock]$Test)
|
||||
try { $ok = [bool](& $Test); $err = '' }
|
||||
catch { $ok = $false; $err = $_.Exception.Message }
|
||||
$results.Add([PSCustomObject]@{ Name=$Name; Pass=$ok; Err=$err })
|
||||
}
|
||||
|
||||
# ── Firewall ─────────────────────────────────────────────────────────
|
||||
foreach ($p in Get-NetFirewallProfile) {
|
||||
$n = $p.Name; $e = $p.Enabled
|
||||
Chk "Firewall $n disabled" { $e -eq $false }
|
||||
}
|
||||
|
||||
# ── Defender ─────────────────────────────────────────────────────────
|
||||
Chk 'Defender GPO DisableAntiSpyware=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' `
|
||||
-Name DisableAntiSpyware -EA Stop).DisableAntiSpyware -eq 1
|
||||
}
|
||||
|
||||
# ── WinRM ─────────────────────────────────────────────────────────────
|
||||
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
|
||||
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
|
||||
Chk 'WinRM AllowUnencrypted=true' {
|
||||
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true'
|
||||
}
|
||||
Chk 'WinRM Auth/Basic=true' {
|
||||
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true'
|
||||
}
|
||||
Chk 'WinRM MaxMemoryPerShellMB >= 2048' {
|
||||
[int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048
|
||||
}
|
||||
|
||||
# ── UAC ───────────────────────────────────────────────────────────────
|
||||
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||
Chk 'UAC EnableLUA=0' {
|
||||
(Get-ItemProperty $polSys -Name EnableLUA -EA Stop).EnableLUA -eq 0
|
||||
}
|
||||
Chk 'UAC LocalAccountTokenFilterPolicy=1' {
|
||||
(Get-ItemProperty $polSys -Name LocalAccountTokenFilterPolicy -EA Stop).LocalAccountTokenFilterPolicy -eq 1
|
||||
}
|
||||
|
||||
# ── Explorer ──────────────────────────────────────────────────────────
|
||||
Chk 'Explorer LaunchTo=1 (HKCU)' {
|
||||
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' `
|
||||
-Name LaunchTo -EA Stop).LaunchTo -eq 1
|
||||
}
|
||||
|
||||
# ── UX hardening ─────────────────────────────────────────────────────
|
||||
Chk 'NoLockScreen=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' `
|
||||
-Name NoLockScreen -EA Stop).NoLockScreen -eq 1
|
||||
}
|
||||
Chk 'Server Manager policy DoNotOpenAtLogon=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager' `
|
||||
-Name DoNotOpenAtLogon -EA Stop).DoNotOpenAtLogon -eq 1
|
||||
}
|
||||
Chk 'Server Manager HKLM DoNotOpenServerManagerAtLogon=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\ServerManager' `
|
||||
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
|
||||
}
|
||||
Chk 'Server Manager task Disabled' {
|
||||
$t = Get-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' `
|
||||
-TaskName 'ServerManager' -EA SilentlyContinue
|
||||
(-not $t) -or ($t.State -eq 'Disabled')
|
||||
}
|
||||
Chk 'Server Manager HKCU DoNotOpenServerManagerAtLogon=1' {
|
||||
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\ServerManager' `
|
||||
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
|
||||
}
|
||||
Chk 'DisableCAD=1 (Policies\System)' {
|
||||
(Get-ItemProperty $polSys -Name DisableCAD -EA Stop).DisableCAD -eq 1
|
||||
}
|
||||
Chk 'OOBE DisablePrivacyExperience=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE' `
|
||||
-Name DisablePrivacyExperience -EA Stop).DisablePrivacyExperience -eq 1
|
||||
}
|
||||
Chk 'DataCollection AllowTelemetry=0' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' `
|
||||
-Name AllowTelemetry -EA Stop).AllowTelemetry -eq 0
|
||||
}
|
||||
Chk 'AutoAdminLogon=1, DefaultUserName=Administrator' {
|
||||
$wl = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -EA Stop
|
||||
$wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator'
|
||||
}
|
||||
|
||||
# ── Windows Update GPO + services ─────────────────────────────────────
|
||||
Chk 'WU GPO NoAutoUpdate=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' `
|
||||
-Name NoAutoUpdate -EA Stop).NoAutoUpdate -eq 1
|
||||
}
|
||||
Chk 'WU GPO DisableWindowsUpdateAccess=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' `
|
||||
-Name DisableWindowsUpdateAccess -EA Stop).DisableWindowsUpdateAccess -eq 1
|
||||
}
|
||||
Chk 'wuauserv StartType=Manual' { (Get-Service wuauserv -EA Stop).StartType -eq 'Manual' }
|
||||
Chk 'UsoSvc StartType=Manual' { (Get-Service UsoSvc -EA Stop).StartType -eq 'Manual' }
|
||||
|
||||
return $results.ToArray()
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
$failed = 0
|
||||
foreach ($r in $checks) {
|
||||
if ($r.Pass) {
|
||||
Write-Host " [OK] $($r.Name)" -ForegroundColor Green
|
||||
} else {
|
||||
$detail = if ($r.Err) { " ($($r.Err))" } else { '' }
|
||||
Write-Host " [FAIL] $($r.Name)$detail" -ForegroundColor Red
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
if ($failed -eq 0) {
|
||||
Write-Host "All $($checks.Count) Deploy checks passed." -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "$failed / $($checks.Count) Deploy checks FAILED." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
} finally {
|
||||
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue
|
||||
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates full post-Setup state of a template VM (Deploy + Setup).
|
||||
|
||||
.DESCRIPTION
|
||||
Connects to the VM via WinRM (HTTP/Basic) and runs all checks for the
|
||||
final BaseClean snapshot state: Deploy OS hardening + Setup CI toolchain.
|
||||
Run this after Prepare-WinBuild2025.ps1 completes (exit 0) to confirm
|
||||
the VM is ready for snapshot and production use.
|
||||
|
||||
.PARAMETER VMIPAddress
|
||||
IP address of the guest VM on VMnet8 (e.g. 192.168.79.129).
|
||||
|
||||
.PARAMETER AdminUsername
|
||||
Admin account inside the VM (default: Administrator).
|
||||
|
||||
.PARAMETER AdminPassword
|
||||
Password for the admin account. Prompted if omitted.
|
||||
|
||||
.PARAMETER BuildUsername
|
||||
CI build account created by Setup (default: ci_build).
|
||||
|
||||
.PARAMETER DotNetChannel
|
||||
Expected .NET SDK channel prefix (default: 10.0).
|
||||
|
||||
.PARAMETER PythonVersion
|
||||
Expected exact Python version string (default: 3.13.3).
|
||||
|
||||
.EXAMPLE
|
||||
.\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $VMIPAddress,
|
||||
[string] $AdminUsername = 'Administrator',
|
||||
[string] $AdminPassword,
|
||||
[string] $BuildUsername = 'ci_build',
|
||||
[string] $DotNetChannel = '10.0',
|
||||
[string] $PythonVersion = '3.13.3'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not $AdminPassword) {
|
||||
$secure = Read-Host "Password for $AdminUsername@$VMIPAddress" -AsSecureString
|
||||
$AdminPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
||||
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
|
||||
}
|
||||
|
||||
$cred = New-Object System.Management.Automation.PSCredential(
|
||||
$AdminUsername,
|
||||
(ConvertTo-SecureString $AdminPassword -AsPlainText -Force)
|
||||
)
|
||||
$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
|
||||
|
||||
$prevAllowUnenc = (Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value
|
||||
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
|
||||
|
||||
try {
|
||||
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
|
||||
$cur = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
|
||||
if ($cur -ne '*' -and $cur -notmatch [regex]::Escape($VMIPAddress)) {
|
||||
Set-Item WSMan:\localhost\Client\TrustedHosts -Value ($cur ? "$cur,$VMIPAddress" : $VMIPAddress) -Force
|
||||
}
|
||||
|
||||
Write-Host "`nValidating full Setup state on $VMIPAddress..." -ForegroundColor Cyan
|
||||
|
||||
$checks = Invoke-Command -ComputerName $VMIPAddress -Credential $cred `
|
||||
-Authentication Basic -SessionOption $so -ScriptBlock {
|
||||
|
||||
param($BuildUsername, $DotNetChannel, $PythonVersion)
|
||||
|
||||
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
|
||||
function Chk {
|
||||
param([string]$Name, [scriptblock]$Test)
|
||||
try { $ok = [bool](& $Test); $err = '' }
|
||||
catch { $ok = $false; $err = $_.Exception.Message }
|
||||
$results.Add([PSCustomObject]@{ Name=$Name; Pass=$ok; Err=$err })
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# DEPLOY checks (same as Validate-DeployState.ps1)
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Firewall
|
||||
foreach ($p in Get-NetFirewallProfile) {
|
||||
$n = $p.Name; $e = $p.Enabled
|
||||
Chk "Firewall $n disabled" { $e -eq $false }
|
||||
}
|
||||
|
||||
# Defender
|
||||
Chk 'Defender GPO DisableAntiSpyware=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender' `
|
||||
-Name DisableAntiSpyware -EA Stop).DisableAntiSpyware -eq 1
|
||||
}
|
||||
|
||||
# WinRM
|
||||
Chk 'WinRM service Running' { (Get-Service WinRM -EA Stop).Status -eq 'Running' }
|
||||
Chk 'WinRM service Automatic' { (Get-Service WinRM -EA Stop).StartType -eq 'Automatic' }
|
||||
Chk 'WinRM AllowUnencrypted=true' {
|
||||
(Get-Item WSMan:\localhost\Service\AllowUnencrypted -EA Stop).Value -eq 'true'
|
||||
}
|
||||
Chk 'WinRM Auth/Basic=true' {
|
||||
(Get-Item WSMan:\localhost\Service\Auth\Basic -EA Stop).Value -eq 'true'
|
||||
}
|
||||
Chk 'WinRM MaxMemoryPerShellMB >= 2048' {
|
||||
[int](Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB -EA Stop).Value -ge 2048
|
||||
}
|
||||
|
||||
# UAC
|
||||
$polSys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||
Chk 'UAC EnableLUA=0' {
|
||||
(Get-ItemProperty $polSys -Name EnableLUA -EA Stop).EnableLUA -eq 0
|
||||
}
|
||||
Chk 'UAC LocalAccountTokenFilterPolicy=1' {
|
||||
(Get-ItemProperty $polSys -Name LocalAccountTokenFilterPolicy -EA Stop).LocalAccountTokenFilterPolicy -eq 1
|
||||
}
|
||||
|
||||
# Explorer
|
||||
Chk 'Explorer LaunchTo=1 (HKCU)' {
|
||||
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' `
|
||||
-Name LaunchTo -EA Stop).LaunchTo -eq 1
|
||||
}
|
||||
|
||||
# UX hardening
|
||||
Chk 'NoLockScreen=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' `
|
||||
-Name NoLockScreen -EA Stop).NoLockScreen -eq 1
|
||||
}
|
||||
Chk 'Server Manager policy DoNotOpenAtLogon=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManager' `
|
||||
-Name DoNotOpenAtLogon -EA Stop).DoNotOpenAtLogon -eq 1
|
||||
}
|
||||
Chk 'Server Manager HKLM DoNotOpenServerManagerAtLogon=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\ServerManager' `
|
||||
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
|
||||
}
|
||||
Chk 'Server Manager task Disabled' {
|
||||
$t = Get-ScheduledTask -TaskPath '\Microsoft\Windows\Server Manager\' `
|
||||
-TaskName 'ServerManager' -EA SilentlyContinue
|
||||
(-not $t) -or ($t.State -eq 'Disabled')
|
||||
}
|
||||
Chk 'Server Manager HKCU DoNotOpenServerManagerAtLogon=1' {
|
||||
(Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\ServerManager' `
|
||||
-Name DoNotOpenServerManagerAtLogon -EA Stop).DoNotOpenServerManagerAtLogon -eq 1
|
||||
}
|
||||
Chk 'DisableCAD=1 (Policies\System)' {
|
||||
(Get-ItemProperty $polSys -Name DisableCAD -EA Stop).DisableCAD -eq 1
|
||||
}
|
||||
Chk 'OOBE DisablePrivacyExperience=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OOBE' `
|
||||
-Name DisablePrivacyExperience -EA Stop).DisablePrivacyExperience -eq 1
|
||||
}
|
||||
Chk 'DataCollection AllowTelemetry=0' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' `
|
||||
-Name AllowTelemetry -EA Stop).AllowTelemetry -eq 0
|
||||
}
|
||||
Chk 'AutoAdminLogon=1, DefaultUserName=Administrator' {
|
||||
$wl = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -EA Stop
|
||||
$wl.AutoAdminLogon -eq '1' -and $wl.DefaultUserName -eq 'Administrator'
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# SETUP checks (post-Prepare state)
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
|
||||
# CI build user
|
||||
Chk "user $BuildUsername exists" {
|
||||
$null -ne (Get-LocalUser -Name $BuildUsername -EA Stop)
|
||||
}
|
||||
Chk "user $BuildUsername enabled" {
|
||||
(Get-LocalUser -Name $BuildUsername -EA Stop).Enabled
|
||||
}
|
||||
Chk "user $BuildUsername PasswordNeverExpires" {
|
||||
(Get-LocalUser -Name $BuildUsername -EA Stop).PasswordExpires -eq $null
|
||||
}
|
||||
Chk "user $BuildUsername member of Administrators" {
|
||||
[bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUsername)
|
||||
}
|
||||
|
||||
# CI directories
|
||||
foreach ($dir in 'C:\CI\build','C:\CI\output','C:\CI\scripts') {
|
||||
Chk "$dir exists" { Test-Path $dir -PathType Container }
|
||||
}
|
||||
|
||||
# Windows Update permanently disabled
|
||||
Chk 'wuauserv StartType=Disabled' { (Get-Service wuauserv -EA Stop).StartType -eq 'Disabled' }
|
||||
Chk 'UsoSvc StartType=Disabled' { (Get-Service UsoSvc -EA Stop).StartType -eq 'Disabled' }
|
||||
Chk 'WU GPO NoAutoUpdate=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' `
|
||||
-Name NoAutoUpdate -EA Stop).NoAutoUpdate -eq 1
|
||||
}
|
||||
Chk 'WU GPO DisableWindowsUpdateAccess=1' {
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' `
|
||||
-Name DisableWindowsUpdateAccess -EA Stop).DisableWindowsUpdateAccess -eq 1
|
||||
}
|
||||
|
||||
# Toolchain
|
||||
Chk ".NET SDK channel $DotNetChannel" {
|
||||
$v = (& dotnet --version 2>&1) -as [string]
|
||||
$v -and $v.StartsWith($DotNetChannel)
|
||||
}
|
||||
Chk 'Python C:\Python\python.exe present' { Test-Path 'C:\Python\python.exe' }
|
||||
Chk "Python version $PythonVersion" {
|
||||
$v = (& 'C:\Python\python.exe' --version 2>&1) -as [string]
|
||||
$v -and $v.Trim() -eq "Python $PythonVersion"
|
||||
}
|
||||
Chk 'MSBuild.exe present' {
|
||||
$msb = Get-Command msbuild -ErrorAction SilentlyContinue
|
||||
$msb -and (Test-Path $msb.Source)
|
||||
}
|
||||
|
||||
# No leftover installer files in C:\CI root
|
||||
Chk 'no leftover installer scripts in C:\CI' {
|
||||
$leftovers = Get-ChildItem 'C:\CI' -File -EA SilentlyContinue |
|
||||
Where-Object { $_.Name -match 'wu_worker|vs_buildtools|dotnet-install|Setup-WinBuild' }
|
||||
$leftovers.Count -eq 0
|
||||
}
|
||||
|
||||
return $results.ToArray()
|
||||
|
||||
} -ArgumentList $BuildUsername, $DotNetChannel, $PythonVersion
|
||||
|
||||
Write-Host ''
|
||||
$failed = 0
|
||||
$deployCnt = 0
|
||||
$setupCnt = 0
|
||||
$inSetup = $false
|
||||
|
||||
foreach ($r in $checks) {
|
||||
if (-not $inSetup -and $r.Name -match "^user $BuildUsername|^C:\\CI\\|wuauserv Start|UsoSvc Start|WU GPO|\.NET SDK|Python|MSBuild|leftover") {
|
||||
Write-Host "`n --- Setup checks ---" -ForegroundColor DarkCyan
|
||||
$inSetup = $true
|
||||
}
|
||||
if ($r.Pass) {
|
||||
Write-Host " [OK] $($r.Name)" -ForegroundColor Green
|
||||
} else {
|
||||
$detail = if ($r.Err) { " ($($r.Err))" } else { '' }
|
||||
Write-Host " [FAIL] $($r.Name)$detail" -ForegroundColor Red
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
if ($failed -eq 0) {
|
||||
Write-Host "All $($checks.Count) checks passed — VM ready for BaseClean snapshot." -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "$failed / $($checks.Count) checks FAILED." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
} finally {
|
||||
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnenc -Force -EA SilentlyContinue
|
||||
Set-Item WSMan:\localhost\Client\TrustedHosts $prevTrustedHosts -Force -EA SilentlyContinue
|
||||
}
|
||||
Reference in New Issue
Block a user