Files
local-ci-cd-system/docs/WINDOWS-TEMPLATE-SETUP.md
Simone a906f4696f
Lint / pssa (push) Failing after 3s
Lint / python (push) Failing after 3s
fix(templates): bump WinRM quotas + desktop heap for parallel Win builds
Parallel MSBuild fan-out (e.g. ns7zip x86/x64/x86-ansi) over pypsrp
Linux→Win fails with MSBuild MSB6003 / Win32 1816 'Not enough quota'
because two default guest limits are too tight for concurrent native
processes via WinRM:

  - Plugin Microsoft.PowerShell MaxMemoryPerShellMB = 150 MB
    (pypsrp uses the plugin endpoint, not the Shell endpoint that
    Deploy was previously bumping)
  - Non-interactive desktop heap SharedSection 3rd field = 768 KB
    (Session 0 csrss; CreateProcess fails when 3+ cl.exe spawn)

Pre-migration Win→Win went through Invoke-Command on PSSession with
quotas inherited from the local process, so the limits never bit.

Changes:
  - Deploy-WinBuild2025.ps1 / Deploy-WinBuild2022.ps1: also set the
    plugin-level MaxMemoryPerShellMB=2048, bump MaxProcessesPerShell
    25→100, patch SharedSection 3rd field 768→4096 (reboot needed for
    csrss to pick up, applied by post-install shutdown).
  - Install-CIToolchain-WinBuild2025/2022.ps1: add Assert-Step for
    plugin quota, MaxProcessesPerShell≥100, SharedSection≥4096; fail
    hard if any are under threshold.
  - docs/WINDOWS-TEMPLATE-SETUP.md: new "WinRM quotas e desktop heap"
    section explaining the diagnosis + one-shot patch for legacy
    templates; updated Step 3 validation row + troubleshooting entry.
  - AGENTS.md: error #13 with symptom, root cause, and pointer to docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 23:35:57 +02:00

