# Windows Template VM Setup Procedura di provisioning della template VM **Windows Server 2025** che alimenta i linked clone usati per ogni job CI. Il template viene creato **una volta**; lo snapshot `BaseClean` è la base immutabile per tutte le build successive. **Prerequisito**: aver eseguito [HOST-SETUP.md](HOST-SETUP.md) (`F:\CI\` esiste, credenziali archiviate, ISO Windows Server presente). --- ## Architettura: tre script | Script | Dove gira | Scopo | | ---------------------------------------- | --------------- | ------------------------------------------------------------------- | | `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, 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 `C:\CI\`, lo esegue, raccoglie l'exit code, valida lo stato del guest. --- ## Specifiche VM | Parametro | Valore | | ----------------------- | --------------------------------------------------------------- | | OS | Windows Server 2025 Standard/Datacenter (Core o Desktop OK) | | vCPU | 4 | | RAM | 6144 MB (6 GB) | | Disco | 80 GB thin provisioned | | NIC | **VMnet8 (NAT)** — internet richiesto per WU + installer | | VMX path | `F:\CI\Templates\WinBuild\CI-WinBuild.vmx` | | ISO | Windows Server 2025 (da `F:\CI\ISO\`) | --- ## Fase A — Crea la VM e installa Windows ### A.1 Crea VM in VMware Workstation - File → New Virtual Machine → Custom - Hardware: 4 vCPU, 6144 MB, 80 GB thin - Network: **VMnet8 (NAT)** - VMX: `F:\CI\Templates\WinBuild\CI-WinBuild.vmx` - CD/DVD: monta ISO Windows Server 2025 ### A.2 Installa Windows Server 2025 - Edition: Standard o Datacenter (Server Core o con Desktop) - Activation: vedi A.4 (deve essere fatto prima dello snapshot) ### A.3 Abilita WinRM dentro la VM Dentro la VM, come Administrator: ```cmd winrm quickconfig -q ``` ```powershell Enable-PSRemoting -Force -SkipNetworkProfileCheck Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force Set-Item WSMan:\localhost\Service\AllowUnencrypted $true -Force Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force ``` ### A.4 Attiva Windows Server 2025 **IMPORTANTE**: deve essere fatto **prima** dello snapshot. I linked clone ereditano l'attivazione. ```cmd slmgr /dli ``` Se non attivato, scegli il metodo: - **KMS** (server in rete): `slmgr /skms ` → `slmgr /ato` - **MAK/Retail key**: `slmgr /ipk XXXXX-XXXXX-XXXXX-XXXXX-XXXXX` → `slmgr /ato` - **KMS pubblico** (lab): vedi https://github.com/robin113x/Windows-Server-Activation/ Verifica: `slmgr /xpr` — deve mostrare attivazione permanente o scadenza lontana. ### A.5 Annota IP DHCP Dentro la VM: ```cmd ipconfig ``` Annota l'IP `192.168.79.x` assegnato dal DHCP di VMnet8. --- ## Fase B — Provisioning automatico dall'host Dall'host (PowerShell elevato): ```powershell cd N:\Code\Workspace\Local-CI-CD-System\template # Provisioning completo + registrazione credenziali automatica: .\Prepare-WinBuild2025.ps1 -VMIPAddress 192.168.79.128 -StoreCredential # Se WU già applicato in precedenza: .\Prepare-WinBuild2025.ps1 -VMIPAddress 192.168.79.128 -SkipWindowsUpdate -StoreCredential ``` ### Cosa fa `Prepare-WinBuild2025.ps1` — passo per passo Ogni step ha validazione `Assert-Step` (throw on failure, `[OK]` su pass): 1. **Pre-flight**: script dir presente, `Setup-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5985 reachable 2. **Configura host WinRM client**: `AllowUnencrypted=true`, aggiunge `$VMIPAddress` ai `TrustedHosts` - Salva valori precedenti, ripristinati nel `finally` (host posture invariata) 3. **Validazione host WinRM**: `AllowUnencrypted=true`, `TrustedHosts` include IP 4. **Test connettività**: `Test-WSMan -Authentication Basic` con credenziali admin 5. **Apre PSSession** verso la VM 6. **Crea `C:\CI`** in guest, valida creazione 7. **Copia `Setup-WinBuild2025.ps1`** in `C:\CI\Setup-WinBuild2025.ps1`, valida size match 8. **Esegue `Setup-WinBuild2025.ps1`** dentro la VM (vedi Fase C) 9. **Gestisce reboot 3010**: se WU richiede reboot, riavvia VM e suggerisce re-run con `-SkipWindowsUpdate` 10. **Post-setup remote validation** (9 check via singola `Invoke-Command`): - WinRM Running - User `$BuildUsername` exists + admin - UAC `EnableLUA=0` - Firewall tutti profili Off - .NET SDK installato - Python `C:\Python\python.exe` presente - MSBuild path presente - `C:\CI\{build,output,scripts}` presenti 11. **Restore host WinRM** nel `finally` (sempre eseguito, anche su failure) ### Parametri principali | Parametro | Default | Note | | --------------------- | -------------- | ----------------------------------------------------------------------------- | | `-VMIPAddress` | (mandatory) | IP DHCP della VM su VMnet8 | | `-AdminUsername` | Administrator | Account admin built-in nella VM | | `-AdminPassword` | (prompt) | Se vuoto, prompt SecureString | | `-BuildPassword` | (prompt) | Password per `ci_build` — mai hard-codare | | `-BuildUsername` | ci_build | Account CI dentro la VM | | `-DotNetSdkVersion` | 10.0 | Channel .NET SDK | | `-SkipWindowsUpdate` | (off) | Pass-through a Setup-WinBuild2025.ps1 | | `-StoreCredential` | (off) | Registra `BuildUsername`/`BuildPassword` in Windows Credential Manager | | `-CredentialTarget` | BuildVMGuest | Target Credential Manager (deve corrispondere a quanto usato da Invoke-CIJob) | --- ## Fase C — Cosa fa `Setup-WinBuild2025.ps1` (dentro la VM) Eseguito automaticamente da Prepare in Fase B. Steps 1/2/3/4b/5b/5c sono **validation-only** — verificano che Deploy abbia già applicato l'hardening OS. Steps 4/5/6/6b/7-10 eseguono la CI customization effettiva. Ogni step ha validazione `Assert-Step` (throw on failure). ``` Step 1 Firewall: validation only — Deploy disabled all profiles [Assert-Step] Step 2 Defender: validation only — Deploy set DisableAntiSpyware GPO + RTP off [Assert-Step] Step 3 WinRM: validation only — Deploy set Basic/Unencrypted/MaxMem/Timeout [Assert-Step] Step 4 Build user account (ci_build, admin, no expire) ── crea utente CI Step 4b UAC: validation only — Deploy set EnableLUA=0 + TokenFilterPolicy [Assert-Step] 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, 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) Step 8 Python install (3.13.3, all-users, C:\Python) Step 9 VS Build Tools 2026 via Scheduled Task SYSTEM Step 10 Toolchain cross-check (dotnet, python, msbuild) Cleanup cleanmgr /sagerun:1 + DISM /StartComponentCleanup /ResetBase Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover ``` ### Razionale dell'ordine - **Steps 1-3 (validazioni)** — verificano prima le precondizioni Deploy (firewall off, Defender off, WinRM pronto) in modo che i failure emergano subito, non a metà toolchain. - **User + UAC (4/4b)** — crea `ci_build`; valida UAC off (set da Deploy); UAC off prima di dir/tool evita prompt di elevazione negli step 7-9. - **CI dirs prima di WU (Step 5)** — il worker WU scrive file temp in `C:\CI\` (`wu_worker.ps1`, `wu_result.txt`, ecc.); la directory deve esistere prima. - **UI tweaks (5b/5c)** — validazioni registro rapide; raggruppate prima di WU (lento). - **WU (Step 6)** — clears i GPO lock di Deploy (Step A), avvia COM API via SYSTEM task, poi Step 6b re-disabilita tutto → snapshot cattura WU permanentemente off. - **Toolchain (7-9) last** — WU completato, nessun scan su payload installer. ### Validazioni per step (sintesi) | Step | Natura | Check chiave | | ---- | ---------- | ------------------------------------------------------------------------------ | | 1 | Assert | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` | | 2 | Assert | GPO DisableAntiSpyware=1 (set da Deploy) | | 3 | Assert | WinRM Running+Automatic, AllowUnencrypted=true, Auth/Basic=true, MaxMem≥2048 | | 4 | Operativo | User exists, enabled, PasswordNeverExpires, member of Administrators | | 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 | | 5 | Operativo | Ogni dir Test-Path -PathType Container | | 5b | Assert | HKCU LaunchTo=1 | | 5c | Assert+Op | NoLockScreen=1, SM policy DoNotOpenAtLogon=1, SM task Disabled, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0; AutoAdminLogon=1+DefaultUserName=Administrator re-affermati se assenti (Windows OOBE può resettarli al primo boot) | | 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` | | 8 | Operativo | `C:\Python\python.exe` esiste, version match esatto, PATH contiene `C:\Python` | | 9 | Operativo | MSBuild.exe esiste, esegue, PATH, `\VC\` dir presente | | 10 | Operativo | dotnet+python+msbuild tutti risolvibili (throw) | | Final| Cross-cut | WinRM, firewall, user+admin, UAC, WU disabled, tutti tool, no leftover installer in `C:\CI` | --- ## Fase D — Snapshot **Solo se `Prepare-WinBuild2025.ps1` exit 0** (tutti gli `Assert-Step` passati): 1. **Spegni la VM**: Start → Shut down (graceful, non hard-stop) 2. **Take snapshot** in VMware Workstation: - VM → Snapshot → Take Snapshot - **Nome esatto: `BaseClean`** (case-sensitive, usato da `New-BuildVM.ps1`) 3. **Lascia la VM spenta** — non modificarla mai più dopo lo snapshot 4. **Verifica `runner/config.yaml`**: `GITEA_CI_TEMPLATE_PATH=F:\CI\Templates\WinBuild\CI-WinBuild.vmx` --- ## 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: ```powershell # Test creazione clone manuale cd N:\Code\Workspace\Local-CI-CD-System\scripts .\New-BuildVM.ps1 -JobId 'smoke-test' # Verifica WinRM raggiungibile sul clone .\Wait-VMReady.ps1 -JobId 'smoke-test' -TimeoutSec 120 # Cleanup .\Remove-BuildVM.ps1 -JobId 'smoke-test' ``` Se i 3 step exit 0 → template pronto per produzione. --- ## Maintenance — refresh semestrale Il template va rinfrescato ogni ~180 giorni (KMS lease) o quando serve aggiornare la toolchain (.NET, VS Build Tools, Python). **Procedura**: 1. Power on della template VM (NIC: VMnet8 NAT) 2. Riattiva: `slmgr /ato` 3. Re-run `Prepare-WinBuild2025.ps1` per applicare update toolchain (passare flag opportuni) 4. Shut down + nuovo snapshot **versionato**: `BaseClean_` (TODO §2.5) 5. Aggiorna `GITEA_CI_SNAPSHOT_NAME` env var in `runner/config.yaml` 6. Tieni i 2 snapshot più recenti, cancella vecchi dopo 1 settimana di uso pulito --- ## Troubleshooting | Sintomo | Diagnosi / Fix | | ------------------------------------------- | ----------------------------------------------------------------------- | | `Cannot reach $VMIPAddress via WinRM` | VM non avviata, WinRM non abilitato, IP errato — verifica con ipconfig | | `Setup-WinBuild2025.ps1 exited with code 3010`| Reboot richiesto post-WU. Re-run con `-SkipWindowsUpdate` | | `[Defender] Validation failed: GPO key` | Defender riattivato da Windows Update — disabilita via gpedit + reboot | | `VS Build Tools installation timed out` | Network slow o cache corrotta. Re-run con cache pulita (`C:\ProgramData\Microsoft\VisualStudio\Packages`) | | `Python version match` fail | `$PythonVersion` ≠ versione installata. Aggiorna param o reinstalla | | Post-setup `MSBuild not found` | VS install fallita silenziosa. Check `C:\CI\vs_log.txt` | | Pre-snapshot Final check fail | Stato VM rotto — non prendere snapshot. Re-run o ricomincia da zero | --- ## Backlog correlato (TODO.md) - **§1.1 [P0]** Migrare WinRM da HTTP/Basic a HTTPS/5986 con cert self-signed nel template - **§1.3 [P0]** Pinning hash SHA256 degli installer (Python, dotnet-install.ps1, vs_buildtools.exe) - **§1.6 [P2]** Documentare threat model per Defender/Firewall/UAC tutti off - **§2.5 [P1]** Snapshot versionato `BaseClean_` (rollback in caso di refresh rotto) - **§2.6 [P2]** Backup automatico VMDK template pre-snapshot - **In-VM Git Clone** — installare Git for Windows + 7-Zip nel template per ottimizzare flusso