feat(template): add Deploy-WinBuild2025.ps1 + autounattend; update TODO
template/Deploy-WinBuild2025.ps1:
New host-side script that drives the unattended Windows install phase:
creates the VM, injects autounattend.xml (disabling Defender/firewall/UAC
before Setup-WinBuild2025.ps1 runs), boots from ISO, waits for first-boot.
template/autounattend.template.xml:
WiM-based answer file template for Windows Server 2025 unattended install.
Sets DisableAntiSpyware GPO + RTP off so Defender is fully off at first
logon (prerequisite for Setup-WinBuild2025.ps1 Step-2 validation-only path).
TODO.md:
- Date + wording updated (2026-05-10)
- §1.1: file refs updated to post-refactor line numbers (Step 3 WinRM,
Deploy-WinBuild2025 Enable-PSRemoting, Invoke-RemoteBuild, Get-BuildArtifacts)
- §1.2: TrustedHosts audit status corrected (Setup-Host.ps1 never sets '*';
Prepare appends IP and restores in finally)
- §1.3: Python/dotnet/VS Build Tools line refs updated
- §1.5: PAT security constraints expanded with grep-on-log safety net rule
- §1.6: Defender/Firewall/UAC state updated to reflect Deploy vs Setup split
- §3.3 deploy reference: VMX path corrected to CI-WinBuild.vmx
- §3.3 doc ref: WINDOWS-TEMPLATE-SETUP updated to list Deploy step
This commit is contained in:
@@ -1,14 +1,13 @@
|
|||||||
# TODO — Local CI/CD System
|
# TODO — Local CI/CD System
|
||||||
|
|
||||||
<!-- Last updated: 2026-05-09 — TODO+CONSIGLI uniti, priorità P0..P3 + rationale per ogni voce aperta -->
|
<!-- Last updated: 2026-05-10 — line refs e wording allineati al refactor 2026-05-09 -->
|
||||||
|
|
||||||
> Documento unico di lavoro: roadmap, audit trail dei task completati, e backlog post-v1.0
|
> Documento unico di lavoro: roadmap, audit trail dei task completati, e backlog post-v1.0
|
||||||
> con priorità e razionale (assorbito da `CONSIGLI.md`). Le voci aperte sono raggruppate
|
> con priorità e razionale. Le voci aperte sono raggruppate per area e marcate **P0..P3**:
|
||||||
> per area e marcate **P0..P3**:
|
|
||||||
>
|
>
|
||||||
> **Doc di setup operativi**:
|
> **Doc di setup operativi**:
|
||||||
> - [docs/HOST-SETUP.md](docs/HOST-SETUP.md) — bootstrap macchina host (Setup-Host.ps1)
|
> - [docs/HOST-SETUP.md](docs/HOST-SETUP.md) — bootstrap macchina host (Setup-Host.ps1)
|
||||||
> - [docs/WINDOWS-TEMPLATE-SETUP.md](docs/WINDOWS-TEMPLATE-SETUP.md) — provisioning template Windows (Prepare + Setup-WinBuild2025)
|
> - [docs/WINDOWS-TEMPLATE-SETUP.md](docs/WINDOWS-TEMPLATE-SETUP.md) — provisioning template Windows (Deploy + Prepare + Setup-WinBuild2025)
|
||||||
> - [docs/LINUX-TEMPLATE-SETUP.md](docs/LINUX-TEMPLATE-SETUP.md) — bozza/TODO template Linux (non implementato)
|
> - [docs/LINUX-TEMPLATE-SETUP.md](docs/LINUX-TEMPLATE-SETUP.md) — bozza/TODO template Linux (non implementato)
|
||||||
>
|
>
|
||||||
> - **P0** — sicurezza o affidabilità: affrontare prima di estendere il sistema
|
> - **P0** — sicurezza o affidabilità: affrontare prima di estendere il sistema
|
||||||
@@ -55,7 +54,7 @@
|
|||||||
### Fase A — Crea e installa la VM (NIC: NAT per internet)
|
### Fase A — Crea e installa la VM (NIC: NAT per internet)
|
||||||
- [x] Crea nuova VM in VMware Workstation con queste impostazioni:
|
- [x] Crea nuova VM in VMware Workstation con queste impostazioni:
|
||||||
- 4 vCPU, 6144 MB RAM, disco 80 GB thin
|
- 4 vCPU, 6144 MB RAM, disco 80 GB thin
|
||||||
- VMX path: `F:\CI\Templates\WinBuild\WinBuild.vmx`
|
- VMX path: `F:\CI\Templates\WinBuild\CI-WinBuild.vmx`
|
||||||
- **NIC: VMnet8 (NAT)** ← internet necessario per Windows Update e installer
|
- **NIC: VMnet8 (NAT)** ← internet necessario per Windows Update e installer
|
||||||
- CD-ROM: `F:\CI\ISO\26100.1742.240906-0331.ge_release_svc_refresh_SERVER_VOL_x64FRE_en-us.iso`
|
- CD-ROM: `F:\CI\ISO\26100.1742.240906-0331.ge_release_svc_refresh_SERVER_VOL_x64FRE_en-us.iso`
|
||||||
- [x] Installa Windows Server 2025 dall'ISO
|
- [x] Installa Windows Server 2025 dall'ISO
|
||||||
@@ -150,7 +149,7 @@
|
|||||||
## 1. Sicurezza & hardening
|
## 1. Sicurezza & hardening
|
||||||
|
|
||||||
### 1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986
|
### 1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986
|
||||||
File: [template/Setup-WinBuild2025.ps1:179-198](template/Setup-WinBuild2025.ps1), [scripts/Invoke-RemoteBuild.ps1:82-91](scripts/Invoke-RemoteBuild.ps1).
|
File: [template/Setup-WinBuild2025.ps1:204-241](template/Setup-WinBuild2025.ps1) (Step 3 WinRM), [template/Deploy-WinBuild2025.ps1:524-534](template/Deploy-WinBuild2025.ps1) (Enable-PSRemoting + AllowUnencrypted/Basic), [scripts/Invoke-RemoteBuild.ps1:83-91](scripts/Invoke-RemoteBuild.ps1), [scripts/Get-BuildArtifacts.ps1:66-75](scripts/Get-BuildArtifacts.ps1).
|
||||||
|
|
||||||
**Motivazione**: la combo `AllowUnencrypted=true` + `Auth/Basic=true` invia credenziali del
|
**Motivazione**: la combo `AllowUnencrypted=true` + `Auth/Basic=true` invia credenziali del
|
||||||
`ci_build` (admin) in chiaro su VMnet8. Anche in lab isolato, l'host può essere compromesso
|
`ci_build` (admin) in chiaro su VMnet8. Anche in lab isolato, l'host può essere compromesso
|
||||||
@@ -164,16 +163,20 @@ manca solo l'esecuzione.
|
|||||||
- [ ] Tenere `-SkipCACheck` lato host: per cert lab self-signed è accettabile, ma documentare la motivazione inline.
|
- [ ] Tenere `-SkipCACheck` lato host: per cert lab self-signed è accettabile, ma documentare la motivazione inline.
|
||||||
- [ ] Regole firewall — limitare WinRM alla subnet build VM (`192.168.79.0/24` — VMnet8 NAT).
|
- [ ] Regole firewall — limitare WinRM alla subnet build VM (`192.168.79.0/24` — VMnet8 NAT).
|
||||||
|
|
||||||
### 1.2 [P0] Restringere `TrustedHosts` lato host
|
### 1.2 [P0] Restringere `TrustedHosts` lato host (audit-only — già coperto)
|
||||||
File: probabile `Setup-Host.ps1` (e in vari run setup) — pattern `*` è incompatibile con altri carichi sull'host Windows 11.
|
File: nessuno script imposta `*` sull'host. Stato corrente:
|
||||||
|
- `Setup-Host.ps1` non tocca TrustedHosts (verificato 2026-05-10)
|
||||||
|
- [template/Prepare-WinBuild2025.ps1:270-294](template/Prepare-WinBuild2025.ps1:270) appende `$VMIPAddress` e ripristina nel `finally` (riga 617)
|
||||||
|
- [docs/HOST-SETUP.md:121-126](docs/HOST-SETUP.md:121) raccomanda `'192.168.79.*'` in setup manuale
|
||||||
|
|
||||||
Sostituire con la subnet build:
|
**Azione residua**: audit eventuali host già configurati con `*` (eredità manuale) e
|
||||||
|
sostituire con la subnet build:
|
||||||
```powershell
|
```powershell
|
||||||
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate
|
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.3 [P0] Pinning hash SHA256 degli installer
|
### 1.3 [P0] Pinning hash SHA256 degli installer
|
||||||
File: [template/Setup-WinBuild2025.ps1:332-399](template/Setup-WinBuild2025.ps1), [template/Setup-WinBuild2025.ps1:401-502](template/Setup-WinBuild2025.ps1).
|
File: [template/Setup-WinBuild2025.ps1:707-755](template/Setup-WinBuild2025.ps1) (Step 8 Python), [template/Setup-WinBuild2025.ps1:757-885](template/Setup-WinBuild2025.ps1) (Step 9 VS Build Tools), [template/Setup-WinBuild2025.ps1:660-705](template/Setup-WinBuild2025.ps1) (Step 7 dotnet-install.ps1).
|
||||||
|
|
||||||
**Motivazione**: Python, `dotnet-install.ps1`, `vs_buildtools.exe` vengono scaricati senza
|
**Motivazione**: Python, `dotnet-install.ps1`, `vs_buildtools.exe` vengono scaricati senza
|
||||||
verifica integrità. Un MITM nella rete dell'host (anche temporaneo) può iniettare binari
|
verifica integrità. Un MITM nella rete dell'host (anche temporaneo) può iniettare binari
|
||||||
@@ -200,16 +203,24 @@ $ipv4 = '^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?
|
|||||||
Estrarre la costante in un modulo condiviso `scripts/_Common.psm1` per evitare drift fra i 4 file (vedi §5.2).
|
Estrarre la costante in un modulo condiviso `scripts/_Common.psm1` per evitare drift fra i 4 file (vedi §5.2).
|
||||||
|
|
||||||
### 1.5 [P1] PAT mai persistito quando si abilita `-UseGitClone`
|
### 1.5 [P1] PAT mai persistito quando si abilita `-UseGitClone`
|
||||||
Quando arriva il momento di implementare il clone in-VM (vedi §3.3 / sezione "In-VM Git Clone"):
|
Requisiti di sicurezza per l'implementazione del clone in-VM. La sequenza operativa
|
||||||
- [ ] Recuperare il PAT da Credential Manager dell'host **subito prima** della WinRM session, mai prima.
|
(quando, dove, come) è dettagliata nella sezione "In-VM Git Clone (Ottimizzazione) — riferimento §3.3".
|
||||||
- [ ] Iniettare via `$using:cred` in `Invoke-Command` e cancellare la variabile in un `finally` interno alla scriptblock.
|
|
||||||
- [ ] Aggiungere un grep automatico nei log job: se il PAT compare grezzo, fallire la build.
|
Vincoli da rispettare in qualsiasi implementazione di `-UseGitClone`:
|
||||||
|
- [ ] PAT recuperato da Credential Manager **subito prima** della WinRM session, mai prima.
|
||||||
|
- [ ] Iniettato via `$using:cred` o env var di sessione, mai in argv né in log/transcript.
|
||||||
|
- [ ] Cancellato in `finally` interno alla scriptblock guest.
|
||||||
|
- [ ] **Grep automatico post-job sui log**: se il PAT compare grezzo in `$jobLog` o
|
||||||
|
`transcript.txt`, marcare la build come fallita e ruotare il PAT (regola di safety net,
|
||||||
|
non sostituisce le precedenti).
|
||||||
|
|
||||||
### 1.6 [P2] Defender + Firewall + UAC tutti disattivati nel template — documentare il modello di minaccia
|
### 1.6 [P2] Defender + Firewall + UAC tutti disattivati nel template — documentare il modello di minaccia
|
||||||
File: [template/Setup-WinBuild2025.ps1:200-329](template/Setup-WinBuild2025.ps1).
|
File: [template/Deploy-WinBuild2025.ps1:485-551](template/Deploy-WinBuild2025.ps1) (UAC, ServerManager, DisableCAD, OOBE — set da Deploy), [template/Setup-WinBuild2025.ps1:176-336](template/Setup-WinBuild2025.ps1) (Firewall Step 1, Defender Step 2 validation-only post-refactor 2026-05-09, WinRM Step 3, User Step 4, UAC Step 4b, Dirs Step 5).
|
||||||
|
|
||||||
Attualmente: `Set-NetFirewallProfile ... -Enabled False`, `EnableLUA=0`, `DisableAntiSpyware=1`,
|
Stato attuale (post-refactor §7):
|
||||||
`DisableRealtimeMonitoring=1`, `LocalAccountTokenFilterPolicy=1`.
|
- **Defender**: disabled da Deploy (`DisableAntiSpyware=1` GPO + RTP off) — Setup Step 2 è validation-only
|
||||||
|
- **Firewall**: disabled da Setup Step 1 (`Set-NetFirewallProfile ... -Enabled False`); §7.1 prevede spostamento a Deploy
|
||||||
|
- **UAC**: `EnableLUA=0`, `LocalAccountTokenFilterPolicy=1` — duplicato Deploy + Setup Step 4b (§7.1 lo riduce a Assert-Step in Setup)
|
||||||
|
|
||||||
Le scelte sono ragionevoli per un template lab efimero, ma vanno raccolte in un'unica sezione
|
Le scelte sono ragionevoli per un template lab efimero, ma vanno raccolte in un'unica sezione
|
||||||
`BEST-PRACTICES.md §X — Threat Model & Hardening Trade-offs` che indichi:
|
`BEST-PRACTICES.md §X — Threat Model & Hardening Trade-offs` che indichi:
|
||||||
@@ -237,6 +248,9 @@ senza tracciamento.
|
|||||||
### 2.1 [P0] Race su IP allocation con `capacity: 4`
|
### 2.1 [P0] Race su IP allocation con `capacity: 4`
|
||||||
File: [runner/config.yaml:17](runner/config.yaml), [scripts/Invoke-CIJob.ps1:244-253](scripts/Invoke-CIJob.ps1).
|
File: [runner/config.yaml:17](runner/config.yaml), [scripts/Invoke-CIJob.ps1:244-253](scripts/Invoke-CIJob.ps1).
|
||||||
|
|
||||||
|
**Stato osservato**: race non riprodotta in e2e-008/009 (un job alla volta). `capacity: 4`
|
||||||
|
resta attivo in produzione — bug teorico finché non si esegue stress test parallelo.
|
||||||
|
|
||||||
**Motivazione**: con 4 job paralleli e DHCP VMnet8, due VM possono ottenere lo stesso IP se
|
**Motivazione**: con 4 job paralleli e DHCP VMnet8, due VM possono ottenere lo stesso IP se
|
||||||
la lease pool è stretta o se due `vmrun start` rispondono troppo vicini.
|
la lease pool è stretta o se due `vmrun start` rispondono troppo vicini.
|
||||||
`getGuestIPAddress` ritorna l'IP visto dalla VM stessa, quindi la collisione non viene
|
`getGuestIPAddress` ritorna l'IP visto dalla VM stessa, quindi la collisione non viene
|
||||||
@@ -337,7 +351,8 @@ $env:PIP_CACHE_DIR = '\\vmware-host\Shared Folders\pip-cache'
|
|||||||
Dopo lo snapshot refresh la cache va riscaldata una volta — accettabile.
|
Dopo lo snapshot refresh la cache va riscaldata una volta — accettabile.
|
||||||
|
|
||||||
### 3.2 [P1] Sostituire `Compress-Archive` con 7-Zip o robocopy
|
### 3.2 [P1] Sostituire `Compress-Archive` con 7-Zip o robocopy
|
||||||
File: [scripts/Invoke-RemoteBuild.ps1:109-122](scripts/Invoke-RemoteBuild.ps1).
|
File: [scripts/Invoke-RemoteBuild.ps1:112,148,185](scripts/Invoke-RemoteBuild.ps1) (3 chiamate `Compress-Archive`).
|
||||||
|
Prerequisito: 7-Zip nel template — vedi §6.6 e sezione "In-VM Git Clone".
|
||||||
|
|
||||||
**Motivazione**: `Compress-Archive` è single-threaded e su repo medio-grandi (>100 MB sorgente)
|
**Motivazione**: `Compress-Archive` è single-threaded e su repo medio-grandi (>100 MB sorgente)
|
||||||
può prendere 20-40 s.
|
può prendere 20-40 s.
|
||||||
@@ -354,7 +369,8 @@ può prendere 20-40 s.
|
|||||||
Misurare con un repo reale: probabile risparmio 10-20 s/build su `nsis-plugin-nsinnounp`.
|
Misurare con un repo reale: probabile risparmio 10-20 s/build su `nsis-plugin-nsinnounp`.
|
||||||
|
|
||||||
### 3.3 [P2] In-VM clone (`-UseGitClone`)
|
### 3.3 [P2] In-VM clone (`-UseGitClone`)
|
||||||
Implementazione disegnata di seguito (sezione "In-VM Git Clone — Ottimizzazione").
|
Implementazione disegnata di seguito (sezione "In-VM Git Clone (Ottimizzazione)").
|
||||||
|
Vincoli sicurezza PAT: vedi §1.5. Tool prerequisiti (Git, 7-Zip): vedi §6.6.
|
||||||
Beneficio reale solo per repo > 200 MB con submoduli grandi; misurare prima.
|
Beneficio reale solo per repo > 200 MB con submoduli grandi; misurare prima.
|
||||||
|
|
||||||
### 3.4 [P3] Pre-warm pool di cloni
|
### 3.4 [P3] Pre-warm pool di cloni
|
||||||
@@ -481,7 +497,7 @@ e centralizza retry/log.
|
|||||||
File: [gitea/workflows/lint.yml](gitea/workflows/lint.yml) (già esiste).
|
File: [gitea/workflows/lint.yml](gitea/workflows/lint.yml) (già esiste).
|
||||||
|
|
||||||
Aggiungere regole specifiche progetto (`PSScriptAnalyzerSettings.psd1` in root):
|
Aggiungere regole specifiche progetto (`PSScriptAnalyzerSettings.psd1` in root):
|
||||||
- `PSAvoidUsingPlainTextForPassword` — rilevante per `Setup-WinBuild2025.ps1:55`.
|
- `PSAvoidUsingPlainTextForPassword` — rilevante per [Setup-WinBuild2025.ps1:123](template/Setup-WinBuild2025.ps1:123) (`[string] $BuildPassword` → `ConvertTo-SecureString -AsPlainText` riga 253).
|
||||||
- `PSAvoidUsingInvokeExpression`.
|
- `PSAvoidUsingInvokeExpression`.
|
||||||
- `PSUseShouldProcessForStateChangingFunctions`.
|
- `PSUseShouldProcessForStateChangingFunctions`.
|
||||||
- Regole custom: vietare hardcoded `F:\CI\` fuori da config (forzare param/env).
|
- Regole custom: vietare hardcoded `F:\CI\` fuori da config (forzare param/env).
|
||||||
@@ -545,6 +561,153 @@ runs-on: ${{ matrix.target }}-build
|
|||||||
Per chiavi di firma (Authenticode, GPG), usare i Gitea secrets nel workflow e passarli a
|
Per chiavi di firma (Authenticode, GPG), usare i Gitea secrets nel workflow e passarli a
|
||||||
`Invoke-CIJob.ps1` come param + env, mai loggati. Stesso modello di §1.5 per il PAT.
|
`Invoke-CIJob.ps1` come param + env, mai loggati. Stesso modello di §1.5 per il PAT.
|
||||||
|
|
||||||
|
### 6.6 [P2] Toolchain Tier-1 in Setup-WinBuild2025
|
||||||
|
File: [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
|
||||||
|
|
||||||
|
**Motivazione**: toolchain attuale (`.NET SDK`, `Python`, `VS Build Tools`) copre solo C#/Python/MSBuild.
|
||||||
|
Aggiungere tools generici-CI riduce il bisogno di installazione ad-hoc nei workflow e abilita
|
||||||
|
build più ampi senza toccare il template.
|
||||||
|
|
||||||
|
Tool da aggiungere come step `Setup-WinBuild2025` (ognuno con `Assert-Step`):
|
||||||
|
|
||||||
|
| Tool | Versione | Razionale |
|
||||||
|
| ------------------- | ----------- | ------------------------------------------------------------ |
|
||||||
|
| **Git for Windows** | latest LTS | Self-clone in VM (alt a host-zip-transfer), submodule fetch |
|
||||||
|
| **7-Zip** | latest | Universale unpack/zip; molti installer e workflow ne dipendono |
|
||||||
|
| **PowerShell 7.x** | latest LTS | Più veloce di 5.1, parallel pipelines, operatori moderni |
|
||||||
|
| **NSIS** | latest | Test build installer (es. `nsis-plugin-nsinnounp`) |
|
||||||
|
| **CMake** | latest | Build system cross-platform per progetti C/C++ nativi |
|
||||||
|
| **Node.js LTS** | latest LTS | Frontend/Electron pipelines, JS toolchain (npm) |
|
||||||
|
| **WiX Toolset** | v4 stable | MSI building da .NET projects |
|
||||||
|
| **gh CLI** | latest | Upload artifact a release Gitea/GitHub, comment PR |
|
||||||
|
| **Sysinternals** | sysinternals.com `live` | Diagnostica process/handle quando build flaky |
|
||||||
|
| **vcpkg** | latest | Package manager C++ per dipendenze native |
|
||||||
|
|
||||||
|
**Approccio**:
|
||||||
|
- Pinning hash SHA256 per ogni installer (vedi §1.3)
|
||||||
|
- Convenzione path guest: tool in `C:\BuildTools\<tool>\` (separato da `C:\CI\` runtime
|
||||||
|
— `C:\CI\` resta per artefatti job e worker temp; `C:\BuildTools\` per software statico).
|
||||||
|
Documentare in `docs/BEST-PRACTICES.md` prima di implementare.
|
||||||
|
- Aggiungere a `PATH` machine-wide
|
||||||
|
- Defender già off → no scan overhead durante install
|
||||||
|
- Step esegue in seriale dopo `Toolchain cross-check` esistente, prima di `Cleanup`
|
||||||
|
- `Final pre-snapshot gate`: aggiungere check `where.exe` per ogni tool
|
||||||
|
- Cross-link: Git for Windows e 7-Zip sono richiesti anche da §3.2 e dalla sezione
|
||||||
|
"In-VM Git Clone" — implementare qui per evitare doppio lavoro
|
||||||
|
|
||||||
|
**Validazione per-tool**:
|
||||||
|
- Eseguibile risolvibile via `where.exe`
|
||||||
|
- Versione exact match al param `-<Tool>Version`
|
||||||
|
- (per gh) `gh --version` returns 0
|
||||||
|
- (per vcpkg) `vcpkg.exe version`
|
||||||
|
|
||||||
|
**Param da aggiungere** a Setup + Prepare:
|
||||||
|
- `-GitVersion`, `-7ZipVersion`, `-Pwsh7Version`, `-NSISVersion`, `-CMakeVersion`,
|
||||||
|
`-NodeJSVersion`, `-WiXVersion`, `-GhCliVersion`, `-VcpkgRev` (rev SHA del repo)
|
||||||
|
|
||||||
|
**Tier-2 (opzionali, separati)**: Java JDK, Maven/Gradle, Rust, Go, LLVM, Docker, Azure/AWS CLI, WinDbg.
|
||||||
|
Non includere in Setup base — workflow-level via `choco install` o download diretto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Refactor confine Deploy ↔ Setup
|
||||||
|
|
||||||
|
**Obiettivo**: separazione netta delle responsabilità — Deploy produce **template OS**
|
||||||
|
stand-alone, Setup fa **build customization** (user, dirs, toolchain). Eliminare drift e
|
||||||
|
duplicazioni; ogni concern ha **una sola** sorgente di verità.
|
||||||
|
|
||||||
|
**Stato attuale (duplicati)**: UAC, Explorer LaunchTo, Server Manager off, DisableCAD,
|
||||||
|
OOBE telemetry, WU services disable — entrambi gli script li toccano.
|
||||||
|
|
||||||
|
### 7.1 [P1] Spostare base OS hardening da Setup a Deploy
|
||||||
|
File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
|
||||||
|
[template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
|
||||||
|
|
||||||
|
**Da MUOVERE Setup → Deploy**:
|
||||||
|
|
||||||
|
| Concern Setup | Step attuale Setup | Azione Deploy |
|
||||||
|
| ----------------------------- | ------------------ | ------------------------------------------- |
|
||||||
|
| Firewall off all profiles | Step 1 | Aggiungere `Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled False` in post-install (ora Deploy aggiunge solo regole RDP/ICMP) |
|
||||||
|
| WinRM tuning memoria/timeout | Step 3 (parziale) | Aggiungere `MaxMemoryPerShellMB=2048`, `IdleTimeOut=7200000`, `MaxProcessesPerShell=25` in post-install dopo Enable-PSRemoting |
|
||||||
|
|
||||||
|
**Da CONVERTIRE in Setup a Assert-Step (validation-only)**:
|
||||||
|
|
||||||
|
| Step Setup | Diventa |
|
||||||
|
| ---------- | ------- |
|
||||||
|
| Step 1 Firewall | `Assert-Step` 3 profili Enabled=False (richiede prima la riga "Da MUOVERE" → Deploy: Set-NetFirewallProfile -Enabled False) |
|
||||||
|
| Step 2 Defender | Già validation-only (refactor 2026-05-09) |
|
||||||
|
| Step 3 WinRM tuning | `Assert-Step` MaxMemoryPerShellMB, IdleTimeOut, AllowUnencrypted, Basic |
|
||||||
|
| Step 4b UAC | `Assert-Step` EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
|
||||||
|
| Step 5b Explorer LaunchTo | `Assert-Step` HKCU LaunchTo=1 |
|
||||||
|
| Step 5c Server Manager / DisableCAD / OOBE | `Assert-Step` HKLM machine policy + DisableCAD=1 + OOBE policy |
|
||||||
|
|
||||||
|
**Da MANTENERE in Setup (build env, non base OS)**:
|
||||||
|
- Step 4: build user `ci_build` + admin
|
||||||
|
- Step 5: `C:\CI\{build,output,scripts}` dirs
|
||||||
|
- Step 7-9: .NET SDK, Python, VS Build Tools
|
||||||
|
- Step 10: Toolchain cross-check
|
||||||
|
- Cleanup, Final pre-snapshot gate
|
||||||
|
|
||||||
|
**Test acceptance**:
|
||||||
|
- Deploy stand-alone produce VM con tutte le validation Setup che passano (no Setup run)
|
||||||
|
- Setup run dopo Deploy: tutte Assert-Step pass `[OK]` immediato (no work)
|
||||||
|
- Test `--Skip` orthogonali (no regressione)
|
||||||
|
|
||||||
|
### 7.2 [P1] WU lifecycle — strategia B (Deploy: GPO + Manual; Setup: lifecycle)
|
||||||
|
File: [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1),
|
||||||
|
[template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1).
|
||||||
|
|
||||||
|
**Stato attuale (conflitto ordering)**:
|
||||||
|
- Deploy: `sc.exe config wuauserv start= disabled` + `sc.exe stop wuauserv` (idem UsoSvc, WaaSMedicSvc) + GPO `NoAutoUpdate=1`
|
||||||
|
- Setup Step 6: necessita servizi UP per SchTask SYSTEM run WU
|
||||||
|
- Setup Step 6b: re-disabilita servizi → doppio lavoro, fragile
|
||||||
|
|
||||||
|
**Strategia B — separazione pulita**:
|
||||||
|
|
||||||
|
**Deploy** (post-install.ps1):
|
||||||
|
- Lascia servizi WU `start=demand` (Manual, default Windows) — NON `disabled`, NON `stop`
|
||||||
|
- Setta solo GPO `NoAutoUpdate=1`, `DisableWindowsUpdateAccess=1` → no background scheduled WU
|
||||||
|
- Risultato: WU è invocabile on-demand ma non parte mai da solo
|
||||||
|
|
||||||
|
**Setup** (Step 6 / 6b):
|
||||||
|
- Step 6 (skip se `-SkipWindowsUpdate`): `sc.exe config wuauserv start= demand` (idempotent), avvia SchTask SYSTEM, attende exit code, parse 0/2/3/3010
|
||||||
|
- Step 6b (sempre, anche con `-Skip`): `sc.exe config wuauserv start= disabled` + `sc.exe stop wuauserv` (idem UsoSvc, WaaSMedicSvc), conferma GPO presenti — snapshot capture WU permanently off
|
||||||
|
|
||||||
|
**Pattern run periodico patches** (allineato §2.5 snapshot versionato):
|
||||||
|
- Re-deploy template `BaseClean_<yyyyMMdd>` ogni N mesi via Deploy + Prepare (no `-Skip`) → patch applicate al template fresh
|
||||||
|
|
||||||
|
**Validazione finale** (Setup Final gate):
|
||||||
|
- `wuauserv.StartType == Disabled`
|
||||||
|
- `UsoSvc.StartType == Disabled`
|
||||||
|
- GPO `NoAutoUpdate=1`, `DisableWindowsUpdateAccess=1`
|
||||||
|
|
||||||
|
**Test acceptance**:
|
||||||
|
- Deploy stand-alone: `Get-Service wuauserv | Select StartType` → `Manual`, `Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name NoAutoUpdate` → 1
|
||||||
|
- Setup `-SkipWindowsUpdate`: Final gate vede `StartType=Disabled` (impostato da Step 6b)
|
||||||
|
- Setup senza `-Skip`: WU runs, exit 0/2/3, poi servizi disabled
|
||||||
|
|
||||||
|
### 7.3 [P2] Update header docs di entrambi gli script
|
||||||
|
- [template/Deploy-WinBuild2025.ps1](template/Deploy-WinBuild2025.ps1) `.DESCRIPTION`:
|
||||||
|
enumerare tutte le hardening voci che Deploy ora possiede (firewall, WinRM tuning,
|
||||||
|
WU GPO non disable)
|
||||||
|
- [template/Setup-WinBuild2025.ps1](template/Setup-WinBuild2025.ps1) `.DESCRIPTION`:
|
||||||
|
marcare Step 1/3/4b/5b/5c come "validation-only — set by Deploy"
|
||||||
|
- [docs/WINDOWS-TEMPLATE-SETUP.md](docs/WINDOWS-TEMPLATE-SETUP.md): aggiornare descrizione
|
||||||
|
"Cosa fa Setup-WinBuild2025" — riflettere nuovo confine
|
||||||
|
|
||||||
|
### 7.4 [P2] Test e2e refactor
|
||||||
|
- Run Deploy stand-alone su VM test → verifica tutte le hardening voci presenti
|
||||||
|
(`reg query` per chiavi UAC/OOBE/Server Manager, `Get-NetFirewallProfile`, `Get-Service WMUauserv`)
|
||||||
|
- Run Prepare-WinBuild2025 dopo Deploy → tutti `Assert-Step` pass `[OK]` senza work
|
||||||
|
- Run Prepare con `-SkipWindowsUpdate` → comportamento invariato
|
||||||
|
- Run Prepare senza `-Skip` → WU runs, services lifecycled correttamente
|
||||||
|
|
||||||
|
### 7.5 [P3] Rinomina Setup → Setup-CITools (opzionale, futuro)
|
||||||
|
Una volta che Setup fa solo build customization, nome `Setup-WinBuild2025` è impreciso —
|
||||||
|
non setup-a la build VM, **estende** un template OS già pronto con toolchain CI.
|
||||||
|
Rinominare in `Setup-CITools.ps1` o `Install-CIToolchain.ps1` rifletterebbe meglio la
|
||||||
|
responsabilità ridotta. Tracciare in TODO post-7.1/7.2.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## In-VM Git Clone (Ottimizzazione) — riferimento §3.3
|
## In-VM Git Clone (Ottimizzazione) — riferimento §3.3
|
||||||
@@ -574,28 +737,18 @@ VM avviata → WinRM: git clone --recurse-submodules https://<token>@gitea/...
|
|||||||
|
|
||||||
### Task
|
### Task
|
||||||
|
|
||||||
- [ ] **Setup-WinBuild2025.ps1 — installare Git for Windows**
|
- [ ] **Tool prerequisiti nel template** — implementati in §6.6 (Tier-1 Toolchain):
|
||||||
- [ ] Scaricare e installare Git for Windows (installer silenzioso da git-scm.com)
|
- Git for Windows + 7-Zip + gh CLI installati con hash pinning e `Assert-Step`
|
||||||
```powershell
|
- Rimuovere il commento "Git is NOT installed" dal docblock di Setup-WinBuild2025.ps1
|
||||||
$gitUrl = 'https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.1/Git-2.47.1-64-bit.exe'
|
una volta applicato §6.6
|
||||||
Start-Process $gitUrl '/VERYSILENT /NORESTART /COMPONENTS=gitlfs' -Wait
|
- curl.exe: già presente in Windows Server 2025 — verificare con `where.exe curl`
|
||||||
```
|
|
||||||
- [ ] Aggiungere `C:\Program Files\Git\cmd` al PATH di sistema nella VM
|
|
||||||
- [ ] Verificare: `git --version` ritorna exit 0 nella VM
|
|
||||||
- [ ] Rimuovere il commento "Git is NOT installed" dal docblock di Setup-WinBuild2025.ps1
|
|
||||||
|
|
||||||
- [ ] **Setup-WinBuild2025.ps1 — altri tool utili**
|
|
||||||
- [ ] **7-Zip**: installer silenzioso — molto più veloce di `Compress-Archive`/`Expand-Archive`
|
|
||||||
per archivi grandi (es. npm/NuGet cache warm). Necessario anche per §3.2.
|
|
||||||
- [ ] **curl.exe**: già presente in Windows 11/Server 2025 (v7.x) — verificare disponibilità
|
|
||||||
- [ ] Valutare: `jq` (parsing JSON in script PowerShell o bash) — opzionale
|
|
||||||
|
|
||||||
- [ ] **Invoke-CIJob.ps1 — aggiungere switch `-UseGitClone`**
|
- [ ] **Invoke-CIJob.ps1 — aggiungere switch `-UseGitClone`**
|
||||||
- [ ] Quando `-UseGitClone` è attivo:
|
- [ ] Quando `-UseGitClone` è attivo:
|
||||||
- Saltare Phase 1 (host git clone) e la creazione del `HostSourceDir` temporaneo
|
- Saltare Phase 1 (host git clone) e la creazione del `HostSourceDir` temporaneo
|
||||||
- Dopo Wait-VMReady, leggere PAT da Windows Credential Manager sull'host
|
- Dopo Wait-VMReady, leggere PAT da Windows Credential Manager sull'host
|
||||||
(es. target `git:https://gitea.emulab.it/...` o un target dedicato CI)
|
(es. target `git:https://gitea.emulab.it/...` o un target dedicato CI)
|
||||||
- Passare PAT come parametro a `Invoke-RemoteBuild.ps1` (mai loggato/stampato)
|
- Passare PAT a `Invoke-RemoteBuild.ps1` rispettando i vincoli sicurezza di §1.5
|
||||||
- [ ] Comportamento default invariato (`-UseGitClone` = `$false`) — nessuna regressione
|
- [ ] Comportamento default invariato (`-UseGitClone` = `$false`) — nessuna regressione
|
||||||
|
|
||||||
- [ ] **Invoke-RemoteBuild.ps1 — modalità in-VM clone**
|
- [ ] **Invoke-RemoteBuild.ps1 — modalità in-VM clone**
|
||||||
@@ -608,8 +761,8 @@ VM avviata → WinRM: git clone --recurse-submodules https://<token>@gitea/...
|
|||||||
C:\CI\build
|
C:\CI\build
|
||||||
git -C C:\CI\build checkout $CloneCommit # pin al commit esatto
|
git -C C:\CI\build checkout $CloneCommit # pin al commit esatto
|
||||||
```
|
```
|
||||||
- [ ] Iniettare PAT come variabile d'ambiente della sessione WinRM (non argv, non log)
|
- [ ] Implementare iniezione/pulizia PAT secondo §1.5 (env var sessione, no argv, no log,
|
||||||
- [ ] Pulire la variabile d'ambiente dopo il clone: `[System.Environment]::SetEnvironmentVariable('CI_GIT_PAT', $null, 'Process')`
|
cleanup `[System.Environment]::SetEnvironmentVariable('CI_GIT_PAT', $null, 'Process')`)
|
||||||
|
|
||||||
- [ ] **Test e2e con `-UseGitClone`**
|
- [ ] **Test e2e con `-UseGitClone`**
|
||||||
- [ ] Aggiornare snapshot `BaseClean` con Git installato
|
- [ ] Aggiornare snapshot `BaseClean` con Git installato
|
||||||
|
|||||||
@@ -0,0 +1,938 @@
|
|||||||
|
#Requires -RunAsAdministrator
|
||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Deploys an unattended Windows Server 2025 VM on VMware Workstation
|
||||||
|
and installs VMware Tools automatically.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Host-side orchestrator. Produces a ready-to-use template VM:
|
||||||
|
Step 1 — Validate prerequisites (vmrun, vmware-vdiskmanager, ISOs)
|
||||||
|
Step 2 — Render autounattend.xml from template + post-install.ps1
|
||||||
|
(placeholders substituted with parameter values)
|
||||||
|
Step 3 — Build autounattend ISO (autounattend.xml + post-install.ps1)
|
||||||
|
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)
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
NIC strategy:
|
||||||
|
Boot install with e1000e (broad OOB Windows compatibility). VMware Tools
|
||||||
|
install brings the vmxnet3 driver. Post-Tools, the host swaps the .vmx
|
||||||
|
to vmxnet3 and reboots. This avoids driver injection in WinPE.
|
||||||
|
|
||||||
|
Disk layout (UEFI/GPT, 80 GB):
|
||||||
|
Part 1: EFI 100 MB FAT32 S:
|
||||||
|
Part 2: MSR 16 MB
|
||||||
|
Part 3: Primary rest NTFS C: ← Windows installs here
|
||||||
|
No Recovery partition (per user choice).
|
||||||
|
|
||||||
|
Hardening done by post-install.ps1 inside the guest:
|
||||||
|
- UAC off (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
|
||||||
|
- Defender real-time protection off
|
||||||
|
- DiagTrack/Telemetry off
|
||||||
|
- Windows Update services disabled
|
||||||
|
- RDP enabled (no NLA, lab only) + firewall rule
|
||||||
|
- WinRM enabled HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth,
|
||||||
|
AllowUnencrypted, TrustedHosts=*
|
||||||
|
- ICMPv4 inbound allowed
|
||||||
|
- VMware Tools installed (Complete) from Tools ISO
|
||||||
|
- install_complete.flag dropped in C:\Windows\Temp
|
||||||
|
|
||||||
|
.PARAMETER WinISO
|
||||||
|
Path to the Windows Server 2025 install ISO.
|
||||||
|
|
||||||
|
.PARAMETER ToolsISO
|
||||||
|
Path to the VMware Tools ISO (windows.iso).
|
||||||
|
|
||||||
|
.PARAMETER VMXPath
|
||||||
|
Full path of the .vmx file to create. The folder is created if missing.
|
||||||
|
|
||||||
|
.PARAMETER VMName
|
||||||
|
Display name in the Workstation library. Defaults to the .vmx file basename.
|
||||||
|
|
||||||
|
.PARAMETER ProductKey
|
||||||
|
Windows Server 2025 product key to embed in autounattend.
|
||||||
|
|
||||||
|
.PARAMETER ComputerName
|
||||||
|
Hostname. Max 15 chars, no spaces.
|
||||||
|
|
||||||
|
.PARAMETER AdminUser
|
||||||
|
.PARAMETER AdminPassword
|
||||||
|
Local Administrator account credentials.
|
||||||
|
|
||||||
|
.PARAMETER ImageIndex
|
||||||
|
Index of the install.wim edition to deploy. Default: 2 (Server 2025
|
||||||
|
Standard Desktop Experience on the VOL ISO). Verify with:
|
||||||
|
dism /Get-WimInfo /WimFile:<isomount>\sources\install.wim
|
||||||
|
|
||||||
|
.PARAMETER DiskSizeGB
|
||||||
|
Virtual disk size in GiB. Default: 80.
|
||||||
|
|
||||||
|
.PARAMETER MemoryMB
|
||||||
|
Guest RAM in MiB. Default: 6144 (6 GB).
|
||||||
|
|
||||||
|
.PARAMETER VCPUCount
|
||||||
|
Total vCPU. Default: 2.
|
||||||
|
|
||||||
|
.PARAMETER CoresPerSocket
|
||||||
|
Cores per socket. Default: 2 (=> 1 socket × 2 cores).
|
||||||
|
|
||||||
|
.PARAMETER LocaleOS
|
||||||
|
OS UI / system locale. Default: en-US.
|
||||||
|
|
||||||
|
.PARAMETER LocaleUser
|
||||||
|
User locale. Default: it-IT.
|
||||||
|
|
||||||
|
.PARAMETER InputKeyboard
|
||||||
|
Keyboard layout id. Default: 0410:00000410 (Italian).
|
||||||
|
|
||||||
|
.PARAMETER TimeZone
|
||||||
|
Windows time zone id. Default: W. Europe Standard Time.
|
||||||
|
|
||||||
|
.PARAMETER SnapshotName
|
||||||
|
Snapshot name taken at the end. Default: PostInstall.
|
||||||
|
|
||||||
|
.PARAMETER VMwareWorkstationDir
|
||||||
|
Override Workstation install dir if not at the default location.
|
||||||
|
|
||||||
|
.PARAMETER GuestPollSeconds
|
||||||
|
Seconds between polls when waiting for install_complete.flag. Default: 30.
|
||||||
|
|
||||||
|
.PARAMETER GuestPollMaxMinutes
|
||||||
|
Hard timeout waiting for install_complete.flag. Default: 90.
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Run from elevated PowerShell on the host. The VM IP detection (Step 9)
|
||||||
|
requires VMware Tools to be running in the guest — guaranteed because
|
||||||
|
Step 7 waited for the install_complete.flag dropped after Tools install.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
# ── ISOs ──
|
||||||
|
[string] $WinISO = 'F:\CI\ISO\26100.1742.240906-0331.ge_release_svc_refresh_SERVER_VOL_x64FRE_en-us.iso',
|
||||||
|
[string] $ToolsISO = 'F:\CI\ISO\VMware-tools-windows.iso',
|
||||||
|
|
||||||
|
# Cache path for the rebuilt no-prompt Windows ISO. Reused across runs when
|
||||||
|
# newer than the source. Empty string => rebuild every run inside the VM dir.
|
||||||
|
[string] $WinISONoPromptPath = '',
|
||||||
|
|
||||||
|
# ── VM identity ──
|
||||||
|
[string] $VMXPath = 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx',
|
||||||
|
[string] $VMName = 'WinBuild2025',
|
||||||
|
|
||||||
|
# ── Windows install ──
|
||||||
|
[string] $ProductKey = 'TVRH6-WHNXV-R9WG3-9XRFY-MY832',
|
||||||
|
[string] $ComputerName = 'WINBUILD-2025',
|
||||||
|
[string] $AdminUser = 'Administrator',
|
||||||
|
[string] $AdminPassword = 'WinBuild!',
|
||||||
|
[int] $ImageIndex = 2,
|
||||||
|
|
||||||
|
# ── Hardware ──
|
||||||
|
[int] $DiskSizeGB = 80,
|
||||||
|
[int] $MemoryMB = 6144,
|
||||||
|
[int] $VCPUCount = 2,
|
||||||
|
[int] $CoresPerSocket = 2,
|
||||||
|
|
||||||
|
# ── Locale ──
|
||||||
|
[string] $LocaleOS = 'en-US',
|
||||||
|
[string] $LocaleUser = 'it-IT',
|
||||||
|
[string] $InputKeyboard = '0410:00000410',
|
||||||
|
[string] $TimeZone = 'W. Europe Standard Time',
|
||||||
|
|
||||||
|
# ── Finalization ──
|
||||||
|
[string] $SnapshotName = 'PostInstall',
|
||||||
|
|
||||||
|
# ── Tooling ──
|
||||||
|
[string] $VMwareWorkstationDir = 'C:\Program Files (x86)\VMware\VMware Workstation',
|
||||||
|
|
||||||
|
# ── Polling ──
|
||||||
|
[int] $GuestPollSeconds = 30,
|
||||||
|
[int] $GuestPollMaxMinutes = 90,
|
||||||
|
|
||||||
|
# When set, vmrun powers on the VM with the Workstation GUI visible.
|
||||||
|
# Default: headless (nogui) for unattended pipelines.
|
||||||
|
[switch] $ShowGui
|
||||||
|
)
|
||||||
|
|
||||||
|
$vmrunUiMode = if ($ShowGui) { 'gui' } else { 'nogui' }
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Helpers
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function Write-Step {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "`n=== $Message ===" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-Step {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)] [string] $StepName,
|
||||||
|
[Parameter(Mandatory)] [string] $Description,
|
||||||
|
[Parameter(Mandatory)] [scriptblock] $Test
|
||||||
|
)
|
||||||
|
$ok = $false
|
||||||
|
try { $ok = [bool](& $Test) }
|
||||||
|
catch { throw "[$StepName] Validation threw on '$Description': $_" }
|
||||||
|
if (-not $ok) { throw "[$StepName] Validation failed: $Description" }
|
||||||
|
Write-Host " [OK] $Description" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# IMAPI2FS-based ISO writer. No external deps. Inline C# bridges IStream → file.
|
||||||
|
$isoWriterCs = @"
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.InteropServices.ComTypes;
|
||||||
|
public class IsoStreamWriter2 {
|
||||||
|
[DllImport("shlwapi.dll", CharSet=CharSet.Unicode, ExactSpelling=true)]
|
||||||
|
static extern int SHCreateStreamOnFileEx(string pszFile, uint grfMode, uint dwAttributes, bool fCreate, IntPtr pstmTemplate, out IStream ppstm);
|
||||||
|
|
||||||
|
public static IStream OpenFileAsStream(string path) {
|
||||||
|
IStream s;
|
||||||
|
// STGM_READ | STGM_SHARE_DENY_WRITE = 0x00000020
|
||||||
|
int hr = SHCreateStreamOnFileEx(path, 0x20, 0, false, IntPtr.Zero, out s);
|
||||||
|
if (hr != 0) throw new Exception("SHCreateStreamOnFileEx 0x" + hr.ToString("X"));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save(object streamObj, string path) {
|
||||||
|
IStream istream = streamObj as IStream;
|
||||||
|
if (istream == null) {
|
||||||
|
// Fallback: QueryInterface for IStream IID via Marshal.
|
||||||
|
IntPtr unk = Marshal.GetIUnknownForObject(streamObj);
|
||||||
|
try {
|
||||||
|
Guid iid = new Guid("0000000C-0000-0000-C000-000000000046");
|
||||||
|
IntPtr pStream;
|
||||||
|
int hr = Marshal.QueryInterface(unk, ref iid, out pStream);
|
||||||
|
if (hr != 0) throw new InvalidCastException("QueryInterface(IStream) failed: 0x" + hr.ToString("X"));
|
||||||
|
try {
|
||||||
|
istream = (IStream)Marshal.GetObjectForIUnknown(pStream);
|
||||||
|
} finally {
|
||||||
|
Marshal.Release(pStream);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Marshal.Release(unk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
using (FileStream fs = File.Open(path, FileMode.Create, FileAccess.Write)) {
|
||||||
|
byte[] buf = new byte[2048 * 16];
|
||||||
|
IntPtr pcb = Marshal.AllocHGlobal(8);
|
||||||
|
try {
|
||||||
|
int n;
|
||||||
|
do {
|
||||||
|
istream.Read(buf, buf.Length, pcb);
|
||||||
|
n = Marshal.ReadInt32(pcb);
|
||||||
|
if (n > 0) fs.Write(buf, 0, n);
|
||||||
|
} while (n > 0);
|
||||||
|
} finally {
|
||||||
|
Marshal.FreeHGlobal(pcb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
if (-not ([System.Management.Automation.PSTypeName]'IsoStreamWriter2').Type) {
|
||||||
|
Add-Type -TypeDefinition $isoWriterCs -Language CSharp
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-IsoFromFolder {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)] [string] $SourceFolder,
|
||||||
|
[Parameter(Mandatory)] [string] $IsoPath,
|
||||||
|
[Parameter(Mandatory)] [string] $VolumeLabel
|
||||||
|
)
|
||||||
|
if (Test-Path $IsoPath) { Remove-Item $IsoPath -Force }
|
||||||
|
$fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage
|
||||||
|
$result = $null
|
||||||
|
$stream = $null
|
||||||
|
try {
|
||||||
|
$fsi.FileSystemsToCreate = 7 # ISO9660 | Joliet | UDF
|
||||||
|
$fsi.VolumeName = $VolumeLabel
|
||||||
|
$fsi.ChooseImageDefaultsForMediaType(13) # IMAPI_MEDIA_TYPE_DISK
|
||||||
|
$fsi.Root.AddTree($SourceFolder, $false)
|
||||||
|
$result = $fsi.CreateResultImage()
|
||||||
|
$stream = $result.ImageStream
|
||||||
|
[IsoStreamWriter2]::Save($stream, $IsoPath)
|
||||||
|
} finally {
|
||||||
|
# Release in reverse order. ImageStream holds file handles to source
|
||||||
|
# files added via AddTree — must be released before the source folder
|
||||||
|
# can be deleted.
|
||||||
|
if ($stream) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($stream) | Out-Null }
|
||||||
|
if ($result) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($result) | Out-Null }
|
||||||
|
[System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($fsi) | Out-Null
|
||||||
|
[GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-WinIsoNoPrompt {
|
||||||
|
<#
|
||||||
|
Rebuilds a Windows install ISO replacing the EFI boot image with
|
||||||
|
efisys_noprompt.bin (extracted from the source ISO itself).
|
||||||
|
Result: UEFI boot proceeds straight into Windows Setup, no
|
||||||
|
"Press any key to boot from CD" prompt.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)] [string] $SourceIsoPath,
|
||||||
|
[Parameter(Mandatory)] [string] $OutputIsoPath
|
||||||
|
)
|
||||||
|
if (Test-Path $OutputIsoPath) { Remove-Item $OutputIsoPath -Force }
|
||||||
|
|
||||||
|
$mount = Mount-DiskImage -ImagePath $SourceIsoPath -PassThru -StorageType ISO -Access ReadOnly
|
||||||
|
try {
|
||||||
|
$vol = $mount | Get-Volume
|
||||||
|
$letter = "$($vol.DriveLetter):\"
|
||||||
|
$bootImg = Join-Path $letter 'efi\microsoft\boot\efisys_noprompt.bin'
|
||||||
|
if (-not (Test-Path $bootImg)) {
|
||||||
|
throw "efisys_noprompt.bin not present in source ISO at $bootImg"
|
||||||
|
}
|
||||||
|
$label = $vol.FileSystemLabel
|
||||||
|
if ([string]::IsNullOrWhiteSpace($label)) { $label = 'WINSERVER' }
|
||||||
|
if ($label.Length -gt 32) { $label = $label.Substring(0,32) }
|
||||||
|
|
||||||
|
$fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage
|
||||||
|
try {
|
||||||
|
$fsi.FileSystemsToCreate = 7 # ISO9660 | Joliet | UDF
|
||||||
|
$fsi.VolumeName = $label
|
||||||
|
$fsi.ChooseImageDefaultsForMediaType(13)
|
||||||
|
# 5+ GB ISO needs explicit large-volume defaults if available.
|
||||||
|
try { $fsi.UDFRevision = 0x102 } catch {}
|
||||||
|
|
||||||
|
Write-Host " Adding ISO tree (this takes a few minutes for 5+ GB)..." -ForegroundColor Yellow
|
||||||
|
$fsi.Root.AddTree($letter, $false)
|
||||||
|
|
||||||
|
$bootStream = [IsoStreamWriter2]::OpenFileAsStream($bootImg)
|
||||||
|
$bootOpts = New-Object -ComObject IMAPI2FS.BootOptions
|
||||||
|
$bootOpts.AssignBootImage($bootStream)
|
||||||
|
$bootOpts.Emulation = 0 # IMAPI_BOOT_EMULATION_TYPE_NONE
|
||||||
|
$bootOpts.PlatformId = 0xEF # EFI
|
||||||
|
$bootOpts.Manufacturer = 'Microsoft'
|
||||||
|
|
||||||
|
# Single-element COM array for BootImageOptionsArray
|
||||||
|
$arr = New-Object 'object[]' 1
|
||||||
|
$arr[0] = $bootOpts
|
||||||
|
try { $fsi.BootImageOptionsArray = $arr }
|
||||||
|
catch { $fsi.BootImageOptions = $bootOpts } # fallback older IMAPI2FS
|
||||||
|
|
||||||
|
Write-Host " Building output ISO..." -ForegroundColor Yellow
|
||||||
|
$result = $fsi.CreateResultImage()
|
||||||
|
$stream = $result.ImageStream
|
||||||
|
[IsoStreamWriter2]::Save($stream, $OutputIsoPath)
|
||||||
|
} finally {
|
||||||
|
if ($stream) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($stream) | Out-Null }
|
||||||
|
if ($result) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($result) | Out-Null }
|
||||||
|
if ($bootStream) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($bootStream) | Out-Null }
|
||||||
|
if ($bootOpts) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($bootOpts) | Out-Null }
|
||||||
|
[System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($fsi) | Out-Null
|
||||||
|
[GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Dismount-DiskImage -ImagePath $SourceIsoPath -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Vmrun {
|
||||||
|
param([Parameter(Mandatory)] [string[]] $Arguments,
|
||||||
|
[switch] $IgnoreErrors)
|
||||||
|
$vmrun = Join-Path $VMwareWorkstationDir 'vmrun.exe'
|
||||||
|
$stdout = & $vmrun @Arguments 2>&1
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
if ($code -ne 0 -and -not $IgnoreErrors) {
|
||||||
|
throw "vmrun $($Arguments -join ' ') failed (exit $code): $stdout"
|
||||||
|
}
|
||||||
|
return $stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-VmxKey {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)] [string] $VmxPath,
|
||||||
|
[Parameter(Mandatory)] [string] $Key,
|
||||||
|
[Parameter(Mandatory)] [string] $Value
|
||||||
|
)
|
||||||
|
$lines = Get-Content -LiteralPath $VmxPath
|
||||||
|
$pattern = "^\s*$([regex]::Escape($Key))\s*="
|
||||||
|
$found = $false
|
||||||
|
$newLines = foreach ($line in $lines) {
|
||||||
|
if ($line -match $pattern) { $found = $true; "$Key = `"$Value`"" }
|
||||||
|
else { $line }
|
||||||
|
}
|
||||||
|
if (-not $found) { $newLines = @($newLines) + "$Key = `"$Value`"" }
|
||||||
|
Set-Content -LiteralPath $VmxPath -Value $newLines -Encoding ASCII
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 1: Pre-flight validation
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step 'Step 1: pre-flight validation'
|
||||||
|
|
||||||
|
$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'
|
||||||
|
|
||||||
|
Assert-Step 'PreFlight' "vmrun.exe found at $vmrunPath" { Test-Path $vmrunPath }
|
||||||
|
Assert-Step 'PreFlight' "vmware-vdiskmanager.exe found at $vdiskMgr" { Test-Path $vdiskMgr }
|
||||||
|
Assert-Step 'PreFlight' "Windows ISO exists: $WinISO" { Test-Path $WinISO }
|
||||||
|
Assert-Step 'PreFlight' "VMware Tools ISO exists: $ToolsISO" { Test-Path $ToolsISO }
|
||||||
|
Assert-Step 'PreFlight' "autounattend template exists: $xmlTemplate" { Test-Path $xmlTemplate }
|
||||||
|
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'
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 2: render autounattend.xml + post-install.ps1
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step 'Step 2: render autounattend.xml + post-install.ps1'
|
||||||
|
|
||||||
|
if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force }
|
||||||
|
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
|
||||||
|
|
||||||
|
# autounattend.xml
|
||||||
|
$xmlText = Get-Content -LiteralPath $xmlTemplate -Raw
|
||||||
|
$xmlSubs = @{
|
||||||
|
'{{LOCALE_OS}}' = $LocaleOS
|
||||||
|
'{{LOCALE_USER}}' = $LocaleUser
|
||||||
|
'{{KEYBOARD}}' = $InputKeyboard
|
||||||
|
'{{IMAGE_INDEX}}' = "$ImageIndex"
|
||||||
|
'{{PRODUCT_KEY}}' = $ProductKey
|
||||||
|
'{{COMPUTER_NAME}}' = $ComputerName
|
||||||
|
'{{TIMEZONE}}' = $TimeZone
|
||||||
|
'{{ADMIN_USER}}' = $AdminUser
|
||||||
|
'{{ADMIN_PWD}}' = $AdminPassword
|
||||||
|
}
|
||||||
|
foreach ($k in $xmlSubs.Keys) { $xmlText = $xmlText.Replace($k, $xmlSubs[$k]) }
|
||||||
|
$xmlOut = Join-Path $stagingDir 'autounattend.xml'
|
||||||
|
Set-Content -LiteralPath $xmlOut -Value $xmlText -Encoding UTF8
|
||||||
|
Assert-Step 'Render' 'autounattend.xml written' { Test-Path $xmlOut }
|
||||||
|
Assert-Step 'Render' 'no placeholders left in xml' { -not (Select-String -Path $xmlOut -Pattern '\{\{[^}]+\}\}' -Quiet) }
|
||||||
|
|
||||||
|
# post-install.ps1 — drops into autounattend ISO root, runs at FirstLogonCommands.
|
||||||
|
# Hardening + WinRM + Tools install + flag file + reboot. All paths are absolute.
|
||||||
|
$postInstall = @"
|
||||||
|
# Auto-generated by Deploy-WinBuild2025.ps1 — runs at first logon as Administrator.
|
||||||
|
`$ErrorActionPreference = 'Continue'
|
||||||
|
`$log = 'C:\Windows\Temp\post-install.log'
|
||||||
|
function L(`$m){ "`$(Get-Date -f s) `$m" | Tee-Object -FilePath `$log -Append }
|
||||||
|
|
||||||
|
L 'post-install start'
|
||||||
|
|
||||||
|
# ── KICK OFF VMware Tools install (async — wrapper detaches MSI child) ────
|
||||||
|
# Started first so the MSI runs in the background while hardening tweaks
|
||||||
|
# below execute. Settled state verified at the very end before reboot.
|
||||||
|
`$selfDir = Split-Path -Parent `$MyInvocation.MyCommand.Path
|
||||||
|
`$toolsExe = `$null
|
||||||
|
foreach (`$name in 'setup64.exe','setup.exe') {
|
||||||
|
`$cand = Join-Path `$selfDir "Tools\`$name"
|
||||||
|
if (Test-Path `$cand) { `$toolsExe = `$cand; break }
|
||||||
|
}
|
||||||
|
if (-not `$toolsExe) {
|
||||||
|
L 'Bundled Tools not found, scanning CD drives...'
|
||||||
|
foreach (`$drv in (Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=5')) {
|
||||||
|
foreach (`$name in 'setup64.exe','setup.exe') {
|
||||||
|
`$cand = Join-Path (`$drv.DeviceID + '\') `$name
|
||||||
|
if (Test-Path `$cand) { `$toolsExe = `$cand; break }
|
||||||
|
}
|
||||||
|
if (`$toolsExe) { break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
L "Tools setup resolved to: `$toolsExe"
|
||||||
|
|
||||||
|
`$localRoot = 'C:\Windows\Temp\VMwareTools'
|
||||||
|
`$msiLog = 'C:\Windows\Temp\vmtools-msi.log'
|
||||||
|
`$exitCode = -1
|
||||||
|
if (`$toolsExe -and (Test-Path `$toolsExe)) {
|
||||||
|
# Copy payload to local writable disk — wrapper extract from CD fails at
|
||||||
|
# FirstLogon phase due to user profile %TEMP% ACL bootstrap timing.
|
||||||
|
`$srcRoot = Split-Path -Parent `$toolsExe
|
||||||
|
if (Test-Path `$localRoot) { Remove-Item `$localRoot -Recurse -Force }
|
||||||
|
L "Copying Tools payload `$srcRoot -> `$localRoot"
|
||||||
|
Copy-Item -Path `$srcRoot -Destination `$localRoot -Recurse -Force
|
||||||
|
|
||||||
|
# PS argument parser strips inner quotes from /v"..."; --% stop-parsing
|
||||||
|
# token passes the tail literally to the EXE.
|
||||||
|
L "Tools cmd: .\setup.exe /s /v`"/qn REBOOT=R /l*v `$msiLog`""
|
||||||
|
Push-Location `$localRoot
|
||||||
|
try {
|
||||||
|
& .\setup.exe --% /s /v"/qn REBOOT=R /l*v C:\Windows\Temp\vmtools-msi.log"
|
||||||
|
`$exitCode = `$LASTEXITCODE
|
||||||
|
} finally { Pop-Location }
|
||||||
|
L "VMware Tools wrapper exit code: `$exitCode (MSI continues async)"
|
||||||
|
} else {
|
||||||
|
L 'ERROR: VMware Tools setup not found (bundled or on any CD-ROM)'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── UAC off (admin script automation) ─────────────────────────────────────
|
||||||
|
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' /v EnableLUA /t REG_DWORD /d 0 /f | Out-Null
|
||||||
|
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
L 'UAC disabled'
|
||||||
|
|
||||||
|
# ── Defender disabled (lab only) — Setup-WinBuild2025 skips Defender step ─
|
||||||
|
# RTP off + tamper-protect GPO. With Defender fully off, exclusion paths
|
||||||
|
# become moot (no scanner to exclude from), so Setup-WinBuild2025 has been
|
||||||
|
# stripped of its Defender step.
|
||||||
|
try { Set-MpPreference -DisableRealtimeMonitoring `$true -ErrorAction Stop; L 'Defender RTP off' }
|
||||||
|
catch { L "Defender RTP off failed: `$(`$_.Exception.Message)" }
|
||||||
|
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows Defender' /v DisableAntiSpyware /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
|
||||||
|
# ── Telemetry / DiagTrack off ─────────────────────────────────────────────
|
||||||
|
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection' /v AllowTelemetry /t REG_DWORD /d 0 /f | Out-Null
|
||||||
|
sc.exe config DiagTrack start= disabled | Out-Null
|
||||||
|
sc.exe stop DiagTrack | Out-Null
|
||||||
|
L 'Telemetry disabled'
|
||||||
|
|
||||||
|
# ── Windows Update services disabled ──────────────────────────────────────
|
||||||
|
foreach (`$svc in 'wuauserv','UsoSvc','WaaSMedicSvc') {
|
||||||
|
sc.exe config `$svc start= disabled | Out-Null
|
||||||
|
sc.exe stop `$svc | Out-Null
|
||||||
|
}
|
||||||
|
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' /v NoAutoUpdate /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
L 'Windows Update disabled'
|
||||||
|
|
||||||
|
# ── Firewall: profiles stay default; allow RDP + ICMP + WinRM HTTPS ──────
|
||||||
|
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server' /v fDenyTSConnections /t REG_DWORD /d 0 /f | Out-Null
|
||||||
|
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' /v UserAuthentication /t REG_DWORD /d 0 /f | Out-Null
|
||||||
|
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue
|
||||||
|
New-NetFirewallRule -DisplayName 'ICMPv4-In' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
L 'RDP + ICMP allowed'
|
||||||
|
|
||||||
|
# ── WinRM HTTP + HTTPS, Basic+Unencrypted, TrustedHosts=* (lab only) ─────
|
||||||
|
Enable-PSRemoting -Force -SkipNetworkProfileCheck | Out-Null
|
||||||
|
winrm set winrm/config/service '@{AllowUnencrypted="true"}' | Out-Null
|
||||||
|
winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null
|
||||||
|
winrm set winrm/config/client/auth '@{Basic="true"}' | Out-Null
|
||||||
|
winrm set winrm/config/client '@{TrustedHosts="*"}' | Out-Null
|
||||||
|
try {
|
||||||
|
`$cert = New-SelfSignedCertificate -DnsName `$env:COMPUTERNAME -CertStoreLocation Cert:\LocalMachine\My
|
||||||
|
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint `$cert.Thumbprint -Force | Out-Null
|
||||||
|
New-NetFirewallRule -DisplayName 'WinRM-HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Any | Out-Null
|
||||||
|
L 'WinRM HTTPS listener configured'
|
||||||
|
} catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" }
|
||||||
|
|
||||||
|
# ── Server Manager / WAC popup / first-logon UX ──────────────────────────
|
||||||
|
# Server Manager auto-start off — machine-wide + current Administrator + Default
|
||||||
|
# profile hive (so future cloned/sysprep'd users inherit).
|
||||||
|
reg add 'HKLM\SOFTWARE\Microsoft\ServerManager' /v DoNotOpenServerManagerAtLogon /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
reg add 'HKCU\SOFTWARE\Microsoft\ServerManager' /v DoNotOpenServerManagerAtLogon /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
reg load HKU\DefaultUser 'C:\Users\Default\NTUSER.DAT' 2>&1 | Out-Null
|
||||||
|
reg add 'HKU\DefaultUser\SOFTWARE\Microsoft\ServerManager' /v DoNotOpenServerManagerAtLogon /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
reg unload HKU\DefaultUser 2>&1 | Out-Null
|
||||||
|
# Windows Admin Center prompt at SM launch off
|
||||||
|
reg add 'HKLM\SOFTWARE\Microsoft\ServerManager' /v DoNotPopWACConsoleAtSMLaunch /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
# DisableCAD — no Ctrl+Alt+Del at logon (CI VMs never need it)
|
||||||
|
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' /v DisableCAD /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
# OOBE privacy experience suppressed
|
||||||
|
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null
|
||||||
|
L 'Server Manager + WAC + CAD + OOBE prompts disabled'
|
||||||
|
|
||||||
|
# ── IE Enhanced Security Configuration off (Admin) ───────────────────────
|
||||||
|
# Long-known CI papercut: ESC blocks every URL in admin sessions.
|
||||||
|
reg add 'HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}' /v IsInstalled /t REG_DWORD /d 0 /f | Out-Null
|
||||||
|
reg add 'HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}' /v IsInstalled /t REG_DWORD /d 0 /f | Out-Null
|
||||||
|
Stop-Process -Name iexplore -ErrorAction SilentlyContinue
|
||||||
|
L 'IE ESC disabled'
|
||||||
|
|
||||||
|
# ── 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
|
||||||
|
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c 2>&1 | Out-Null
|
||||||
|
powercfg /change monitor-timeout-ac 0 | Out-Null
|
||||||
|
powercfg /change disk-timeout-ac 0 | Out-Null
|
||||||
|
powercfg /change standby-timeout-ac 0 | Out-Null
|
||||||
|
powercfg /change hibernate-timeout-ac 0 | Out-Null
|
||||||
|
L 'Hibernate off, power plan High Performance'
|
||||||
|
|
||||||
|
# ── Unneeded services off (CI build VM, not a print/search/file server) ──
|
||||||
|
foreach (`$svc in 'WSearch','Spooler','SysMain','WerSvc','RemoteRegistry','Fax','MapsBroker','RetailDemo','XblAuthManager','XblGameSave','XboxNetApiSvc','XboxGipSvc') {
|
||||||
|
`$s = Get-Service -Name `$svc -ErrorAction SilentlyContinue
|
||||||
|
if (`$s) {
|
||||||
|
sc.exe config `$svc start= disabled | Out-Null
|
||||||
|
sc.exe stop `$svc | Out-Null
|
||||||
|
L " service disabled: `$svc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Windows Customer Experience tasks off ────────────────────────────────
|
||||||
|
foreach (`$task in @(
|
||||||
|
'\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser',
|
||||||
|
'\Microsoft\Windows\Application Experience\ProgramDataUpdater',
|
||||||
|
'\Microsoft\Windows\Autochk\Proxy',
|
||||||
|
'\Microsoft\Windows\Customer Experience Improvement Program\Consolidator',
|
||||||
|
'\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip',
|
||||||
|
'\Microsoft\Windows\Feedback\Siuf\DmClient',
|
||||||
|
'\Microsoft\Windows\Feedback\Siuf\DmClientOnScenarioDownload'
|
||||||
|
)) {
|
||||||
|
schtasks /Change /TN `$task /Disable 2>&1 | Out-Null
|
||||||
|
}
|
||||||
|
L 'CEIP / feedback scheduled tasks 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)'
|
||||||
|
|
||||||
|
# ── 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)
|
||||||
|
# (b) vmtoolsd.exe on disk
|
||||||
|
# (c) VMTools service Running (not just registered — install finalized)
|
||||||
|
# Hard timeout 15 min.
|
||||||
|
if (`$toolsExe) {
|
||||||
|
`$installDeadline = (Get-Date).AddMinutes(15)
|
||||||
|
while ((Get-Date) -lt `$installDeadline) {
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
`$busy = Get-Process -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { `$_.ProcessName -in 'setup','setup64','VMware-Tools','VMware-Tools-Installer','vminst' }
|
||||||
|
`$vmtoolsd = Test-Path 'C:\Program Files\VMware\VMware Tools\vmtoolsd.exe'
|
||||||
|
`$svc = Get-Service -Name VMTools -ErrorAction SilentlyContinue
|
||||||
|
`$svcRunning = (`$svc -and `$svc.Status -eq 'Running')
|
||||||
|
L " install poll: busy=`$(`$busy.Count) vmtoolsd=`$vmtoolsd svc=`$(if (`$svc) { `$svc.Status } else { 'absent' })"
|
||||||
|
if (-not `$busy -and `$vmtoolsd -and `$svcRunning) { L 'Tools install settled.'; break }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path `$msiLog) {
|
||||||
|
L "---- MSI log tail (last 30 lines) ----"
|
||||||
|
Get-Content `$msiLog -Tail 30 | ForEach-Object { L `$_ }
|
||||||
|
L '---- end MSI log ----'
|
||||||
|
} else {
|
||||||
|
L "MSI log NOT created at `$msiLog (wrapper failed before MSI launched)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path 'C:\Program Files\VMware\VMware Tools\vmtoolsd.exe') {
|
||||||
|
L 'vmtoolsd.exe present -> Tools installed OK'
|
||||||
|
} else {
|
||||||
|
L "WARNING: vmtoolsd.exe NOT present -> install failed (wrapper exit `$exitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Drop completion flag (host polls for this via vmrun) ──────────────────
|
||||||
|
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"
|
||||||
|
"@
|
||||||
|
$psOut = Join-Path $stagingDir 'post-install.ps1'
|
||||||
|
Set-Content -LiteralPath $psOut -Value $postInstall -Encoding UTF8
|
||||||
|
Assert-Step 'Render' 'post-install.ps1 written' { Test-Path $psOut }
|
||||||
|
|
||||||
|
# Bundle VMware Tools contents under autounattend\Tools\ so post-install.ps1
|
||||||
|
# always finds setup64.exe on the same CD it boots from. Avoids the
|
||||||
|
# "third SATA CD-ROM disconnected at first logon" failure mode.
|
||||||
|
Write-Host ' Extracting VMware Tools ISO into autounattend staging...' -ForegroundColor Yellow
|
||||||
|
$toolsStage = Join-Path $stagingDir 'Tools'
|
||||||
|
New-Item -ItemType Directory -Path $toolsStage -Force | Out-Null
|
||||||
|
$toolsMount = Mount-DiskImage -ImagePath $ToolsISO -PassThru -Access ReadOnly
|
||||||
|
try {
|
||||||
|
$toolsLetter = "$(($toolsMount | Get-Volume).DriveLetter):\"
|
||||||
|
Copy-Item -Path (Join-Path $toolsLetter '*') -Destination $toolsStage -Recurse -Force
|
||||||
|
} finally {
|
||||||
|
Dismount-DiskImage -ImagePath $ToolsISO -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
}
|
||||||
|
Assert-Step 'Render' 'Tools setup bundled in staging\Tools' {
|
||||||
|
(Test-Path (Join-Path $toolsStage 'setup.exe')) -or
|
||||||
|
(Test-Path (Join-Path $toolsStage 'setup64.exe'))
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 3: build autounattend ISO
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step 'Step 3: build autounattend ISO via IMAPI2FS'
|
||||||
|
|
||||||
|
New-IsoFromFolder -SourceFolder $stagingDir -IsoPath $autounattendIso -VolumeLabel 'AUTOUNATTEND'
|
||||||
|
Assert-Step 'IsoBuild' "autounattend.iso created at $autounattendIso" { Test-Path $autounattendIso }
|
||||||
|
Assert-Step 'IsoBuild' 'autounattend.iso non-empty' { (Get-Item $autounattendIso).Length -gt 4096 }
|
||||||
|
|
||||||
|
# Cleanup staging — only ISO matters from here on. IMAPI2FS may hold a
|
||||||
|
# transient lock on bundled binaries; force GC then retry, then ignore.
|
||||||
|
[GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect()
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
if (Test-Path $stagingDir) {
|
||||||
|
Write-Host " [warn] staging dir not fully cleaned, leaving in place: $stagingDir" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 4: create VMDK
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Step 4: create $DiskSizeGB GB VMDK (single-file growable)"
|
||||||
|
|
||||||
|
# -t 0 = single growable file (thin); -a lsilogic affects descriptor only,
|
||||||
|
# the disk attaches to the NVMe controller defined in the VMX.
|
||||||
|
$vdiskArgs = @('-c', '-s', "${DiskSizeGB}GB", '-a', 'lsilogic', '-t', '0', $vmdkPath)
|
||||||
|
& $vdiskMgr @vdiskArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "vmware-vdiskmanager failed (exit $LASTEXITCODE)" }
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
$srcMtime = (Get-Item $WinISO).LastWriteTime
|
||||||
|
$rebuild = $true
|
||||||
|
if (Test-Path $WinISONoPromptPath) {
|
||||||
|
if ((Get-Item $WinISONoPromptPath).LastWriteTime -ge $srcMtime) {
|
||||||
|
Write-Host " Cached no-prompt ISO is current, reusing: $WinISONoPromptPath" -ForegroundColor Green
|
||||||
|
$rebuild = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($rebuild) {
|
||||||
|
Write-Host " Building no-prompt ISO from $WinISO" -ForegroundColor Yellow
|
||||||
|
Write-Host " Output: $WinISONoPromptPath" -ForegroundColor Yellow
|
||||||
|
New-WinIsoNoPrompt -SourceIsoPath $WinISO -OutputIsoPath $WinISONoPromptPath
|
||||||
|
}
|
||||||
|
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 }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 5: generate VMX
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step 'Step 5: generate VMX'
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
$vmxLines = @(
|
||||||
|
'.encoding = "windows-1252"',
|
||||||
|
'config.version = "8"',
|
||||||
|
'virtualHW.version = "21"',
|
||||||
|
"displayName = `"$VMName`"",
|
||||||
|
"guestOS = `"$guestOS`"",
|
||||||
|
'firmware = "efi"',
|
||||||
|
'uefi.secureBoot.enabled = "FALSE"',
|
||||||
|
"memSize = `"$MemoryMB`"",
|
||||||
|
"numvcpus = `"$VCPUCount`"",
|
||||||
|
"cpuid.coresPerSocket = `"$CoresPerSocket`"",
|
||||||
|
'tools.syncTime = "TRUE"',
|
||||||
|
'tools.upgrade.policy = "manual"',
|
||||||
|
'powerType.powerOff = "soft"',
|
||||||
|
'powerType.powerOn = "soft"',
|
||||||
|
'powerType.suspend = "soft"',
|
||||||
|
'powerType.reset = "soft"',
|
||||||
|
'uuid.action = "create"',
|
||||||
|
'',
|
||||||
|
'# PCIe root ports — required for explicit PCI slot assignments below.',
|
||||||
|
'# Each pcieRootPort exposes 8 slots → enough headroom for NVMe + SATA + NIC.',
|
||||||
|
'pciBridge0.present = "TRUE"',
|
||||||
|
'pciBridge4.present = "TRUE"',
|
||||||
|
'pciBridge4.virtualDev = "pcieRootPort"',
|
||||||
|
'pciBridge4.functions = "8"',
|
||||||
|
'pciBridge5.present = "TRUE"',
|
||||||
|
'pciBridge5.virtualDev = "pcieRootPort"',
|
||||||
|
'pciBridge5.functions = "8"',
|
||||||
|
'pciBridge6.present = "TRUE"',
|
||||||
|
'pciBridge6.virtualDev = "pcieRootPort"',
|
||||||
|
'pciBridge6.functions = "8"',
|
||||||
|
'pciBridge7.present = "TRUE"',
|
||||||
|
'pciBridge7.virtualDev = "pcieRootPort"',
|
||||||
|
'pciBridge7.functions = "8"',
|
||||||
|
'',
|
||||||
|
'# NVMe controller + 80 GB system disk',
|
||||||
|
'nvme0.present = "TRUE"',
|
||||||
|
'nvme0.pciSlotNumber = "224"',
|
||||||
|
'nvme0:0.present = "TRUE"',
|
||||||
|
"nvme0:0.fileName = `"$vmdkName`"",
|
||||||
|
'nvme0:0.deviceType = "disk"',
|
||||||
|
'',
|
||||||
|
'# Three SATA CD-ROMs: install / autounattend / Tools',
|
||||||
|
'sata0.present = "TRUE"',
|
||||||
|
'sata0.pciSlotNumber = "32"',
|
||||||
|
'sata0:0.present = "TRUE"',
|
||||||
|
'sata0:0.deviceType = "cdrom-image"',
|
||||||
|
"sata0:0.fileName = `"$WinISONoPromptPath`"",
|
||||||
|
'sata0:0.startConnected = "TRUE"',
|
||||||
|
'sata0:1.present = "TRUE"',
|
||||||
|
'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"',
|
||||||
|
'ethernet0.virtualDev = "e1000e"',
|
||||||
|
'ethernet0.connectionType = "nat"',
|
||||||
|
'ethernet0.addressType = "generated"',
|
||||||
|
'ethernet0.startConnected = "TRUE"',
|
||||||
|
'ethernet0.pciSlotNumber = "192"',
|
||||||
|
'',
|
||||||
|
'# USB / video',
|
||||||
|
'usb.present = "TRUE"',
|
||||||
|
'usb.pciSlotNumber = "34"',
|
||||||
|
'ehci.present = "TRUE"',
|
||||||
|
'ehci.pciSlotNumber = "35"',
|
||||||
|
'usb_xhci.present = "TRUE"',
|
||||||
|
'usb_xhci.pciSlotNumber = "33"',
|
||||||
|
'svga.present = "TRUE"',
|
||||||
|
'svga.graphicsMemoryKB = "262144"',
|
||||||
|
'',
|
||||||
|
'# No floppy / no sound (CI VM)',
|
||||||
|
'floppy0.present = "FALSE"',
|
||||||
|
'sound.present = "FALSE"',
|
||||||
|
'',
|
||||||
|
'# Misc',
|
||||||
|
'vmci0.present = "TRUE"',
|
||||||
|
'vmci0.pciSlotNumber = "36"',
|
||||||
|
'hpet0.present = "TRUE"',
|
||||||
|
'mks.enable3d = "FALSE"',
|
||||||
|
'snapshot.action = "keep"'
|
||||||
|
)
|
||||||
|
Set-Content -LiteralPath $VMXPath -Value $vmxLines -Encoding ASCII
|
||||||
|
Assert-Step 'Vmx' "VMX written: $VMXPath" { Test-Path $VMXPath }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 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) }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 7: wait for install_complete.flag inside guest
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Step 7: wait for guest install_complete.flag (max ${GuestPollMaxMinutes} min)"
|
||||||
|
|
||||||
|
$deadline = (Get-Date).AddMinutes($GuestPollMaxMinutes)
|
||||||
|
$flagPath = 'C:\Windows\Temp\install_complete.flag'
|
||||||
|
$flagSeen = $false
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
Start-Sleep -Seconds $GuestPollSeconds
|
||||||
|
$toolsState = Invoke-Vmrun -Arguments @('-T','ws','checkToolsState',$VMXPath) -IgnoreErrors
|
||||||
|
Write-Host (" [{0:HH:mm:ss}] toolsState={1}" -f (Get-Date), ($toolsState -join ' '))
|
||||||
|
if ($toolsState -notmatch 'running') { continue }
|
||||||
|
|
||||||
|
# Tools running → can probe filesystem with guest creds.
|
||||||
|
$probe = & $vmrunPath -T ws -gu $AdminUser -gp $AdminPassword `
|
||||||
|
fileExistsInGuest $VMXPath $flagPath 2>&1
|
||||||
|
Write-Host (" fileExistsInGuest -> {0}" -f ($probe -join ' '))
|
||||||
|
if ($probe -match 'The file exists') { $flagSeen = $true; break }
|
||||||
|
}
|
||||||
|
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'
|
||||||
|
|
||||||
|
Invoke-Vmrun -Arguments @('-T','ws','stop',$VMXPath,'soft') -IgnoreErrors | Out-Null
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-VmxKey -VmxPath $VMXPath -Key 'ethernet0.virtualDev' -Value 'vmxnet3'
|
||||||
|
Assert-Step 'NicSwap' 'ethernet0.virtualDev = vmxnet3' {
|
||||||
|
(Get-Content -LiteralPath $VMXPath) -match '^\s*ethernet0\.virtualDev\s*=\s*"vmxnet3"\s*$'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 9: power on, verify vmxnet3 + DHCP IP
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step 'Step 9: power on with vmxnet3, verify DHCP IP'
|
||||||
|
|
||||||
|
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null
|
||||||
|
$ipDeadline = (Get-Date).AddMinutes(10)
|
||||||
|
$guestIP = $null
|
||||||
|
do {
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
$guestIP = Invoke-Vmrun -Arguments @('-T','ws','getGuestIPAddress',$VMXPath,'-wait') -IgnoreErrors
|
||||||
|
Write-Host " guestIP probe -> $guestIP"
|
||||||
|
} 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+' }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 10: shutdown → snapshot (cold) → power on
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Step 10: graceful shutdown, snapshot '$SnapshotName' (cold), power on"
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
$ipDeadline = (Get-Date).AddMinutes(5)
|
||||||
|
do {
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
$guestIP = Invoke-Vmrun -Arguments @('-T','ws','getGuestIPAddress',$VMXPath,'-wait') -IgnoreErrors
|
||||||
|
} 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+' }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Final summary
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step 'Done'
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " VMX : $VMXPath"
|
||||||
|
Write-Host " VMDK : $vmdkPath"
|
||||||
|
Write-Host " Hostname : $ComputerName"
|
||||||
|
Write-Host " Guest IP : $guestIP"
|
||||||
|
Write-Host " Admin user : $AdminUser"
|
||||||
|
Write-Host " Snapshot : $SnapshotName"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " RDP : mstsc /v:$guestIP"
|
||||||
|
Write-Host " WinRM HTTP : 5985"
|
||||||
|
Write-Host " WinRM HTTPS : 5986 (self-signed)"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " VM is powered on with vmxnet3 + DHCP. Live snapshot taken." -ForegroundColor Green
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Windows Server 2025 unattended install template.
|
||||||
|
Double-brace placeholders are substituted by Deploy-WinBuild2025.ps1.
|
||||||
|
|
||||||
|
Boot NIC is e1000e (broad compat). VMware Tools install brings
|
||||||
|
vmxnet3 driver; the host script switches the VMX to vmxnet3
|
||||||
|
after Tools install completes.
|
||||||
|
|
||||||
|
Disk layout (UEFI/GPT, 80 GB):
|
||||||
|
Part 1: EFI 100 MB FAT32 S:
|
||||||
|
Part 2: MSR 16 MB (no fmt)
|
||||||
|
Part 3: Primary rest NTFS C:
|
||||||
|
No Recovery partition (per user choice).
|
||||||
|
|
||||||
|
Image index 2 = "Windows Server 2025 SERVERSTANDARD" (Desktop Experience)
|
||||||
|
on the VOL ISO. Verify with:
|
||||||
|
dism /Get-WimInfo /WimFile:D:\sources\install.wim
|
||||||
|
-->
|
||||||
|
<unattend xmlns="urn:schemas-microsoft-com:unattend"
|
||||||
|
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
|
||||||
|
<settings pass="windowsPE">
|
||||||
|
<component name="Microsoft-Windows-International-Core-WinPE"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<SetupUILanguage>
|
||||||
|
<UILanguage>{{LOCALE_OS}}</UILanguage>
|
||||||
|
</SetupUILanguage>
|
||||||
|
<InputLocale>{{KEYBOARD}}</InputLocale>
|
||||||
|
<SystemLocale>{{LOCALE_OS}}</SystemLocale>
|
||||||
|
<UILanguage>{{LOCALE_OS}}</UILanguage>
|
||||||
|
<UILanguageFallback>{{LOCALE_OS}}</UILanguageFallback>
|
||||||
|
<UserLocale>{{LOCALE_USER}}</UserLocale>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<component name="Microsoft-Windows-Setup"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<DiskConfiguration>
|
||||||
|
<Disk wcm:action="add">
|
||||||
|
<DiskID>0</DiskID>
|
||||||
|
<WillWipeDisk>true</WillWipeDisk>
|
||||||
|
<CreatePartitions>
|
||||||
|
<CreatePartition wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<Type>EFI</Type>
|
||||||
|
<Size>100</Size>
|
||||||
|
</CreatePartition>
|
||||||
|
<CreatePartition wcm:action="add">
|
||||||
|
<Order>2</Order>
|
||||||
|
<Type>MSR</Type>
|
||||||
|
<Size>16</Size>
|
||||||
|
</CreatePartition>
|
||||||
|
<CreatePartition wcm:action="add">
|
||||||
|
<Order>3</Order>
|
||||||
|
<Type>Primary</Type>
|
||||||
|
<Extend>true</Extend>
|
||||||
|
</CreatePartition>
|
||||||
|
</CreatePartitions>
|
||||||
|
<ModifyPartitions>
|
||||||
|
<ModifyPartition wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<PartitionID>1</PartitionID>
|
||||||
|
<Format>FAT32</Format>
|
||||||
|
<Label>System</Label>
|
||||||
|
<Letter>S</Letter>
|
||||||
|
</ModifyPartition>
|
||||||
|
<ModifyPartition wcm:action="add">
|
||||||
|
<Order>2</Order>
|
||||||
|
<PartitionID>2</PartitionID>
|
||||||
|
</ModifyPartition>
|
||||||
|
<ModifyPartition wcm:action="add">
|
||||||
|
<Order>3</Order>
|
||||||
|
<PartitionID>3</PartitionID>
|
||||||
|
<Format>NTFS</Format>
|
||||||
|
<Label>Windows</Label>
|
||||||
|
<Letter>C</Letter>
|
||||||
|
</ModifyPartition>
|
||||||
|
</ModifyPartitions>
|
||||||
|
</Disk>
|
||||||
|
</DiskConfiguration>
|
||||||
|
|
||||||
|
<ImageInstall>
|
||||||
|
<OSImage>
|
||||||
|
<InstallFrom>
|
||||||
|
<MetaData wcm:action="add">
|
||||||
|
<Key>/IMAGE/INDEX</Key>
|
||||||
|
<Value>{{IMAGE_INDEX}}</Value>
|
||||||
|
</MetaData>
|
||||||
|
</InstallFrom>
|
||||||
|
<InstallTo>
|
||||||
|
<DiskID>0</DiskID>
|
||||||
|
<PartitionID>3</PartitionID>
|
||||||
|
</InstallTo>
|
||||||
|
</OSImage>
|
||||||
|
</ImageInstall>
|
||||||
|
|
||||||
|
<UserData>
|
||||||
|
<AcceptEula>true</AcceptEula>
|
||||||
|
<FullName>CI</FullName>
|
||||||
|
<Organization>CI Lab</Organization>
|
||||||
|
<ProductKey>
|
||||||
|
<Key>{{PRODUCT_KEY}}</Key>
|
||||||
|
<WillShowUI>OnError</WillShowUI>
|
||||||
|
</ProductKey>
|
||||||
|
</UserData>
|
||||||
|
|
||||||
|
<UseConfigurationSet>false</UseConfigurationSet>
|
||||||
|
|
||||||
|
<DynamicUpdate>
|
||||||
|
<Enable>false</Enable>
|
||||||
|
<WillShowUI>Never</WillShowUI>
|
||||||
|
</DynamicUpdate>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
|
||||||
|
<settings pass="specialize">
|
||||||
|
<component name="Microsoft-Windows-Shell-Setup"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<ComputerName>{{COMPUTER_NAME}}</ComputerName>
|
||||||
|
<TimeZone>{{TIMEZONE}}</TimeZone>
|
||||||
|
<RegisteredOwner>CI</RegisteredOwner>
|
||||||
|
<RegisteredOrganization>CI Lab</RegisteredOrganization>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
|
||||||
|
<settings pass="oobeSystem">
|
||||||
|
<component name="Microsoft-Windows-International-Core"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<InputLocale>{{KEYBOARD}}</InputLocale>
|
||||||
|
<SystemLocale>{{LOCALE_OS}}</SystemLocale>
|
||||||
|
<UILanguage>{{LOCALE_OS}}</UILanguage>
|
||||||
|
<UILanguageFallback>{{LOCALE_OS}}</UILanguageFallback>
|
||||||
|
<UserLocale>{{LOCALE_USER}}</UserLocale>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<component name="Microsoft-Windows-Shell-Setup"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<UserAccounts>
|
||||||
|
<AdministratorPassword>
|
||||||
|
<Value>{{ADMIN_PWD}}</Value>
|
||||||
|
<PlainText>true</PlainText>
|
||||||
|
</AdministratorPassword>
|
||||||
|
</UserAccounts>
|
||||||
|
|
||||||
|
<AutoLogon>
|
||||||
|
<Username>{{ADMIN_USER}}</Username>
|
||||||
|
<Password>
|
||||||
|
<Value>{{ADMIN_PWD}}</Value>
|
||||||
|
<PlainText>true</PlainText>
|
||||||
|
</Password>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<LogonCount>5</LogonCount>
|
||||||
|
</AutoLogon>
|
||||||
|
|
||||||
|
<OOBE>
|
||||||
|
<HideEULAPage>true</HideEULAPage>
|
||||||
|
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||||
|
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||||
|
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||||
|
<NetworkLocation>Work</NetworkLocation>
|
||||||
|
<ProtectYourPC>3</ProtectYourPC>
|
||||||
|
<SkipMachineOOBE>true</SkipMachineOOBE>
|
||||||
|
<SkipUserOOBE>true</SkipUserOOBE>
|
||||||
|
</OOBE>
|
||||||
|
|
||||||
|
<TimeZone>{{TIMEZONE}}</TimeZone>
|
||||||
|
<RegisteredOwner>CI</RegisteredOwner>
|
||||||
|
<RegisteredOrganization>CI Lab</RegisteredOrganization>
|
||||||
|
|
||||||
|
<FirstLogonCommands>
|
||||||
|
<SynchronousCommand wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<CommandLine>cmd /c for %d in (C D E F G H I J K L M N O P Q R S T U V W X Y Z) do if exist %d:\post-install.ps1 powershell.exe -NoProfile -ExecutionPolicy Bypass -File %d:\post-install.ps1 ^> C:\Windows\Temp\post-install.log 2^>^&1</CommandLine>
|
||||||
|
<Description>Run post-install script from autounattend CD</Description>
|
||||||
|
</SynchronousCommand>
|
||||||
|
</FirstLogonCommands>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
</unattend>
|
||||||
Reference in New Issue
Block a user