366 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Server 2025 unattended da ISO |
| `template/Prepare-WinBuild2025.ps1` | **Host** | Orchestratore: copia + esegue Setup nel guest via WinRM (2025) |
| `template/Install-CIToolchain-WinBuild2025.ps1` | **Dentro VM** | CI toolchain (user, .NET, Python, VS) + validazione hardening Deploy |
| `template/Deploy-WinBuild2022.ps1` | **Host** | Opzionale: crea VM e installa Windows Server 2022 unattended da ISO |
| `template/Prepare-WinBuild2022.ps1` | **Host** | Orchestratore: copia + esegue Setup nel guest via WinRM (2022) |
| `template/Install-CIToolchain-WinBuild2022.ps1` | **Dentro VM** | CI toolchain per Windows Server 2022 (stesso flusso del 2025) |
| `template/Validate-DeployState.ps1` | **Host** | Valida stato post-Deploy prima di eseguire Prepare (2025 e 2022) |
| `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.
- `Install-CIToolchain-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 `Install-CIToolchain-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\WinBuild2025\WinBuild2025.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\WinBuild2025\WinBuild2025.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
# AllowUnencrypted left false — Deploy post-install.ps1 creates HTTPS/5986 listener
# Basic auth is secure over HTTPS; host connects on port 5986 with -SkipCACheck
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 <IP_KMS>``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, `Install-CIToolchain-WinBuild2025.ps1` presente, IP ottetti 0-255, TCP/5986 reachable
2. **Configura host WinRM client**: aggiunge `$VMIPAddress` ai `TrustedHosts` (no `AllowUnencrypted` — HTTPS non lo richiede)
- Salva valore precedente, ripristinato nel `finally` (host posture invariata)
3. **Validazione host WinRM**: `TrustedHosts` include IP
4. **Test connettività**: `Test-WSMan -Port 5986 -UseSSL -Authentication Basic` con credenziali admin
5. **Apre PSSession** `-UseSSL -Port 5986` verso la VM
6. **Crea `C:\CI`** in guest, valida creazione
7. **Copia `Install-CIToolchain-WinBuild2025.ps1`** in `C:\CI\Install-CIToolchain-WinBuild2025.ps1`, valida size match
8. **Esegue `Install-CIToolchain-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 Install-CIToolchain-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 `Install-CIToolchain-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=false (HTTPS-only), Auth/Basic=true, Shell+Plugin MaxMem≥2048, MaxProcesses≥100, desktop heap SharedSection 3rd≥4096 |
| 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\WinBuild2025\WinBuild2025.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_<yyyyMMdd>` (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
---
## WinRM quotas e desktop heap (build paralleli)
I template Windows escono di default da Windows con quote dimensionate per uso
interattivo. Da Linux/`pypsrp` queste quote si rivelano strette appena il build
guest fa fan-out di processi nativi (compilatori, linker) in parallelo. Tre
configurazioni che il template **deve** applicare prima dello snapshot
`BaseClean` (gestite automaticamente da `Deploy-WinBuild2025.ps1` / `2022`):
| Setting | Default | Valore template | Motivo |
| ---------------------------------------------------------------------------------------- | ------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WSMan:\localhost\Shell\MaxMemoryPerShellMB` | 150 | 2048 | Limite globale memoria per shell WinRM (winrs) |
| `WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB` | 150 | 2048 | **pypsrp da Linux usa l'endpoint plugin Microsoft.PowerShell**, non lo Shell. Senza questo, builds paralleli falliscono con `MSB6003 / Not enough quota` anche se Shell è a 2048 |
| `WSMan:\localhost\Shell\MaxProcessesPerShell` | 25 | 100 | MSBuild + cl + link + mt + altri tool si moltiplicano in fan-out parallelo |
| `HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems\Windows SharedSection` | `…,768` | `…,4096` | Heap del desktop **non-interattivo** (Session 0). CreateProcess fallisce con `ERROR_NOT_ENOUGH_QUOTA` (1816) quando 3+ cl.exe partono insieme. Richiede **reboot** per essere letto da `csrss.exe`. |
**Importante**: la modifica `SharedSection` è una scrittura di registry che il
kernel legge **solo al boot** (`csrss.exe`). Lo `shutdown /s` finale di
`post-install.ps1` fa sì che il prossimo avvio del template (per ulteriore
manutenzione, o l'invocazione di `Install-CIToolchain`) parta con il valore
corretto. Il `BaseClean` snapshot va preso **dopo** un boot completo per essere
certi che il valore sia attivo.
**Perché pre-migrazione (Win host) funzionava**: il path Windows→Windows usava
`Invoke-Command` su `PSSession` con la stessa identità del processo locale (act_runner
come LocalSystem), ereditando spesso quote più generose tramite GPO e default
del client locale. Il path Linux→Windows (`pypsrp`) attinge ai default puri
dell'endpoint plugin Microsoft.PowerShell del guest — se il template non li ha
bumpati, sono **150 MB / 25 processi / 768 KB heap**.
**Validazione**: `Install-CIToolchain-WinBuild2025.ps1` Step 3 controlla tutti e
quattro i valori e fallisce hard se anche solo uno è sotto soglia. Per
template già esistenti, applicare il blocco PowerShell sotto, reboot, shutdown,
nuovo snapshot `BaseClean`:
```powershell
# Una tantum su template legacy
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force
Set-Item 'WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB' 2048 -Force
Set-Item WSMan:\localhost\Shell\MaxProcessesPerShell 100 -Force
$k = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems'
$cur = (Get-ItemProperty -Path $k -Name Windows).Windows
$new = $cur -replace 'SharedSection=(\d+),(\d+),\d+', 'SharedSection=$1,$2,4096'
Set-ItemProperty -Path $k -Name Windows -Value $new
Restart-Service WinRM -Force
Restart-Computer -Force # necessario per il desktop heap
```
---
## Troubleshooting
| Sintomo | Diagnosi / Fix |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `Cannot reach $VMIPAddress via WinRM` | VM non avviata, WinRM non abilitato, IP errato — verifica con ipconfig |
| `Install-CIToolchain-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 |
| MSBuild `error MSB6003: CL.exe could not be run` + `Win32Exception (0x80004005): Not enough quota` su build paralleli | Quote WinRM/desktop-heap non bumpate nel template (vedi §"WinRM quotas e desktop heap" sotto). Tipico quando il build interno fa fan-out (es. 3× cl.exe paralleli) via pypsrp Linux→Win |
---
## 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_<yyyyMMdd>` (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