docs: add host setup and template provisioning guides
- HOST-SETUP.md: bootstrap macchina host (Setup-Host.ps1) - WINDOWS-TEMPLATE-SETUP.md: provisioning template Windows con Prepare + Setup-TemplateVM - LINUX-TEMPLATE-SETUP.md: bozza TODO template Linux (non implementato)
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# Host Setup — Local CI/CD
|
||||
|
||||
Procedura di bootstrap della macchina host Windows (i9-10900X, 64 GB RAM, NVMe).
|
||||
Eseguire **una volta** prima di qualsiasi provisioning del template VM o esecuzione di job CI.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisiti host
|
||||
|
||||
| Componente | Versione/Note |
|
||||
| ----------------------- | ----------------------------------------------------------------- |
|
||||
| OS | Windows 11 (host fisico) o Windows Server 2022+ |
|
||||
| VMware Workstation | Pro 17.x — necessario per `vmrun.exe` e supporto linked clone |
|
||||
| Storage | NVMe dedicato (consigliato `F:\` riservato a CI, ≥ 500 GB libero) |
|
||||
| RAM | ≥ 32 GB (4 VM parallele × 6-8 GB) |
|
||||
| Rete | VMnet8 (NAT) configurato in VMware Workstation |
|
||||
| PowerShell | 5.1+ (built-in) o 7.x |
|
||||
| Privilegi | Account amministrativo per Setup-Host.ps1 |
|
||||
|
||||
Software opzionali installati on-demand dallo script:
|
||||
- **NSSM** — installazione automatica via Chocolatey se presente; altrimenti install manuale da https://nssm.cc
|
||||
- **CredentialManager** module — installato automaticamente (scope CurrentUser)
|
||||
|
||||
---
|
||||
|
||||
## Directory layout
|
||||
|
||||
`Setup-Host.ps1` crea l'intera struttura sotto `$CIRoot` (default `F:\CI\`):
|
||||
|
||||
```
|
||||
F:\CI\
|
||||
├── BuildVMs\ # cloni temporanei delle VM build (auto-cleanup post-job)
|
||||
├── Artifacts\ # output build raccolti dall'host
|
||||
├── Logs\ # log per-job (retention: 30 giorni)
|
||||
├── Templates\
|
||||
│ └── WinBuild\ # VMX template Windows (immutabile dopo snapshot)
|
||||
├── act_runner\
|
||||
│ ├── act_runner.exe # binario runner Gitea
|
||||
│ ├── config.yaml # config runner (path, label, capacity)
|
||||
│ └── logs\ # stdout/stderr del servizio
|
||||
├── Cache\
|
||||
│ └── NuGet\ # cache NuGet condivisa via shared folder (TODO §3.1)
|
||||
├── RunnerWork\ # working dir di act_runner (job staging)
|
||||
└── ISO\ # ISO di installazione (Windows Server 2025, ecc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Procedura — esecuzione
|
||||
|
||||
### Step 1 — Clonare il repository
|
||||
|
||||
```powershell
|
||||
git clone https://gitea.emulab.it/Simone/Local-CI-CD-System.git N:\Code\Workspace\Local-CI-CD-System
|
||||
cd N:\Code\Workspace\Local-CI-CD-System
|
||||
```
|
||||
|
||||
### Step 2 — Eseguire Setup-Host.ps1 (elevato)
|
||||
|
||||
Apri PowerShell **come Administrator**, poi:
|
||||
|
||||
```powershell
|
||||
# Esecuzione minimale (crea dirs, archivia credenziali, salta install runner)
|
||||
.\Setup-Host.ps1 -SkipRunnerInstall
|
||||
|
||||
# Setup completo con registrazione runner
|
||||
.\Setup-Host.ps1 -GiteaRunnerToken 'abc123token'
|
||||
|
||||
# CI root su drive diverso
|
||||
.\Setup-Host.ps1 -CIRoot 'D:\CI' -GiteaRunnerToken 'abc123token'
|
||||
```
|
||||
|
||||
### Cosa fa Setup-Host.ps1 — passo per passo
|
||||
|
||||
1. **Crea albero directory** sotto `$CIRoot` (vedi layout sopra)
|
||||
2. **Verifica VMware Workstation** — controlla presenza di `C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe`
|
||||
3. **Installa modulo CredentialManager** (scope CurrentUser, se non presente)
|
||||
4. **Archivia credenziali guest VM** in Windows Credential Manager (target `BuildVMGuest`)
|
||||
- Se non passa `-GuestPassword`, prompt interattivo (SecureString)
|
||||
- Se credenziale già esistente, non sovrascrive — istruzioni per update
|
||||
5. **Copia `runner/config.yaml`** in `$CIRoot\act_runner\config.yaml` (se non esistente)
|
||||
6. **Installa act_runner come servizio Windows** via NSSM:
|
||||
- Verifica presenza NSSM in PATH; tenta install via `choco install nssm -y`
|
||||
- Registra runner con Gitea (`act_runner register --no-interactive --instance ... --token ... --name local-windows-runner --labels windows-build:host,dotnet:host,msbuild:host`) se token fornito
|
||||
- Crea servizio: `AppDirectory`, `AppParameters daemon --config <path>`, log rotation 10 MB
|
||||
- Avvia servizio (`SERVICE_AUTO_START`)
|
||||
7. **Configura SSH alias `gitea-ci`** in `~/.ssh/config` (HostName, Port, User git)
|
||||
|
||||
---
|
||||
|
||||
## Parametri principali
|
||||
|
||||
| Parametro | Default | Note |
|
||||
| ---------------------------- | -------------------------------- | ------------------------------------- |
|
||||
| `-CIRoot` | `F:\CI` | Base directory per tutti i dati CI |
|
||||
| `-GuestCredentialTarget` | `BuildVMGuest` | Target Credential Manager |
|
||||
| `-GuestUsername` | `ci_build` | Account build dentro la VM |
|
||||
| `-GuestPassword` | (prompt) | Mai hard-codare |
|
||||
| `-ActRunnerExe` | `<CIRoot>\act_runner\act_runner.exe` | Path binario runner |
|
||||
| `-ActRunnerConfigYaml` | `<repo>\runner\config.yaml` | Source del config da copiare |
|
||||
| `-GiteaUrl` | `http://10.10.20.11:3100` | URL Gitea per registrazione |
|
||||
| `-GiteaRunnerToken` | (vuoto) | Token registrazione (Admin → Runners) |
|
||||
| `-SkipRunnerInstall` | `$false` | Salta installazione servizio |
|
||||
| `-SkipSSHConfig` | `$false` | Salta scrittura SSH alias |
|
||||
| `-GiteaSSHHost` | `10.10.20.11` | Hostname SSH Gitea |
|
||||
| `-GiteaSSHPort` | `2222` | Porta SSH Gitea |
|
||||
|
||||
---
|
||||
|
||||
## Post-setup checklist
|
||||
|
||||
Allo `Setup-Host.ps1` exit 0:
|
||||
|
||||
1. **Posiziona binario act_runner** se non già fatto:
|
||||
- Path: `F:\CI\act_runner\act_runner.exe`
|
||||
- Download: https://gitea.com/gitea/act_runner/releases/tag/v1.0.2
|
||||
|
||||
2. **Posiziona ISO Windows Server 2025** in `F:\CI\ISO\`
|
||||
(es. `26100.1742.240906-0331.ge_release_svc_refresh_SERVER_VOL_x64FRE_en-us.iso`)
|
||||
|
||||
3. **Configura host WinRM client** (necessario per Prepare-TemplateSetup.ps1):
|
||||
```powershell
|
||||
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force
|
||||
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '192.168.79.*' -Force -Concatenate
|
||||
```
|
||||
*Note*: Prepare-TemplateSetup.ps1 imposta queste in modo transitorio e le ripristina nel `finally`.
|
||||
|
||||
4. **Provisiona il template VM** — vedi [WINDOWS-TEMPLATE-SETUP.md](WINDOWS-TEMPLATE-SETUP.md)
|
||||
|
||||
5. **Registra chiave SSH con Gitea**:
|
||||
- Gitea UI → User Settings → SSH Keys → Add Key
|
||||
- Chiave pubblica: `%USERPROFILE%\.ssh\id_rsa.pub`
|
||||
|
||||
6. **Verifica runner online** in Gitea:
|
||||
- `<GiteaUrl>/-/admin/runners`
|
||||
- Status atteso: **Online**, label `windows-build:host,dotnet:host,msbuild:host`
|
||||
|
||||
---
|
||||
|
||||
## Reference Paths
|
||||
|
||||
| Item | Path / Value |
|
||||
| ------------------------- | ------------------------------------------------------------ |
|
||||
| vmrun.exe | `C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe` |
|
||||
| act_runner exe | `F:\CI\act_runner\act_runner.exe` (servizio: `act_runner`) |
|
||||
| act_runner config | `F:\CI\act_runner\config.yaml` |
|
||||
| act_runner logs | `F:\CI\act_runner\logs\` (stdout.log, stderr.log) |
|
||||
| Template VMX | `F:\CI\Templates\WinBuild\CI-WinBuild.vmx` |
|
||||
| Snapshot name | `BaseClean` |
|
||||
| Clone base dir | `F:\CI\BuildVMs\` |
|
||||
| Artifact dir | `F:\CI\Artifacts\` |
|
||||
| Log dir | `F:\CI\Logs\` (retention: 30 giorni) |
|
||||
| Gitea URL (LAN) | `http://10.10.20.11:3100` |
|
||||
| Gitea URL (ext) | `https://gitea.emulab.it` |
|
||||
| Runner name | `local-windows-runner` (ID: 1) |
|
||||
| Build VM subnet | `192.168.79.0/24` (VMnet8 — NAT, internet access per build) |
|
||||
| Credential Manager target | `BuildVMGuest` |
|
||||
| Guest username | `ci_build` |
|
||||
|
||||
---
|
||||
|
||||
## Maintenance & operational tasks
|
||||
|
||||
Voci di backlog (vedi [TODO.md](../TODO.md)):
|
||||
|
||||
- **§2.2** Cleanup VM orfane schedulato (`scripts/Cleanup-OrphanedBuildVMs.ps1` ogni 6h, `-MaxAgeHours 4`)
|
||||
- **§2.3** Retention artifact + log (Artifacts > 30 gg, Logs > 30 gg, guard libero < 50 GB)
|
||||
- **§2.7** Health check runner (query API Gitea ogni 15 min, restart su offline)
|
||||
- **§4.3** Disk space alert (`F:` < 50 GB → eventcreate/webhook)
|
||||
- **§1.2** Restringere `TrustedHosts` da `*` a `192.168.79.*`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Sintomo | Diagnosi / Fix |
|
||||
| --------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `vmrun.exe NOT found` | Installa VMware Workstation Pro, riesegui |
|
||||
| `NSSM not available` | Install manuale da https://nssm.cc, aggiungi a PATH, riesegui |
|
||||
| Runner registration `exit != 0` | Verifica token Gitea + URL raggiungibile da host |
|
||||
| Service `act_runner` non parte | Check `F:\CI\act_runner\logs\stderr.log` |
|
||||
| `New-StoredCredential` fail | Verifica modulo CredentialManager importato + scope corretto |
|
||||
| Job CI fallisce su `Test-WSMan` | TrustedHosts host non include subnet VM, AllowUnencrypted=$false |
|
||||
@@ -0,0 +1,346 @@
|
||||
# Linux Template VM Setup — DRAFT / TODO
|
||||
|
||||
> **Status**: bozza. Non implementato. Vedi [TODO.md §6.1](../TODO.md) (P2).
|
||||
> Documento di pianificazione per estendere il sistema CI con build su Linux,
|
||||
> in parallelo al template Windows già operativo.
|
||||
|
||||
---
|
||||
|
||||
## Obiettivo
|
||||
|
||||
Aggiungere supporto a build su Linux per:
|
||||
- Compilazione cross-platform (es. versione Linux di plugin C/C++, build check con GCC/Clang)
|
||||
- Test su distro Linux per progetti che lo richiedono
|
||||
- Build matrix `[windows, linux]` nei workflow Gitea
|
||||
|
||||
Architettura simmetrica al template Windows: VM template → snapshot `BaseClean-Linux` →
|
||||
linked clone per ogni job → cleanup automatico.
|
||||
|
||||
---
|
||||
|
||||
## Decisioni di design (da validare)
|
||||
|
||||
### Distro
|
||||
|
||||
| Opzione | Pro | Contro |
|
||||
| ------------------------ | ------------------------------------------------------------------ | -------------------------------------------- |
|
||||
| **Ubuntu 24.04 LTS** | Toolchain recente, supporto LTS fino al 2029, cloud-init ottimo | Snap default (lento per CI) |
|
||||
| Debian 12 | Stabile, leggero, no snap | Toolchain meno recente |
|
||||
| Rocky Linux 9 / Alma 9 | RHEL-compat, glibc moderno, dnf | Meno comune in workflow CI tipici |
|
||||
|
||||
**Scelta proposta**: Ubuntu 24.04 LTS minimal. Rationale: maggior parità con runner GitHub
|
||||
ufficiali (`ubuntu-latest` mappa a 22.04/24.04), toolchain recente out-of-box, cloud-init
|
||||
maturo per provisioning automatico.
|
||||
|
||||
### Transport host → VM
|
||||
|
||||
| Opzione | Pro | Contro |
|
||||
| ------------------------ | ------------------------------------------------------------------ | -------------------------------------------- |
|
||||
| **SSH (chiave pubblica)**| Standard Linux, supporto nativo OpenSSH su Win11/Server 2025 | Diversa API rispetto WinRM (PSSession) |
|
||||
| WinRM su Linux (PSCore) | Codice script unificato | Setup complesso, non standard, fragile |
|
||||
|
||||
**Scelta proposta**: SSH key-based con `ssh.exe` + `scp.exe` (built-in Windows 10+) o
|
||||
modulo `Posh-SSH`. WinRM su Linux non è production-ready.
|
||||
|
||||
### Layer di astrazione
|
||||
|
||||
Aggiungere `scripts/_Transport.psm1` con interfaccia comune:
|
||||
|
||||
```powershell
|
||||
# Pseudo-API
|
||||
function Invoke-RemoteCommand {
|
||||
param($Target, $ScriptBlock, $Args, [ValidateSet('WinRM','SSH')] $Transport)
|
||||
}
|
||||
function Copy-RemoteItem {
|
||||
param($Source, $Destination, $Target, $Direction, $Transport)
|
||||
}
|
||||
function Test-RemoteReady {
|
||||
param($Target, $TimeoutSec, $Transport)
|
||||
}
|
||||
```
|
||||
|
||||
Implementazioni:
|
||||
- **WinRM** wrapper: `New-PSSession` + `Invoke-Command` + `Copy-Item -ToSession`
|
||||
- **SSH** wrapper: `ssh.exe` + `scp.exe` con `-i $keyfile -o StrictHostKeyChecking=accept-new`
|
||||
|
||||
`Invoke-CIJob.ps1` riceve `-GuestOS Windows|Linux` (o auto-detect dal VMX `guestOS`),
|
||||
sceglie transport.
|
||||
|
||||
---
|
||||
|
||||
## Specifiche VM proposte
|
||||
|
||||
| Parametro | Valore |
|
||||
| ----------------------- | --------------------------------------------------------------- |
|
||||
| OS | Ubuntu Server 24.04 LTS minimal |
|
||||
| vCPU | 4 |
|
||||
| RAM | 4096 MB (4 GB) — meno di Windows, footprint Linux più snello |
|
||||
| Disco | 40 GB thin |
|
||||
| NIC | VMnet8 (NAT) — internet per `apt` |
|
||||
| VMX path | `F:\CI\Templates\LinuxBuild\LinuxBuild.vmx` |
|
||||
| ISO | `ubuntu-24.04.x-live-server-amd64.iso` (in `F:\CI\ISO\`) |
|
||||
| Snapshot name | `BaseClean-Linux` |
|
||||
|
||||
---
|
||||
|
||||
## TODO — Implementazione
|
||||
|
||||
### Pre-requisiti host
|
||||
|
||||
- [ ] **OpenSSH client** verificato presente su host (default Win11/Server 2025)
|
||||
```powershell
|
||||
Get-WindowsCapability -Online -Name OpenSSH.Client*
|
||||
```
|
||||
- [ ] **Genera SSH key dedicata** per CI (no passphrase, separata da chiavi utente):
|
||||
```powershell
|
||||
ssh-keygen -t ed25519 -f F:\CI\keys\ci_linux -N '""' -C 'ci-linux-runner'
|
||||
```
|
||||
- [ ] **Aggiungere a `Setup-Host.ps1`** uno step opzionale `-GenerateLinuxCIKey` che crea
|
||||
la chiave e la archivia con permessi corretti (`F:\CI\keys\` con ACL ristretta)
|
||||
|
||||
### Fase A — Crea VM e installa Ubuntu
|
||||
|
||||
- [ ] Crea nuova VM in VMware Workstation (4 vCPU, 4 GB RAM, 40 GB thin)
|
||||
- [ ] NIC: VMnet8 (NAT)
|
||||
- [ ] Boot da ISO Ubuntu Server 24.04 LTS minimal
|
||||
- [ ] Durante install scegli:
|
||||
- [ ] **Minimal install** (no extra packages)
|
||||
- [ ] OpenSSH server: **Yes**
|
||||
- [ ] Username: `ci_build`
|
||||
- [ ] Hostname: `ci-linux-template`
|
||||
- [ ] LVM: opzionale (non necessario per template)
|
||||
- [ ] Annota IP DHCP (`ip a`)
|
||||
|
||||
### Fase B — Provisioning iniziale (manuale, una volta)
|
||||
|
||||
Dentro la VM, come `ci_build`:
|
||||
|
||||
- [ ] **Disabilita swap permanente** (overhead I/O su NVMe condivisa):
|
||||
```bash
|
||||
sudo swapoff -a
|
||||
sudo sed -i '/ swap / s/^/#/' /etc/fstab
|
||||
```
|
||||
|
||||
- [ ] **Disabilita servizi non necessari** (snap, motd-news, ecc.):
|
||||
```bash
|
||||
sudo systemctl disable --now snapd.service snapd.socket snapd.seeded.service 2>/dev/null
|
||||
sudo systemctl disable --now motd-news.timer 2>/dev/null
|
||||
sudo systemctl disable --now apt-daily.timer apt-daily-upgrade.timer
|
||||
```
|
||||
Rationale: snap e timer apt rallentano boot e mangiano CPU su VM efimere.
|
||||
|
||||
- [ ] **Sudo passwordless** per `ci_build` (parità con WinRM admin senza prompt UAC):
|
||||
```bash
|
||||
echo 'ci_build ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/90-ci_build
|
||||
sudo chmod 440 /etc/sudoers.d/90-ci_build
|
||||
```
|
||||
|
||||
- [ ] **Authorized SSH key** dell'host CI:
|
||||
```bash
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
# Incolla la chiave pubblica generata su host (F:\CI\keys\ci_linux.pub)
|
||||
echo 'ssh-ed25519 AAAA... ci-linux-runner' >> ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
- [ ] **SSH hardening** per template (ma non così stretto da bloccare CI):
|
||||
```bash
|
||||
sudo sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
|
||||
sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
### Fase C — Setup-LinuxTemplateVM.sh (script equivalente a Setup-TemplateVM.ps1)
|
||||
|
||||
Da creare: `template/Setup-LinuxTemplateVM.sh` con `set -euo pipefail` + `assert_step()` helper.
|
||||
|
||||
- [ ] **Helper `assert_step`** (throw on fail, log su success):
|
||||
```bash
|
||||
assert_step() {
|
||||
local step="$1"; local desc="$2"; shift 2
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
echo " [OK] [$step] $desc"
|
||||
else
|
||||
echo " [FAIL] [$step] $desc" >&2; exit 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 1 — `apt update` + upgrade**
|
||||
```bash
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq
|
||||
assert_step 'apt' 'no errors' true
|
||||
```
|
||||
|
||||
- [ ] **Step 2 — Toolchain build essentials**
|
||||
```bash
|
||||
sudo apt-get install -y -qq \
|
||||
build-essential gcc g++ clang \
|
||||
make cmake ninja-build \
|
||||
pkg-config autoconf automake libtool \
|
||||
git curl wget ca-certificates \
|
||||
python3 python3-pip python3-venv \
|
||||
unzip zip xz-utils
|
||||
assert_step 'toolchain' 'gcc presente' command -v gcc
|
||||
assert_step 'toolchain' 'clang presente' command -v clang
|
||||
assert_step 'toolchain' 'cmake presente' command -v cmake
|
||||
assert_step 'toolchain' 'python3 presente' command -v python3
|
||||
```
|
||||
|
||||
- [ ] **Step 3 — Toolchain extra opzionale**
|
||||
- [ ] **mingw-w64** se serve cross-compile per Windows: `sudo apt-get install -y mingw-w64`
|
||||
- [ ] **.NET SDK** (per parità Windows): script `dotnet-install.sh` con channel pinned
|
||||
- [ ] **Node.js** se workflow lo richiede: nvm o nodesource repo
|
||||
|
||||
- [ ] **Step 4 — CI working directories**
|
||||
```bash
|
||||
sudo mkdir -p /opt/ci/{build,output,scripts,cache}
|
||||
sudo chown -R ci_build:ci_build /opt/ci
|
||||
assert_step 'dirs' 'tutte presenti' test -d /opt/ci/build -a -d /opt/ci/output
|
||||
```
|
||||
|
||||
- [ ] **Step 5 — Disabilita auto-update apt** (no surprise reboot/lock):
|
||||
```bash
|
||||
sudo apt-get remove -y unattended-upgrades 2>/dev/null
|
||||
sudo systemctl mask apt-daily.service apt-daily-upgrade.service
|
||||
```
|
||||
|
||||
- [ ] **Step 6 — Cleanup pre-snapshot**
|
||||
```bash
|
||||
sudo apt-get clean
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
sudo journalctl --vacuum-time=1d
|
||||
sudo rm -rf /tmp/* /var/tmp/*
|
||||
history -c && cat /dev/null > ~/.bash_history
|
||||
# Zero free space (riduce dim VMDK dopo compact, opzionale)
|
||||
# sudo dd if=/dev/zero of=/zero bs=1M; sudo rm /zero
|
||||
```
|
||||
|
||||
- [ ] **Step 7 — Final pre-snapshot validation**
|
||||
- SSH server abilitato e in ascolto su 22
|
||||
- `sudo` passwordless funzionante per `ci_build`
|
||||
- Tutti i tool elencati (`gcc --version`, `cmake --version`, ecc.) ritornano exit 0
|
||||
- `/opt/ci/{build,output,scripts}` esistono con owner corretto
|
||||
- No leftover `/tmp/*`, history vuota
|
||||
|
||||
### Fase D — Prepare-LinuxTemplateSetup.ps1 (orchestratore host-side)
|
||||
|
||||
Equivalente di `Prepare-TemplateSetup.ps1` per Linux. Usa SSH invece di WinRM.
|
||||
|
||||
- [ ] **Pre-flight host**:
|
||||
- SSH key esiste (`F:\CI\keys\ci_linux`)
|
||||
- `ssh.exe` e `scp.exe` raggiungibili
|
||||
- IP target risponde a `Test-NetConnection -Port 22`
|
||||
|
||||
- [ ] **Copia script in VM**:
|
||||
```powershell
|
||||
scp -i F:\CI\keys\ci_linux -o StrictHostKeyChecking=accept-new `
|
||||
template\Setup-LinuxTemplateVM.sh ci_build@${VMIPAddress}:/tmp/
|
||||
```
|
||||
|
||||
- [ ] **Esegui script in VM**:
|
||||
```powershell
|
||||
ssh -i F:\CI\keys\ci_linux ci_build@${VMIPAddress} `
|
||||
"chmod +x /tmp/Setup-LinuxTemplateVM.sh && sudo /tmp/Setup-LinuxTemplateVM.sh"
|
||||
```
|
||||
Cattura stdout + exit code, parse `[OK]`/`[FAIL]` lines.
|
||||
|
||||
- [ ] **Post-setup remote validation** (parità Windows): query SSH per state checks:
|
||||
- `systemctl is-active ssh` = active
|
||||
- `id ci_build` ritorna gruppo + sudo
|
||||
- `gcc --version`, `cmake --version`, `python3 --version` exit 0
|
||||
- `test -d /opt/ci/build` exit 0
|
||||
|
||||
### Fase E — Snapshot
|
||||
|
||||
- [ ] Solo se Prepare exit 0 → power off VM:
|
||||
```bash
|
||||
sudo shutdown -h now
|
||||
```
|
||||
- [ ] Take snapshot in VMware Workstation: **`BaseClean-Linux`** (case-sensitive)
|
||||
|
||||
### Fase F — Adattamento script CI
|
||||
|
||||
- [ ] **`scripts/_Transport.psm1`** (nuovo modulo) con interfaccia comune
|
||||
- [ ] **`scripts/Invoke-CIJob.ps1`** — aggiungere `-GuestOS Windows|Linux`, branching su transport
|
||||
- [ ] **`scripts/Wait-VMReady.ps1`** — supporto SSH probe (`Test-NetConnection -Port 22` + `ssh -o ConnectTimeout=5 ...`)
|
||||
- [ ] **`scripts/Invoke-RemoteBuild.ps1`** — branch SSH+scp per Linux
|
||||
- [ ] **`scripts/Get-BuildArtifacts.ps1`** — `scp` per Linux invece di `Copy-Item -FromSession`
|
||||
- [ ] **`scripts/New-BuildVM.ps1`** — already OS-agnostic (vmrun clone), verificare che `BaseClean-Linux` sia parametrizzabile
|
||||
- [ ] **`scripts/Remove-BuildVM.ps1`** — already OS-agnostic
|
||||
|
||||
### Fase G — Workflow Gitea
|
||||
|
||||
- [ ] **Registrare label `linux-build`** sul runner (oppure secondo runner dedicato):
|
||||
```yaml
|
||||
# runner/config.yaml — aggiungere a labels esistenti
|
||||
labels:
|
||||
- 'windows-build:host'
|
||||
- 'linux-build:host'
|
||||
```
|
||||
|
||||
- [ ] **Workflow matrix**:
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
target: [windows, linux]
|
||||
runs-on: ${{ matrix.target }}-build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: pwsh -File scripts/Invoke-CIJob.ps1 -GuestOS ${{ matrix.target }}
|
||||
```
|
||||
|
||||
### Fase H — Test e2e
|
||||
|
||||
- [ ] **Smoke test** clone + SSH ready:
|
||||
```powershell
|
||||
.\scripts\New-BuildVM.ps1 -JobId 'linux-smoke' -SnapshotName 'BaseClean-Linux'
|
||||
.\scripts\Wait-VMReady.ps1 -JobId 'linux-smoke' -Transport SSH
|
||||
.\scripts\Remove-BuildVM.ps1 -JobId 'linux-smoke'
|
||||
```
|
||||
|
||||
- [ ] **Build reale**: scegliere repo con build Linux nativo (es. `cmake` based) e
|
||||
eseguire job e2e via `Invoke-CIJob.ps1 -GuestOS Linux`
|
||||
|
||||
- [ ] **Confronto artifact**: build dello stesso repo target Windows vs Linux,
|
||||
verifica che entrambi vengano raccolti in `F:\CI\Artifacts\<jobId>\`
|
||||
|
||||
---
|
||||
|
||||
## Differenze chiave rispetto al template Windows
|
||||
|
||||
| Aspetto | Windows | Linux |
|
||||
| ----------------------- | -------------------------------------- | ------------------------------------------- |
|
||||
| Transport | WinRM HTTP/5985 + Basic | SSH/22 + ed25519 key |
|
||||
| Auth fluid | `ci_build` admin + UAC off + LATFP=1 | `ci_build` + sudoers NOPASSWD |
|
||||
| Trasferimento file | `Copy-Item -ToSession` | `scp` o `rsync` |
|
||||
| Esecuzione remota | `Invoke-Command -Session` | `ssh user@host 'cmd'` |
|
||||
| AV/Firewall disable | Defender + Win Firewall off | ufw inattivo (default), no AV |
|
||||
| Update package | Windows Update (COM API + sched task) | `apt-get update && upgrade` non-interactive |
|
||||
| Toolchain | VS Build Tools 2026 + .NET SDK | gcc/g++/clang + cmake (+ .NET SDK opt) |
|
||||
| Snapshot name | `BaseClean` | `BaseClean-Linux` |
|
||||
| Footprint VM | 6 GB RAM, 80 GB disk | 4 GB RAM, 40 GB disk |
|
||||
| Boot time | ~30-60 s post-clone | ~10-20 s post-clone (atteso) |
|
||||
|
||||
---
|
||||
|
||||
## Backlog correlato (TODO.md)
|
||||
|
||||
- **§6.1 [P2]** Linux Build VM — entry parent
|
||||
- **§6.4 [P3]** Build matrix `[windows, linux]` nel workflow
|
||||
- **§6.3 [P3]** Multi-host runner federation (se 4 VM Linux + 4 Windows non bastano)
|
||||
- **§5.2 [P2]** Modulo `_Transport.psm1` condiviso (prerequisito architetturale)
|
||||
|
||||
---
|
||||
|
||||
## Domande aperte
|
||||
|
||||
1. **.NET SDK su Linux** — sempre installato o solo se workflow lo richiede?
|
||||
Decisione: installare **se** workload richiede parità di build. Default off, opt-in via flag.
|
||||
2. **Image size** — Ubuntu minimal + toolchain ≈ 5 GB. Compact post-snapshot per ridurre footprint VMDK?
|
||||
Decisione: sì, `dd if=/dev/zero` + `vmware-vdiskmanager -k` (manuale, una tantum).
|
||||
3. **Pre-warm pool Linux** — boot Linux è veloce, beneficio minore. Decisione: implementare solo se profile dimostra > 30% overhead.
|
||||
4. **Cloud-init vs script manuale** — cloud-init richiede ISO seed o NoCloud datasource. Più pulito ma overhead di setup. Decisione: script bash diretto (parità con flusso Windows). Cloud-init valutabile in v2.
|
||||
@@ -0,0 +1,278 @@
|
||||
# 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: due script
|
||||
|
||||
| Script | Dove gira | Scopo |
|
||||
| ---------------------------------------- | --------------- | -------------------------------------------------- |
|
||||
| `template/Prepare-TemplateSetup.ps1` | **Host** | Orchestratore: copia + esegue lo script in VM |
|
||||
| `template/Setup-TemplateVM.ps1` | **Dentro VM** | Provisioning effettivo (firewall, defender, tool) |
|
||||
|
||||
`Prepare-TemplateSetup.ps1` apre una WinRM session, copia `Setup-TemplateVM.ps1` in
|
||||
`C:\CI\`, lo esegue, raccoglie l'exit code, valida lo stato del guest.
|
||||
|
||||
---
|
||||
|
||||
## Specifiche VM
|
||||
|
||||
| Parametro | Valore |
|
||||
| ----------------------- | --------------------------------------------------------------- |
|
||||
| OS | Windows Server 2025 Standard/Datacenter (Core o Desktop OK) |
|
||||
| vCPU | 4 |
|
||||
| RAM | 6144 MB (6 GB) |
|
||||
| Disco | 80 GB thin provisioned |
|
||||
| NIC | **VMnet8 (NAT)** — internet richiesto per WU + installer |
|
||||
| VMX path | `F:\CI\Templates\WinBuild\WinBuild.vmx` |
|
||||
| ISO | Windows Server 2025 (da `F:\CI\ISO\`) |
|
||||
|
||||
---
|
||||
|
||||
## Fase A — Crea la VM e installa Windows
|
||||
|
||||
### A.1 Crea VM in VMware Workstation
|
||||
- File → New Virtual Machine → Custom
|
||||
- Hardware: 4 vCPU, 6144 MB, 80 GB thin
|
||||
- Network: **VMnet8 (NAT)**
|
||||
- VMX: `F:\CI\Templates\WinBuild\WinBuild.vmx`
|
||||
- CD/DVD: monta ISO Windows Server 2025
|
||||
|
||||
### A.2 Installa Windows Server 2025
|
||||
- Edition: Standard o Datacenter (Server Core o con Desktop)
|
||||
- Activation: vedi A.4 (deve essere fatto prima dello snapshot)
|
||||
|
||||
### A.3 Abilita WinRM dentro la VM
|
||||
Dentro la VM, come Administrator:
|
||||
|
||||
```cmd
|
||||
winrm quickconfig -q
|
||||
```
|
||||
|
||||
```powershell
|
||||
Enable-PSRemoting -Force -SkipNetworkProfileCheck
|
||||
Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force
|
||||
Set-Item WSMan:\localhost\Service\AllowUnencrypted $true -Force
|
||||
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
|
||||
```
|
||||
|
||||
### A.4 Attiva Windows Server 2025
|
||||
**IMPORTANTE**: deve essere fatto **prima** dello snapshot. I linked clone ereditano
|
||||
l'attivazione.
|
||||
|
||||
```cmd
|
||||
slmgr /dli
|
||||
```
|
||||
|
||||
Se non attivato, scegli il metodo:
|
||||
- **KMS** (server in rete): `slmgr /skms <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-TemplateSetup.ps1 -VMIPAddress 192.168.79.128 -StoreCredential
|
||||
|
||||
# Se WU già applicato in precedenza:
|
||||
.\Prepare-TemplateSetup.ps1 -VMIPAddress 192.168.79.128 -SkipWindowsUpdate -StoreCredential
|
||||
```
|
||||
|
||||
### Cosa fa `Prepare-TemplateSetup.ps1` — passo per passo
|
||||
|
||||
Ogni step ha validazione `Assert-Step` (throw on failure, `[OK]` su pass):
|
||||
|
||||
1. **Pre-flight**: script dir presente, `Setup-TemplateVM.ps1` presente, IP ottetti 0-255, TCP/5985 reachable
|
||||
2. **Configura host WinRM client**: `AllowUnencrypted=true`, aggiunge `$VMIPAddress` ai `TrustedHosts`
|
||||
- Salva valori precedenti, ripristinati nel `finally` (host posture invariata)
|
||||
3. **Validazione host WinRM**: `AllowUnencrypted=true`, `TrustedHosts` include IP
|
||||
4. **Test connettività**: `Test-WSMan -Authentication Basic` con credenziali admin
|
||||
5. **Apre PSSession** verso la VM
|
||||
6. **Crea `C:\CI`** in guest, valida creazione
|
||||
7. **Copia `Setup-TemplateVM.ps1`** in `C:\CI\Setup-TemplateVM.ps1`, valida size match
|
||||
8. **Esegue `Setup-TemplateVM.ps1`** dentro la VM (vedi Fase C)
|
||||
9. **Gestisce reboot 3010**: se WU richiede reboot, riavvia VM e suggerisce re-run con `-SkipWindowsUpdate`
|
||||
10. **Post-setup remote validation** (9 check via singola `Invoke-Command`):
|
||||
- WinRM Running
|
||||
- User `$BuildUsername` exists + admin
|
||||
- UAC `EnableLUA=0`
|
||||
- Firewall tutti profili Off
|
||||
- .NET SDK installato
|
||||
- Python `C:\Python\python.exe` presente
|
||||
- MSBuild path presente
|
||||
- `C:\CI\{build,output,scripts}` presenti
|
||||
11. **Restore host WinRM** nel `finally` (sempre eseguito, anche su failure)
|
||||
|
||||
### Parametri principali
|
||||
|
||||
| Parametro | Default | Note |
|
||||
| --------------------- | -------------- | ----------------------------------------------------------------------------- |
|
||||
| `-VMIPAddress` | (mandatory) | IP DHCP della VM su VMnet8 |
|
||||
| `-AdminUsername` | Administrator | Account admin built-in nella VM |
|
||||
| `-AdminPassword` | (prompt) | Se vuoto, prompt SecureString |
|
||||
| `-BuildPassword` | (prompt) | Password per `ci_build` — mai hard-codare |
|
||||
| `-BuildUsername` | ci_build | Account CI dentro la VM |
|
||||
| `-DotNetSdkVersion` | 10.0 | Channel .NET SDK |
|
||||
| `-SkipWindowsUpdate` | (off) | Pass-through a Setup-TemplateVM.ps1 |
|
||||
| `-StoreCredential` | (off) | Registra `BuildUsername`/`BuildPassword` in Windows Credential Manager |
|
||||
| `-CredentialTarget` | BuildVMGuest | Target Credential Manager (deve corrispondere a quanto usato da Invoke-CIJob) |
|
||||
|
||||
---
|
||||
|
||||
## Fase C — Cosa fa `Setup-TemplateVM.ps1` (dentro la VM)
|
||||
|
||||
Eseguito automaticamente da Prepare in Fase B. Ordine ottimizzato per ridurre overhead:
|
||||
tutti i tweak UI/registro sono raggruppati **prima** di Windows Update (operazione lenta).
|
||||
Ogni step ha validazione `Assert-Step`.
|
||||
|
||||
```
|
||||
Step 1 Firewall off (Domain/Private/Public) ── primo, rimuove blocchi
|
||||
Step 2 Defender + Antimalware off (Set-MpPreference + GPO)
|
||||
── prima di WinRM e WU
|
||||
Step 3 WinRM hardening (Basic, AllowUnencrypted, 2 GB shell mem, 2h timeout)
|
||||
Step 4 Build user account (ci_build, admin, no expire) ── prima di WU
|
||||
Step 4b UAC off (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
|
||||
Step 5 CI dirs: C:\CI\{build, output, scripts} ── C:\CI deve esistere prima di WU
|
||||
Step 5b Explorer LaunchTo=1 (This PC)
|
||||
Step 5c CI UX hardening: Server Manager off, DisableCAD, OOBE/telemetry off
|
||||
Step 6 Windows Update via Scheduled Task SYSTEM ── senza scan Defender, C:\CI presente
|
||||
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
|
||||
|
||||
- **Firewall first** — Windows blocca alcune chiamate WinRM finché non è classificato il
|
||||
profilo di rete. Off subito → niente race condition durante Step 3.
|
||||
- **Defender second** — disabilitato prima di WinRM e WU. WU scarica/installa update
|
||||
senza scan (5-15 min risparmiati). Step 7-9 (download + install ~200-400 MB di
|
||||
payload .NET/Python/VS) non vengono scansionati.
|
||||
- **WinRM third** — già non interferito da firewall/defender. Hardening pulito.
|
||||
- **User + UAC (4/4b)** — operazioni registro rapide; UAC off prima di dir/tool evita
|
||||
prompt di elevazione; nessun motivo di aspettare WU.
|
||||
- **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)** — tutti i tweak registro raggruppati prima del WU (lento);
|
||||
scritture veloci, nessuna dipendenza da WU o toolchain.
|
||||
- **WU sixth** — gira con tutto già off + `C:\CI` presente; condizioni ottimali.
|
||||
- **WU disable subito dopo** — snapshot cattura WU permanentemente off.
|
||||
- **Toolchain (7-9) last** — WU completato, nessun scan su payload installer.
|
||||
|
||||
### Validazioni per step (sintesi)
|
||||
|
||||
| Step | Check chiave |
|
||||
| ---- | ------------------------------------------------------------------------------- |
|
||||
| 1 | `Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False` |
|
||||
| 2 | 6 GPO key + `Set-MpPreference DisableRealtimeMonitoring=true` + ExclusionPath |
|
||||
| 3 | WinRM Running+Automatic, AllowUnencrypted=true, Auth/Basic=true, MaxMem≥2048 |
|
||||
| 4 | User exists, enabled, PasswordNeverExpires, member of Administrators |
|
||||
| 4b | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
|
||||
| 5 | Ogni dir Test-Path -PathType Container |
|
||||
| 5b | HKCU LaunchTo=1 |
|
||||
| 5c | ServerManager machine policy DoNotOpenAtLogon=1, HKCU DoNotOpenServerManagerAtLogon=1, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0 |
|
||||
| 6 | ResultCode ∈ {0,2,3}, no reboot required (else exit 3010) |
|
||||
| 6b | wuauserv StartType=Disabled, NoAutoUpdate=1, DisableWindowsUpdateAccess=1 |
|
||||
| 7 | `dotnet --version` channel match, machine PATH contiene `C:\dotnet` |
|
||||
| 8 | `C:\Python\python.exe` esiste, version match esatto, PATH contiene `C:\Python` |
|
||||
| 9 | MSBuild.exe esiste, esegue, PATH, `\VC\` dir presente |
|
||||
| 10 | dotnet+python+msbuild tutti risolvibili (throw, non più warn) |
|
||||
| Final| WinRM, firewall, user+admin, UAC, WU disabled, tutti tool, no leftover installer in `C:\CI` |
|
||||
|
||||
---
|
||||
|
||||
## Fase D — Snapshot
|
||||
|
||||
**Solo se `Prepare-TemplateSetup.ps1` exit 0** (tutti gli `Assert-Step` passati):
|
||||
|
||||
1. **Spegni la VM**: Start → Shut down (graceful, non hard-stop)
|
||||
2. **Take snapshot** in VMware Workstation:
|
||||
- VM → Snapshot → Take Snapshot
|
||||
- **Nome esatto: `BaseClean`** (case-sensitive, usato da `New-BuildVM.ps1`)
|
||||
3. **Lascia la VM spenta** — non modificarla mai più dopo lo snapshot
|
||||
4. **Verifica `runner/config.yaml`**: `GITEA_CI_TEMPLATE_PATH=F:\CI\Templates\WinBuild\CI-WinBuild.vmx`
|
||||
|
||||
---
|
||||
|
||||
## 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-TemplateSetup.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
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Sintomo | Diagnosi / Fix |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `Cannot reach $VMIPAddress via WinRM` | VM non avviata, WinRM non abilitato, IP errato — verifica con ipconfig |
|
||||
| `Setup-TemplateVM.ps1 exited with code 3010`| Reboot richiesto post-WU. Re-run con `-SkipWindowsUpdate` |
|
||||
| `[Defender] Validation failed: GPO key` | Defender riattivato da Windows Update — disabilita via gpedit + reboot |
|
||||
| `VS Build Tools installation timed out` | Network slow o cache corrotta. Re-run con cache pulita (`C:\ProgramData\Microsoft\VisualStudio\Packages`) |
|
||||
| `Python version match` fail | `$PythonVersion` ≠ versione installata. Aggiorna param o reinstalla |
|
||||
| Post-setup `MSBuild not found` | VS install fallita silenziosa. Check `C:\CI\vs_log.txt` |
|
||||
| Pre-snapshot Final check fail | Stato VM rotto — non prendere snapshot. Re-run o ricomincia da zero |
|
||||
|
||||
---
|
||||
|
||||
## Backlog correlato (TODO.md)
|
||||
|
||||
- **§1.1 [P0]** Migrare WinRM da HTTP/Basic a HTTPS/5986 con cert self-signed nel template
|
||||
- **§1.3 [P0]** Pinning hash SHA256 degli installer (Python, dotnet-install.ps1, vs_buildtools.exe)
|
||||
- **§1.6 [P2]** Documentare threat model per Defender/Firewall/UAC tutti off
|
||||
- **§2.5 [P1]** Snapshot versionato `BaseClean_<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
|
||||
Reference in New Issue
Block a user