Files
local-ci-cd-system/plans/implementation-plan-A-B.md
T
Simone 573d11691d docs: Windows host compat is a categorical constraint (B8 cancelled)
User requirement (non-negotiable): the project must stay
first-class compatible with a Windows host permanently — Phase B
adds Linux *alongside* Windows, never replacing/degrading it.
- implementation-plan-A-B §2: categorical-constraint banner; B8
  decommissioning cancelled (kept only as historical record);
  master checklist B8 row updated.
- PhaseB-user-checklist: Passo 9 (B8) cancelled -> dual-host kept;
  Passo 8 no longer a decommission gate; tracking table + closing
  note updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:45:31 +02:00

1082 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Piano implementativo unificato — Fase A (Python) + Fase B (host Linux)
## Summary esecutivo
Obiettivo combinato: riscrivere l'orchestratore CI/CD in Python 3.11+
cross-platform (Fase A) e poi spostare l'host da Windows 11 a Linux Mint
mantenendo VMware Workstation Pro come hypervisor (Fase B). Il risultato
è un unico stack Python in produzione su Linux, con i template VM, i
workflow Gitea Actions e act_runner invariati nella sostanza.
Sintesi fasi:
- **Fase A — Rewrite Python dell'orchestratore (host Windows attuale)**
- Output verificabile: `python -m ci_orchestrator job ...` sostituisce
`Invoke-CIJob.ps1`; `pytest` ≥70% coverage; workflow
`build-ns7zip.yml` PASS.
- **Stato: COMPLETATA** ✅ — validata end-to-end (vedi
`plans/PhaseA-user-checklist.md` Passi 1-9), merge `feature →
main` `2aa12bb`, tag `v2.0.0-phaseA`. Dettaglio storico in §1.
- **Fase B — Migrazione host a Linux Mint + Workstation Pro Linux**
- Output verificabile: act_runner come `act-runner.service` systemd;
storage `/var/lib/ci/`; burn-in 4 job concorrenti PASS.
- **Stato: pronta** (gating "A done" soddisfatto). Esecuzione via
`plans/PhaseB-user-checklist.md`; design/rischi/cronoprogramma in
§2/§3/§5/§6 di questo piano.
- **Fase C — (Hook only, non implementata) backend ESXi via `pyVmomi`**
- Output verificabile: Protocol `VmBackend` rispettato; `[backend]`
selector in `config.toml`.
- Stato: non in scope.
Decisioni architetturali già prese e non rinegoziabili:
- Linguaggio: Python 3.11+, packaging `pyproject.toml`, CLI via `click`
- Transport WinRM: `pypsrp` (no dipendenza da `New-PSSession`)
- Transport SSH: `paramiko` (no dipendenza da `ssh.exe` / `scp.exe` esterni)
- Credential store: `keyring` (Credential Manager su Win, Secret Service su Linux)
- Hypervisor host Linux: VMware Workstation Pro Linux (binario `/usr/bin/vmrun`)
- Astrazione hypervisor: Protocol `VmBackend` con prima implementazione `WorkstationVmrunBackend`
- Lint dual-stack: `ruff` + `mypy --strict` per Python, PSScriptAnalyzer per i PS legacy residui
Vincoli che NON cambiano: act_runner v1.0.2+ come consumer agnostico di
shell, Gitea `http://10.10.20.11:3100` / `https://gitea.emulab.it`,
template VM (`WinBuild2025`, `WinBuild2022`, `LinuxBuild2404`) e relativi
snapshot `BaseClean` / `BaseClean-Linux`, workflow YAML in
`gitea/workflows/` (cambia solo la `shell:` e il comando invocato in Fase
A4; cambiano solo path/env vars in Fase B).
## Checklist riassuntiva master
- [x] [A1] Creare `pyproject.toml` + package `src/ci_orchestrator/` + venv `F:\CI\python\venv\`
- [x] [A1] Implementare `config.py` (env vars + `config.toml`) con default Windows e hook path Linux
- [x] [A1] Implementare `backends/protocol.py` con Protocol `VmBackend` (firma neutra, no `vmrun_*` nei nomi pubblici)
- [x] [A1] Implementare `backends/workstation.py` (`WorkstationVmrunBackend`) usando `subprocess` + `shutil.which('vmrun')`
- [x] [A1] Implementare `transport/winrm.py` con wrapper `pypsrp.client.Client`
- [x] [A1] Implementare `transport/ssh.py` con wrapper `paramiko.SSHClient` / `SFTPClient`
- [x] [A1] Implementare `credentials.py` con Protocol `CredentialStore` + `KeyringCredentialStore`
- [x] [A1] PoC `wait-ready` end-to-end via `pypsrp` contro un guest Windows reale (smoke self-test PASS)
- [x] [A1] Test pytest unitari per `vmrun.py`, `winrm.py`, `ssh.py`, `credentials.py` con mock
- [x] [A1] Aggiungere job pytest a `gitea/workflows/lint.yml`
- [x] [A2] Portare `Wait-VMReady.ps1``python -m ci_orchestrator wait-ready`
- [x] [A2] Portare `Remove-BuildVM.ps1``vm remove`
- [x] [A2] Portare `Cleanup-OrphanedBuildVMs.ps1``vm cleanup`
- [x] [A2] Portare `Watch-DiskSpace.ps1``monitor disk`
- [x] [A2] Portare `Watch-RunnerHealth.ps1``monitor runner`
- [x] [A2] Portare `Get-CIJobSummary.ps1``report job`
- [x] [A2] Sostituire ognuno dei `.ps1` portati con shim a 3 righe verso la CLI Python
- [x] [A3] Portare `New-BuildVM.ps1``vm new`
- [x] [A3] Portare `Invoke-RemoteBuild.ps1``build run`
- [x] [A3] Portare `Get-BuildArtifacts.ps1``artifacts collect`
- [x] [A3] Convertire test Pester `New-BuildVM.Tests.ps1`, `Wait-VMReady.Tests.ps1`, `Remove-BuildVM.Tests.ps1` in pytest
- [x] [A4] Portare `Invoke-CIJob.ps1``python -m ci_orchestrator job`
- [x] [A4] Aggiornare `gitea/actions/local-ci-build/action.yml` per invocare la CLI Python direttamente
- [x] [A4] Forzare `PYTHONIOENCODING=utf-8` in `runner/config.yaml`
- [x] [A4] Eseguire workflow `build-ns7zip.yml` (matrix Win+Linux) end-to-end PASS (+ job release validato con tag throwaway)
- [x] [A5] Convertire errori frequenti `AGENTS.md` #9, #10, #11, #12 in test pytest dedicati
- [x] [A5] Aggiornare `AGENTS.md` con sezione "Python development" (venv, ruff, mypy, pytest)
- [x] [A5] Aggiornare `docs/ARCHITECTURE.md` con nuovo layout package
- [x] [A5] Aggiornare `README.md` con setup Python
- [x] [A5] Eseguire burn-in 4 job concorrenti PASS (Start-BurnInTest: 3 round × 4 = 12/12, 0 orfani)
- [ ] [B1] Installare Linux Mint LTS + aggiornamenti su hardware target
- [ ] [B1] Installare VMware Workstation Pro Linux (bundle ufficiale Broadcom) e validare `vmrun` su VM di test
- [ ] [B1] Configurare `vmnet8` NAT range `192.168.79.0/24` (allineato all'host Windows)
- [ ] [B1] Creare utente `ci-runner` (uid dedicato) e storage `/var/lib/ci/{build-vms,artifacts,templates,keys}` con ownership `ci-runner:ci-runner` mode 750
- [ ] [B1] Installare Python 3.11+ e creare venv in `/opt/ci/venv/` con `pip install -e .` del package portato in Fase A
- [ ] [B1] Validare `python -m ci_orchestrator --help` da utente `ci-runner`
- [ ] [B2] Spegnere fully powered-off i template VM sull'host Windows
- [ ] [B2] Trasferire `F:\CI\Templates\``/var/lib/ci/templates/` con `rsync` (preservare snapshot `BaseClean` / `BaseClean-Linux`)
- [ ] [B2] Registrare i `.vmx` su Workstation Linux e validare snapshot via `vmrun listSnapshots`
- [ ] [B2] Smoke test manuale: `vm new` + `wait-ready` + `vm remove` da host Linux
- [ ] [B3] Copiare `F:\CI\keys\ci_linux*``/etc/ci/keys/` con perms 600 owner `ci-runner`
- [ ] [B3] Re-store credenziali `BuildVMGuest` e `GiteaPAT` con `secret-tool` nel keyring Linux
- [ ] [B3] PoC accesso headless al keyring sotto systemd (file vault `age`/`sops` come fallback)
- [ ] [B4] Scaricare act_runner Linux ≥ v1.0.2 e registrarlo verso Gitea con label `windows-build:host` e `linux-build:host`
- [ ] [B4] Creare unit `/etc/systemd/system/act-runner.service` con `User=ci-runner`, env `CI_*`, `PYTHONIOENCODING=utf-8`
- [ ] [B4] `systemctl enable --now act-runner.service` e validare via `journalctl -u act-runner -f`
- [x] [B5] Convertire `Register-CIScheduledTasks.ps1` in coppie `*.service` + `*.timer` (`cleanup-orphaned-vms`, `retention-policy`, `watch-disk-space`, `watch-runner-health`, `backup-template`)
- [ ] [B5] Abilitare tutti i timer con `systemctl enable --now` e validare `systemctl list-timers`
- [ ] [B6] Stop act_runner sull'host Windows e verifica coda Gitea vuota
- [ ] [B6] Rsync incrementale finale `F:\CI\Artifacts\``/var/lib/ci/artifacts/`
- [ ] [B6] Trigger smoke workflow + `build-ns7zip.yml` matrix da host Linux PASS
- [ ] [B7] Eseguire burn-in 4 job concorrenti × 10 round su template Windows e Linux con tempi entro ±20% baseline
- [ ] [B8] (Facoltativo) Backup periodico di `F:\CI\`
- [x] [B8] ~~Decommissioning host Windows~~ **ANNULLATO** — vincolo categorico: host Windows supportato in permanenza (dual-host), non dismesso
- [ ] [X1] Aggiornare `lint.yml` per girare ruff + mypy + pytest oltre a PSSA
- [ ] [X2] Aggiungere observability: log strutturato `logging` + journald handler in Fase B
- [ ] [X3] Documentare gestione credenziali headless (DPAPI machine scope su Win, vault `age` su Linux)
- [ ] [X4] Aggiornare `docs/RUNBOOK.md`, `docs/HOST-SETUP.md`, `docs/CI-FLOW.md` end-of-Fase-B
## 0. Prerequisiti e gating
Da soddisfare prima di iniziare A1:
> Prerequisiti Fase A **soddisfatti** (Fase A completata). Per `BuildVMGuest`
> emerso in validazione: deve stare nel vault **LocalSystem** (act_runner
> service), username host-qualificato — vedi `scripts/Set-CIGuestCredential.ps1`.
- [x] Repo `local-ci-cd-system` clonato e workflow `lint.yml` + `self-test.yml` verdi sull'host Windows attuale
- [x] Backup integro di `F:\CI\Templates\` (snapshot `BaseClean` validato con `vmrun listSnapshots`)
- [x] Python 3.11+ installato sull'host Windows (venv produzione `F:\CI\python\venv`, install **non-editable**)
- [x] Accesso amministrativo a Gitea per registrare/de-registrare runner durante A4 e B4
- [x] Credenziali `BuildVMGuest` e `GiteaPAT` nel vault SYSTEM (LocalSystem), username host-qualificato
- [x] `AGENTS.md` letto integralmente da chi esegue il piano (errori #1#12 sono test case obbligatori)
- [ ] Hardware target Linux Mint disponibile fisicamente (può restare spento fino a B1)
## 1. Fase A — Rewrite Python (host Windows attuale)
> **✅ FASE A COMPLETATA E VALIDATA END-TO-END.**
> Merge `feature/python-rewrite-and-linux-migration → main` (`2aa12bb`),
> tag `v2.0.0-phaseA`. Validazione: `plans/PhaseA-user-checklist.md`
> Passi 1-9 (smoke wait-ready/job Win+Linux, self-test entrambi i
> transport, `build-ns7zip.yml` matrix + job release, burn-in 12/12,
> benchmark). Le sottosezioni A1-A5 sotto sono **record storico** del
> piano; lo stato reale è negli `[x]` della checklist master e nel
> checklist utente.
>
> **Deviazioni/scoperte rilevanti in validazione** (non previste dal
> piano originale, ora gestite):
> - venv produzione = install **non-editable** (act_runner=LocalSystem;
> `-e` punta a path non risolti → `No module named ci_orchestrator`).
> `lint.yml` non deve installare nella venv condivisa (venv effimera).
> - Credenziali (`BuildVMGuest`, `GiteaPAT`) nel vault **SYSTEM** non
> utente; username WinRM **host-qualificato**; transport WinRM forza
> `auth=ntlm`. Helper: `scripts/Set-CIGuestCredential.ps1`,
> `scripts/Test-CIGuestWinRM.ps1`.
> - Gitea: `DEFAULT_ACTIONS_URL=github` + `uses:` URL completo; repo
> action **pubblico**; `upload/download-artifact@v4`→`@v3` (GHES);
> artifact **github-free** (handoff via filesystem host).
> - Transport sorgente: `use-git-clone`/`submodules` default ON;
> variante host-side clone (`--host-clone`) per SSH alias host-only.
> - Shim PS: i forwarder ciechi NON inoltrano i common param PS.
> - Deferred (non bloccanti, `TODO.md`): §8.1 mirror `actions/checkout`
> su Gitea, §8.2 streaming live output build Windows.
### A1 — Bootstrap progetto + moduli core
**Obiettivo**: creare scheletro package Python con `VmBackend`, transport
WinRM/SSH e credential store funzionanti contro l'ambiente reale via PoC
`wait-ready`.
**Input / dipendenze**: prerequisiti §0; `scripts/_Common.psm1` e
`scripts/_Transport.psm1` come riferimento di comportamento atteso;
`AGENTS.md` errori #9#12 da preservare semanticamente.
**Attività**:
- [x] Creare `pyproject.toml` (build backend `setuptools` o `hatchling`) con dipendenze `pypsrp`, `paramiko`, `keyring`, `click`, `tomli` (se Python <3.11), `rich` opzionale per logging
- [x] Creare layout `src/ci_orchestrator/` con `__init__.py`, `__main__.py` (entry point `click`)
- [x] Creare venv in `F:\CI\python\venv\` e installare il package in editable (`pip install -e .[dev]`)
- [x] Implementare `config.py`: caricamento env vars (`CI_ROOT`, `CI_TEMPLATES`, `CI_BUILD_VMS`, `CI_ARTIFACTS`, `CI_KEYS`) con merge da `config.toml` opzionale; default OS-aware (Windows → `F:\CI\...`, Linux → `/var/lib/ci/...`)
- [x] Implementare `backends/protocol.py` con Protocol `VmBackend` (metodi `clone_linked`, `start`, `stop`, `delete`, `get_ip`, `list_snapshots`) e dataclass `VmHandle` (path/identificatore opaco)
- [x] Implementare `backends/workstation.py`: `WorkstationVmrunBackend` che usa `subprocess.run([vmrun, '-T', 'ws', op, *args], check=False, capture_output=True, text=True, encoding='utf-8')` con check esplicito su `returncode`
- [x] Implementare `transport/winrm.py`: wrapper su `pypsrp.client.Client(host, username, password, ssl=True, cert_validation=False)` con metodi `run`, `copy`, `fetch`
- [x] Implementare `transport/ssh.py`: wrapper su `paramiko.SSHClient` con `set_missing_host_key_policy(AutoAddPolicy)`, `known_hosts` configurabile (default `None` per non interferire con clone ephemeri — vedi `AGENTS.md` errore #12)
- [x] Implementare `credentials.py`: Protocol `CredentialStore` + `KeyringCredentialStore` che usa `keyring.get_credential(target, None)` e ritorna oggetto `Credential(username, password)`
- [ ] PoC end-to-end: comando `python -m ci_orchestrator wait-ready --vmx <path> --timeout 120` su un clone reale del template `WinBuild2025` (validare WinRM HTTPS self-signed)
- [x] Setup `pytest`, `pytest-mock`, `ruff`, `mypy` come dev dependencies; configurare `pyproject.toml` con `[tool.ruff]` e `[tool.mypy]` strict
- [x] Test pytest unitari: `test_vmrun.py` (mock subprocess, casi `vmrun list` con e senza VMX target — copre errore #10), `test_winrm.py` (mock `pypsrp`), `test_ssh.py` (mock `paramiko`, copre errore #12), `test_credentials.py` (mock `keyring`)
- [x] Aggiungere job `python` a `gitea/workflows/lint.yml`: setup venv → `ruff check src/ tests/``mypy --strict src/``pytest --cov=ci_orchestrator --cov-fail-under=70`
**Hook futuri Fase C**: il Protocol `VmBackend` deve usare nomi neutri
(`clone_linked`, non `vmrun_clone`), accettare `template`/`snapshot`/`name`
come stringhe opache (non path Windows), separare `clone_linked` da
`start` (ESXi richiede `PowerOn` come task distinto). Niente assunzioni
"backend == stesso host del control plane" — `WorkstationVmrunBackend`
deve esporre la stessa firma anche se di fatto è locale.
**Test / validazione**:
- `pytest -q tests/` PASS
- `pytest --cov=ci_orchestrator --cov-fail-under=70` PASS
- `ruff check src/ tests/` 0 errori
- `mypy --strict src/` 0 errori
- PoC `wait-ready` ritorna exit 0 entro 120s contro clone reale di `WinBuild2025`
- `gitea/workflows/lint.yml` PASS in CI
**Rollback**: rimuovere cartella `src/`, `tests/`, `pyproject.toml`,
venv `F:\CI\python\venv\`; ripristinare `lint.yml` precedente da git
(`git checkout HEAD~1 -- gitea/workflows/lint.yml`). Nessun PS legacy
modificato in A1.
**Definizione di fatto step A1**:
- [ ] PoC `wait-ready` PASS contro VM reale
- [x] Coverage pytest ≥70% sui moduli core
- [x] `lint.yml` aggiornato e verde
- [x] Protocol `VmBackend` reviewato e congelato
- [x] Documentato setup venv in `README.md` (sezione minima)
### A2 — Script "foglia" (no state condiviso)
**Obiettivo**: portare in Python tutti gli script PS che non condividono
stato con l'orchestratore, sostituendoli con shim minimi.
**Input / dipendenze**: A1 done. File: `Wait-VMReady.ps1`,
`Remove-BuildVM.ps1`, `Cleanup-OrphanedBuildVMs.ps1`,
`Watch-DiskSpace.ps1`, `Watch-RunnerHealth.ps1`, `Get-CIJobSummary.ps1`.
**Attività**:
- [x] Implementare `commands/wait.py` con sottocomando `wait-ready` (parametri `--vmx`, `--timeout`, `--guest-os`)
- [x] Implementare `commands/vm.py` con sottocomando `vm remove` (parametri `--vmx`, `--force`)
- [x] Implementare `vm cleanup` (scan `CI_BUILD_VMS`, riconoscimento clone orfani per pattern naming + età)
- [x] Implementare `commands/monitor.py` con `monitor disk` (soglie configurabili, output JSON o human)
- [x] Implementare `monitor runner` (controllo processo act_runner attivo + ultimo heartbeat)
- [x] Implementare `commands/report.py` con `report job` (read-only su artifact dir + log job)
- [x] Sostituire `scripts/Wait-VMReady.ps1` con shim PS 5.1 di 3 righe: `& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator wait-ready @args; exit $LASTEXITCODE`
- [x] Idem per `Remove-BuildVM.ps1`, `Cleanup-OrphanedBuildVMs.ps1`, `Watch-DiskSpace.ps1`, `Watch-RunnerHealth.ps1`, `Get-CIJobSummary.ps1`
- [x] Aggiungere test pytest per ogni comando (mock backend + mock filesystem via `tmp_path`)
- [ ] Convertire `tests/Wait-VMReady.Tests.ps1` in `tests/test_commands_wait.py` (preservando i casi negativi)
- [ ] Convertire `tests/Remove-BuildVM.Tests.ps1` in `tests/test_commands_vm_remove.py`
- [ ] Verificare che gli scheduled task esistenti (`Register-CIScheduledTasks.ps1`) continuino a funzionare invocando gli shim PS
**Hook futuri Fase C**: `vm cleanup` non deve assumere scan locale del
filesystem — la firma deve accettare un `VmBackend` come dipendenza,
così che in Fase C la stessa logica interroghi l'API ESXi (folder-scoped
list).
**Test / validazione**:
- `pytest tests/test_commands_*.py` PASS
- Coverage globale rimane ≥70%
- Trigger manuale di `Watch-DiskSpace` via shim PS produce stesso output
semantico della versione PS originale (json comparabile)
- Smoke: clone manuale di una VM → `wait-ready``vm remove` end-to-end
**Rollback**: `git checkout HEAD~1 -- scripts/Wait-VMReady.ps1 ...`
(ripristino .ps1 originali); rimozione `commands/*.py` portati;
scheduled tasks restano collegati ai .ps1 ripristinati.
**Definizione di fatto step A2**:
- [ ] Tutti gli script "foglia" hanno shim PS che chiama Python
- [ ] Test pytest sostituiscono i Pester corrispondenti
- [ ] Scheduled task continuano a funzionare via shim
- [ ] Nessuna regressione su `self-test.yml`
### A3 — Pipeline di build
**Obiettivo**: portare la pipeline core (clone → build → collect) in
Python preservando il comportamento dei `.ps1` esistenti.
**Input / dipendenze**: A2 done. File: `New-BuildVM.ps1`,
`Invoke-RemoteBuild.ps1`, `Get-BuildArtifacts.ps1`.
**Attività**:
- [x] Implementare `vm new` in `commands/vm.py` (parametri `--template`, `--snapshot`, `--name`, `--guest-os`)
- [x] Integrare `WorkstationVmrunBackend.clone_linked` + `start` + attesa IP via `get_ip`
- [x] Implementare `commands/build.py` con `build run` (parametri `--vmx`, `--script`, `--workdir`, `--guest-os`); usa `transport.winrm` per Windows, `transport.ssh` per Linux
- [x] Gestione streaming output build verso stdout (act_runner cattura stdout — preservare comportamento `Write-Host`)
- [x] Implementare `commands/artifacts.py` con `artifacts collect` (parametri `--vmx`, `--remote-path`, `--local-dir`)
- [x] Sostituire `scripts/New-BuildVM.ps1`, `Invoke-RemoteBuild.ps1`, `Get-BuildArtifacts.ps1` con shim
- [x] Convertire `tests/New-BuildVM.Tests.ps1` in `tests/test_commands_vm_new.py`
- [x] Aggiungere test pytest per `build run` (mock transport + capture stdout)
- [x] Aggiungere test pytest per `artifacts collect` (mock SFTP/WinRM file copy + tmp_path)
- [ ] Validare end-to-end: clone WinBuild2025 → build script PowerShell trivial → collect artifact ZIP
**Hook futuri Fase C**: `build run` deve ricevere un `VmHandle` opaco,
non un path VMX (ESXi non ha path locale al control plane). Adattare le
CLI a accettare entrambe (`--vmx` per workstation, `--vm-id` futuro per
ESXi) con dispatcher nel layer `commands/`.
**Test / validazione**:
- `pytest tests/test_commands_vm_new.py tests/test_commands_build.py tests/test_commands_artifacts.py` PASS
- Coverage globale ≥70%
- Smoke: workflow CI manuale end-to-end usando esclusivamente gli shim
- `Test-NsinnounpBuild.ps1` continua a passare (usa shim sotto)
**Rollback**: ripristino `.ps1` originali da git; rimozione comandi
Python; nessuna modifica ai workflow YAML in A3 (solo shim).
**Definizione di fatto step A3**:
- [x] Pipeline build completa accessibile via Python CLI
- [x] Shim PS preservano l'API per i caller esistenti
- [ ] Smoke build PASS contro VM reale Windows e Linux
- [x] Pester `New-BuildVM.Tests.ps1` rimosso e sostituito
### A4 — Orchestratore + switch workflow
**Obiettivo**: portare `Invoke-CIJob.ps1` e fare switch dei workflow
Gitea per chiamare la CLI Python direttamente.
**Input / dipendenze**: A3 done. File: `Invoke-CIJob.ps1`,
`gitea/actions/local-ci-build/action.yml`, `runner/config.yaml`.
**Attività**:
- [x] Implementare `commands/job.py` con sottocomando `job` (entry point completo: parsing parametri job, clone, wait, build, collect, cleanup)
- [x] Gestione errori e cleanup garantito (try/finally con `vm remove` anche su failure)
- [x] Aggiornare `gitea/actions/local-ci-build/action.yml`: cambiare `shell: powershell``shell: cmd` o invocazione diretta `python -m ci_orchestrator job ...`
- [x] Aggiungere `PYTHONIOENCODING=utf-8` in `runner/config.yaml` env
- [x] Lasciare `Invoke-CIJob.ps1` come shim per scheduled task / chiamate manuali esterne (rimosso in A5)
- [x] Test pytest end-to-end con mock backend + transport: `test_commands_job.py`
- [ ] Eseguire workflow `build-ns7zip.yml` matrix (Win + Linux) PASS
- [ ] Eseguire workflow `self-test.yml` PASS
- [ ] Eseguire workflow `lint.yml` PASS
**Hook futuri Fase C**: la selezione del backend deve avvenire in
`job.py` leggendo `config.toml` `[backend].type` (default `workstation`).
Niente import diretto di `WorkstationVmrunBackend` in `job.py` — usare
factory `backends.load_backend(config)`.
**Test / validazione**:
- `pytest tests/test_commands_job.py` PASS con coverage ≥80% sul comando `job`
- Workflow `build-ns7zip.yml` end-to-end PASS dal nuovo entry point
- Output catturato da act_runner leggibile (no encoding broken, no ANSI rotti)
- Tempi job entro ±10% rispetto al baseline PS (misurati con `Measure-CIBenchmark` ancora in PS, lecito)
**Rollback**: `git checkout HEAD~1 -- gitea/actions/local-ci-build/action.yml runner/config.yaml`;
`Invoke-CIJob.ps1` originale è ancora in git history e referenziabile.
**Definizione di fatto step A4**:
- [x] `action.yml` chiama Python direttamente
- [ ] Workflow matrix Win+Linux PASS
- [x] `runner/config.yaml` ha `PYTHONIOENCODING=utf-8`
- [x] Coverage `job.py` ≥80%
- [ ] Misurazione tempo entro ±10% baseline
### A5 — Test, documentazione, cleanup
**Obiettivo**: chiudere debito tecnico, rimuovere shim ridondanti,
aggiornare documentazione, eseguire burn-in finale.
**Input / dipendenze**: A4 done.
**Attività**:
- [x] Implementare test pytest dedicati per ognuno degli errori frequenti `AGENTS.md` #9 (snapshot powered-off), #10 (`vmrun list` vs `getGuestIPAddress`), #11 (machine-id reset → unique clone-name guard), #12 (stderr handling → AutoAddPolicy + no shell-out)
- [x] Verificare con `grep_search` che gli shim PS abbiano ancora call site esterni (`Register-CIScheduledTasks.ps1`, doc, `Test-*.ps1`) → nessuno candidato a rimozione in A5; rimozione differita a B5 (systemd timers)
- [x] Lasciare `Install-CIToolchain-*.ps1`, `Prepare-*.ps1`, `Deploy-*.ps1` in PowerShell (girano dentro guest o per ricostruzione template — vedi §4 di [idea-1](idea-1-python-rewrite.md))
- [x] Aggiornare `AGENTS.md`: aggiungere sezione "Python development" (venv path, ruff, mypy strict, pytest, encoding utf-8, mappatura PS → Python)
- [x] Aggiornare `docs/ARCHITECTURE.md` con nuovo layout `src/ci_orchestrator/` + Phase C extension point
- [x] Aggiornare `README.md` con setup Python e quick-start CLI per tutti i 10 sub-comandi
- [ ] Portare `Test-CapacityBurnIn.ps1` in `commands/burnin.py` (o lasciare in PS se ancora utile come driver esterno) — non implementato in A5; lo shim attuale già delega via `job` Python
- [ ] Eseguire burn-in 4 job concorrenti × 10 round PASS sull'host Windows — richiede VM reali, a carico utente
- [ ] Aggiornare `docs/RUNBOOK.md` con sezione "Operare da Python CLI" — deferito a chiusura Fase B (RUNBOOK riscritto end-to-end in B-finale)
**Hook futuri Fase C**: documentare in `docs/ARCHITECTURE.md` il
contratto `VmBackend` come "extension point" e il selettore `[backend]`
in `config.toml` come unica via per aggiungere ESXi.
**Test / validazione**:
- `pytest -q` PASS, coverage globale ≥70%
- `ruff check` + `mypy --strict` puliti
- `lint.yml` PASS (PSSA su PS residui + ruff/mypy/pytest su Python)
- Burn-in 4×10 PASS senza VM orfane
- `grep_search Invoke-CIJob` nei workflow YAML ritorna 0 hit
**Rollback**: ripristino doc da git; ri-aggiungere shim rimossi se
qualche caller esterno non ancora migrato emerge.
**Definizione di fatto step A5**:
- [x] Errori #9#12 coperti da test pytest (`tests/python/test_agents_errors.py`, 12 test)
- [x] Documentazione aggiornata (`AGENTS.md`, `docs/ARCHITECTURE.md`, `README.md`)
- [x] Shim non referenziati rimossi → nessuno candidato (tutti referenziati da `Register-CIScheduledTasks.ps1`/doc/`Test-*.ps1`); rimozione differita a B5 con sostituzione systemd
- [x] Burn-in PASS — `Start-BurnInTest.ps1` 3 round × 4 job = 12/12, 0 orfani
- [x] **Fase A done**: validata end-to-end su hardware reale; mergiata in `main` (`2aa12bb`), tag `v2.0.0-phaseA`
## 2. Fase B — Migrazione host Linux Mint
> **VINCOLO CATEGORICO (non negoziabile): compatibilità host Windows
> permanente.** Fase B **aggiunge** il supporto host Linux **accanto**
> a Windows — non lo sostituisce né lo degrada. Windows host resta un
> target di **prima classe** e supportato, NON solo rollback. In
> concreto: **B8 NON va eseguito come rimozione reale** (l'host Windows
> resta operativo/usabile); nessun assunto Linux-only nel codice
> condiviso; la rimozione shim (B5) deve lasciare un modo Windows-nativo
> di invocare la CLI Python; ogni modifica Fase B deve dichiarare
> l'impatto su host Windows e preferire parità dual-host.
>
> **Parallelizzazione**: B1 può iniziare durante A3/A4 (setup hardware
> indipendente dal codice). B2, B3 possono iniziare appena B1 è done e
> A5 è done. B4B6 sono **strettamente sequenziali** dopo "A done".
### B1 — Setup host Linux Mint in parallelo
**Obiettivo**: preparare hardware Linux Mint con Workstation Pro Linux,
storage layout e venv Python pronti, senza impatto produzione.
**Input / dipendenze**: hardware target disponibile. Può girare in
parallelo a A3/A4. Riferimento `idea-2-linux-host.md` §4.
**Attività**:
- [ ] Installare Linux Mint LTS sull'hardware target + aggiornamenti (`apt full-upgrade`)
- [ ] Scaricare e installare VMware Workstation Pro Linux (bundle `.bundle` da Broadcom)
- [ ] Validare `vmrun -T ws --version` ritorna versione coerente
- [ ] Smoke test Workstation: creare VM trivia, clone, start, stop, delete via UI e via `vmrun`
- [ ] Configurare `vmnet8` NAT range `192.168.79.0/24` (allineato all'host Windows) editando `/etc/vmware/vmnet8/dhcpd/dhcpd.conf` o via `vmware-netcfg`
- [ ] Creare utente di servizio `ci-runner` (`useradd -r -m -s /bin/bash ci-runner`) con uid/gid dedicati
- [ ] Creare layout `/var/lib/ci/{build-vms,artifacts,templates,keys}` con ownership `ci-runner:ci-runner` mode 750
- [ ] Applicare ACL POSIX: `setfacl -d -m u:ci-runner:rwX /var/lib/ci/build-vms`
- [ ] Installare Python 3.11+ (`apt install python3.11 python3.11-venv`)
- [ ] Creare venv `/opt/ci/venv/` con `python3.11 -m venv` e installare il package: `/opt/ci/venv/bin/pip install -e /path/to/local-ci-cd-system`
- [ ] Validare `sudo -u ci-runner /opt/ci/venv/bin/python -m ci_orchestrator --help`
- [ ] Aggiungere `[paths.linux]` a `config.example.toml` con i path POSIX
**Hook futuri Fase C**: lo stesso host Linux farà da control plane se
si attiverà ESXi. Non installare nulla che assuma "VM girano in locale"
(es. condivisioni `vmware-shared-folders` host-only).
**Rischi specifici** (da `idea-2-linux-host.md` §6):
- `vmrun` su Linux ha differenze sottili nel parsing output → mitigazione: test del modulo `vmrun.py` su Linux subito in B1, confronto output con Windows
- Permessi `/var/lib/ci/build-vms/`: `vmrun` come `ci-runner` deve scrivere VMDK CoW → mitigazione: ACL POSIX + test scrittura
- `vmnet8` NAT range diverso → mitigazione: allineamento range a `192.168.79.0/24`
**Test / validazione**:
- `vmrun -T ws list` ritorna lista vuota o coerente
- `python -m ci_orchestrator --help` PASS da `ci-runner`
- ACL POSIX verificata con `getfacl /var/lib/ci/build-vms`
- `pytest` (subset cross-platform) PASS sul venv Linux
**Rollback**: il host Linux è fisicamente separato. Spegnerlo. Nessun
impatto su produzione Windows.
**Definizione di fatto step B1**:
- [ ] Workstation Pro Linux operativa
- [ ] Storage layout creato con ACL
- [ ] Python venv pronto e package installato
- [ ] `vmnet8` configurato
### B2 — Trasferimento template VM
**Obiettivo**: copiare i template VMware dall'host Windows a Linux
mantenendo snapshot integri.
**Input / dipendenze**: B1 done. Può girare durante A4/A5. Riferimento
`AGENTS.md` errore #9 (snapshot solo da powered-off).
**Attività**:
- [ ] Verificare assenza di `*.vmem` / `*.vmsn` di runtime in `F:\CI\Templates\<name>\` (template fully powered-off)
- [ ] Eseguire `rsync -av --progress /cygdrive/f/CI/Templates/ ci-runner@<linux-host>:/var/lib/ci/templates/` (o `scp -r` se rsync non disponibile)
- [ ] Verificare integrità: `find /var/lib/ci/templates -name '*.vmx' -exec vmrun -T ws listSnapshots {} \;`
- [ ] Aprire i `.vmx` su Workstation Linux per registrarli (eventuale prompt "I moved it / I copied it" → "I copied it")
- [ ] Validare snapshot `BaseClean` su `WinBuild2025.vmx`, `WinBuild2022.vmx`
- [ ] Validare snapshot `BaseClean-Linux` su `LinuxBuild2404.vmx`
- [ ] Smoke test: `python -m ci_orchestrator vm new --template /var/lib/ci/templates/WinBuild2025/WinBuild2025.vmx --snapshot BaseClean --name smoke1` + `wait-ready` + `vm remove`
**Hook futuri Fase C**: i template restano in formato VMX. Per ESXi
serviranno OVF (`ovftool`) — non fare nulla in B2, ma documentare che
il dataset attuale è la base per export futuro.
**Rischi specifici**:
- Performance I/O linked clone su ext4 vs NTFS → mitigazione: misurare in B7, eventualmente XFS/BTRFS
- Snapshot corrotto in transit → mitigazione: checksum (`sha256sum`) prima e dopo rsync
**Test / validazione**:
- `vmrun listSnapshots` ritorna gli snapshot attesi su tutti i template
- Smoke `vm new` + `wait-ready` PASS con WinRM da Linux verso guest Windows
- Smoke `vm new` + `wait-ready` PASS con SSH da Linux verso guest Linux
**Rollback**: i template originali su `F:\CI\Templates\` sono intatti.
Spegnere host Linux. Eventualmente eliminare `/var/lib/ci/templates/`.
**Definizione di fatto step B2**:
- [ ] Tutti i template registrati su Workstation Linux
- [ ] Snapshot integri verificati
- [ ] Smoke `vm new` PASS per Win e Linux
### B3 — Trasferimento credenziali e chiavi
**Obiettivo**: rendere disponibili sull'host Linux le credenziali
guest e i token Gitea, accessibili da `ci-runner` headless.
**Input / dipendenze**: B1 done. Può girare durante A5. Riferimento
`idea-2-linux-host.md` §6 (rischio Alta su keyring headless).
**Attività**:
- [ ] Copiare `F:\CI\keys\ci_linux` e `ci_linux.pub``/etc/ci/keys/` con `chmod 600` e `chown ci-runner:ci-runner`
- [ ] Re-store credenziale guest Windows: `sudo -u ci-runner secret-tool store --label='BuildVMGuest' service ci target BuildVMGuest`
- [ ] Re-store Gitea PAT: `sudo -u ci-runner secret-tool store --label='GiteaPAT' service ci target GiteaPAT`
- [ ] Validare lettura: `sudo -u ci-runner /opt/ci/venv/bin/python -c "import keyring; print(keyring.get_credential('BuildVMGuest', None))"`
- [ ] PoC headless: testare lettura keyring da contesto systemd (no sessione D-Bus utente). Se fallisce, implementare backend `keyring` file-based con vault `age` o `sops` come fallback documentato
- [ ] Documentare la scelta finale in `docs/HOST-SETUP.md`
**Hook futuri Fase C**: anche ESXi userà credenziali da `keyring` (user
vSphere). Lo stesso `KeyringCredentialStore` deve poter caricare `EsxiHost`
target — niente hard-coding di nomi credenziali.
**Rischi specifici**:
- `secret-tool` headless: act_runner systemd non ha D-Bus user session → mitigazione: backend file-based con `age` (decisione PoC in B3)
**Test / validazione**:
- Lettura keyring PASS da utente interattivo
- Lettura keyring PASS da contesto systemd (`systemd-run --uid=ci-runner ...` come simulazione)
- Pytest `test_credentials.py` con backend Linux PASS
**Rollback**: rimuovere `/etc/ci/keys/`, `secret-tool clear` per ogni
target. Credenziali su host Windows intatte.
**Definizione di fatto step B3**:
- [ ] Chiavi SSH copiate con perms corretti
- [ ] Credenziali nel keyring leggibili headless
- [ ] Strategia documentata in `docs/HOST-SETUP.md`
### B4 — Setup act_runner come systemd service
**Obiettivo**: act_runner Linux registrato verso Gitea e gestito da
systemd, equivalente al servizio Windows attuale.
**Input / dipendenze**: B1, B2, B3 done. Sequenziale dopo "A done"
(serve la CLI Python stabile per i comandi invocati dai workflow).
**Attività**:
- [ ] Scaricare binario act_runner Linux ≥ v1.0.2 in `/opt/ci/act_runner/`
- [ ] Generare token registrazione su Gitea per il nuovo runner
- [ ] Registrare runner: `act_runner register --no-interactive --instance <gitea-url> --token <t> --name ci-linux --labels windows-build:host,linux-build:host`
- [ ] Creare `/etc/systemd/system/act-runner.service`:
- `[Service]` `User=ci-runner`, `WorkingDirectory=/var/lib/ci/runner`, `ExecStart=/opt/ci/act_runner/act_runner daemon`
- `Environment="PYTHONIOENCODING=utf-8"`, `Environment="CI_ROOT=/var/lib/ci"`, `Environment="CI_TEMPLATES=/var/lib/ci/templates"`, `Environment="CI_BUILD_VMS=/var/lib/ci/build-vms"`, `Environment="CI_ARTIFACTS=/var/lib/ci/artifacts"`, `Environment="CI_KEYS=/etc/ci/keys"`
- `Restart=on-failure`, `RestartSec=10`
- [ ] `systemctl daemon-reload && systemctl enable --now act-runner.service`
- [ ] Validare `journalctl -u act-runner -f` mostra connessione a Gitea OK
- [ ] **NON** avviare ancora workflow di produzione — il runner Windows è ancora primario
**Hook futuri Fase C**: env vars `CI_*` restano valide; per ESXi si
aggiungeranno solo `CI_BACKEND=esxi` + sezione `[backend.esxi]` in
`config.toml`. `act-runner.service` non cambia.
**Rischi specifici**:
- Runner Linux in idle che intercetta job destinati a Windows → mitigazione: in B4 il runner viene registrato ma in stato `paused` lato Gitea, oppure label distinte temporanee fino a B6
**Test / validazione**:
- `systemctl status act-runner``active (running)`
- Runner visibile in Gitea admin UI come "online"
- Job di test manuale (workflow `self-test.yml`) PASS sul runner Linux
- `journalctl -u act-runner --since "5min ago"` senza errori critici
**Rollback**: `systemctl disable --now act-runner`, de-registrare
runner da Gitea. Runner Windows resta primario.
**Definizione di fatto step B4**:
- [ ] act-runner.service attivo e abilitato
- [ ] Runner online in Gitea
- [ ] `self-test.yml` PASS dal nuovo runner
- [ ] Logging via journald validato
### B5 — Conversione scheduled tasks → systemd timers
**Obiettivo**: tutti i task periodici del runner Windows sono
replicati come coppie `*.service` + `*.timer` su Linux.
**Input / dipendenze**: B4 done. Riferimento `Register-CIScheduledTasks.ps1`
per l'inventario dei task.
**Attività**:
- [x] Inventariare i task in `Register-CIScheduledTasks.ps1` (identificare cadenza e comando per ognuno)
- [x] Per `cleanup-orphaned-vms`: creare `ci-cleanup-orphaned-vms.service` (oneshot, `ExecStart=/opt/ci/venv/bin/python -m ci_orchestrator vm cleanup`) + `.timer` (`OnCalendar=hourly`)
- [x] Per `retention-policy`: creare `ci-retention-policy.service` + `.timer` (`OnCalendar=daily`)
- [x] Per `watch-disk-space`: `ci-watch-disk-space.service` + `.timer` (`OnCalendar=*:0/15`)
- [x] Per `watch-runner-health`: `ci-watch-runner-health.service` + `.timer` (`OnCalendar=*:0/5`)
- [x] Per `backup-template`: `ci-backup-template.service` + `.timer` (`OnCalendar=weekly`)
- [ ] `systemctl daemon-reload` e `systemctl enable --now <ognuno>.timer`
- [ ] Validare `systemctl list-timers` mostra tutti i timer schedulati
- [x] Documentare il mapping in `docs/HOST-SETUP.md`
**Hook futuri Fase C**: `vm cleanup` con backend ESXi userà la stessa
unit — il selettore backend è in `config.toml`, non nel comando.
**Rischi specifici**:
- Cadenza non perfettamente equivalente tra Task Scheduler e systemd timers → mitigazione: usare `OnCalendar` esplicito + `Persistent=true` per non perdere esecuzioni durante reboot
**Test / validazione**:
- `systemctl list-timers --all` mostra tutti i timer attesi
- Trigger manuale `systemctl start ci-cleanup-orphaned-vms.service` esegue senza errori
- `journalctl -u ci-*` mostra esecuzioni riuscite
**Rollback**: `systemctl disable --now <name>.timer` per ognuno;
rimuovere file unit. Task Scheduler Windows ancora attivo.
**Definizione di fatto step B5**:
- [ ] Tutti i task periodici come timer systemd attivi
- [ ] Trigger manuale di ognuno PASS
- [ ] Mapping documentato
### B6 — Cutover
**Obiettivo**: transizione produzione dall'host Windows al Linux in
una finestra di manutenzione concordata.
**Input / dipendenze**: A done, B1B5 done. Strettamente sequenziale.
**Attività**:
- [ ] Annunciare finestra di manutenzione
- [ ] Verificare nessun job in coda lato Gitea
- [ ] Stop act_runner sull'host Windows: `Stop-Service actions-runner` (PS 5.1, `$LASTEXITCODE` controllato)
- [ ] Disabilitare scheduled task Windows: `Unregister-ScheduledTask -TaskName 'CI-*' -Confirm:$false`
- [ ] Rsync incrementale finale `F:\CI\Artifacts\``/var/lib/ci/artifacts/` (preserva permessi)
- [ ] Verificare che il runner Linux sia "online" e non "paused" su Gitea
- [ ] Trigger manuale di workflow smoke (`self-test.yml`) PASS
- [ ] Trigger manuale `build-ns7zip.yml` matrix Win + Linux PASS
- [ ] Monitorare `journalctl -u act-runner -f` per ≥30 min sotto carico reale
**Hook futuri Fase C**: nessuno specifico in B6. Cutover non tocca
backend.
**Rischi specifici**:
- Gap di tempo tra stop runner Windows e start runner Linux durante il quale i job vanno in errore → mitigazione: cutover in finestra a zero traffic + Gitea UI "paused" preventivo
- WinRM da Linux verso guest Windows fallisce su edge case TLS → mitigazione: test in B2 + B4, fallback documentato in `idea-2-linux-host.md` §7 (dual-host)
**Test / validazione**:
- `self-test.yml` PASS dal runner Linux
- `build-ns7zip.yml` matrix Win+Linux PASS dal runner Linux
- Nessun errore in `journalctl -u act-runner --since "1h ago"` di severità critica
- Artifact correttamente in `/var/lib/ci/artifacts/`
**Rollback** (entro 30 min se PASS critico fallisce):
- `systemctl stop act-runner` su Linux
- `Start-Service actions-runner` su Windows
- `Register-ScheduledTask` per riabilitare task Windows
- Documentare incident e tornare a B1B5 per fix
**Definizione di fatto step B6**:
- [ ] Runner Linux primario, runner Windows fermo
- [ ] Workflow matrix PASS dal nuovo host
- [ ] Artifact migrati
- [ ] 30 min di esercizio senza incidenti
### B7 — Capacity burn-in sul nuovo host
**Obiettivo**: validare che il nuovo host regga il carico target con
tempi paragonabili.
**Input / dipendenze**: B6 done.
**Attività**:
- [ ] Eseguire burn-in 4 job concorrenti × 10 round su template `WinBuild2025`
- [ ] Eseguire burn-in 4 job concorrenti × 10 round su template `LinuxBuild2404`
- [ ] Confrontare tempi medi vs baseline pre-migrazione (registrato in A5)
- [ ] Verificare success rate 100% (no VM orfane, no failure transienti)
- [ ] Misurare uso disco `/var/lib/ci/build-vms/` durante e dopo burn-in (cleanup automatico)
- [ ] Documentare risultati in `docs/RUNBOOK.md`
**Hook futuri Fase C**: i risultati di B7 sono il baseline contro cui
confrontare un eventuale backend ESXi (criterio di "C done" in
`idea-3-esxi-support.md` §8).
**Rischi specifici**:
- Performance I/O ext4 inferiore → mitigazione: se delta >20%, valutare XFS/BTRFS
- VM orfane non pulite → mitigazione: timer `ci-cleanup-orphaned-vms` deve essere già attivo (B5)
**Test / validazione**:
- Burn-in PASS con success rate 100%
- Tempo medio entro ±20% baseline Windows
- Nessuna VM orfana al termine
- Uso disco torna al baseline post-cleanup
**Rollback**: non applicabile (B7 è validazione, non modifica). Se
fallisce, rollback a Windows (vedi B6 rollback).
**Definizione di fatto step B7**:
- [ ] Burn-in PASS
- [ ] Tempi documentati
- [ ] Nessuna VM orfana
### B8 — ~~Decommissioning host Windows~~ — ANNULLATO (vincolo categorico)
> **B8 NON va eseguito.** Vincolo categorico: l'host Windows resta
> supportato di prima classe in modo permanente (non solo rollback).
> Niente dismissione, niente riallocazione hardware che rimuova la
> capacità di girare su host Windows. Questo step resta solo come
> record storico del piano originale.
**Obiettivo (annullato)**: ~~dopo ≥1 settimana di stabilità, dismettere
host Windows.~~ → Host Windows mantenuto operativo a regime.
**Attività (NON eseguire la dismissione; resta solo, se utile):**
- [ ] (Facoltativo) Backup periodico di `F:\CI\` su archivio esterno
- [ ] Host Windows **resta installato e utilizzabile** (run dual-host)
- [ ] Documentare in `docs/RUNBOOK.md` come usare entrambi gli host
- [ ] ~~Riallocare hardware~~ — NON applicabile (Windows host permanente)
**Hook futuri Fase C**: nessuno.
**Rischi specifici**:
- Decommissioning prematuro elimina rollback → mitigazione: gating ≥1 settimana + ≥1 mese spento prima di riallocare
**Test / validazione**:
- Backup verificato (estrazione di prova in tmp)
- Host Windows spento, hardware integro
- Runbook aggiornato
**Rollback**: riaccendere host Windows, ripristinare scheduled task,
re-registrare runner Windows verso Gitea.
**Definizione di fatto step B8**:
- [ ] Backup `F:\CI\` archiviato e validato
- [ ] Host Windows spento ma intatto
- [ ] Runbook rollback documentato
- [ ] **Fase B done**: tutti i criteri di "fatto" §7 soddisfatti
## 3. Step trasversali (X)
### X1 — Lint dual-stack (PSSA + ruff/mypy)
**Obiettivo**: pipeline di lint unica che copre Python e PowerShell residuo.
**Input / dipendenze**: A1 (per la parte Python). Aggiornata in A5 e B8.
**Attività**:
- [ ] Aggiornare `gitea/workflows/lint.yml` con job `python-lint` (ruff + mypy + pytest) e job `pwsh-lint` (PSSA su `scripts/`, `template/`, `tests/`)
- [ ] In B8: rimuovere o relegare a job opzionale la parte PSSA (resta utile solo per `template/` Windows)
- [ ] Configurare `PSScriptAnalyzerSettings.psd1` per coprire `scripts/`, `template/`, `tests/` (già presente in repo)
- [ ] Configurare `pyproject.toml` con `[tool.ruff]`, `[tool.mypy]`, `[tool.pytest.ini_options]`
**Hook futuri Fase C**: nessuno specifico.
**Test / validazione**:
- `lint.yml` PASS con entrambi i job
- 0 errori PSSA, 0 errori ruff, 0 errori mypy strict
**Rollback**: ripristino `lint.yml` da git.
**Definizione di fatto step X1**:
- [ ] `lint.yml` con job dual-stack
- [ ] 0 warning bloccanti
- [ ] Documentato in `AGENTS.md`
### X2 — Observability
**Obiettivo**: log strutturati + integrazione journald in Fase B.
**Input / dipendenze**: A1 (logger setup); B4 (systemd journald).
**Attività**:
- [ ] Configurare `logging` Python con formatter strutturato (JSON o key=value), nessun colore ANSI obbligatorio (act_runner non li gestisce sempre)
- [ ] In B4: configurare service unit per inviare stdout/stderr a journald (default systemd) e validare `journalctl -u act-runner -o json`
- [ ] Documentare query journald utili in `docs/RUNBOOK.md`
**Hook futuri Fase C**: log strutturati facilitano correlazione job ↔ backend.
**Test / validazione**:
- `journalctl -u act-runner -o json` ritorna eventi parsabili
- Log Python catturato correttamente da act_runner stdout
**Rollback**: revert formatter logging.
**Definizione di fatto step X2**:
- [ ] Logging strutturato
- [ ] journald query documentate
### X3 — Gestione credenziali headless
**Obiettivo**: credenziali accessibili da contesto SYSTEM (Win) e
systemd (Linux) senza sessione utente interattiva.
**Input / dipendenze**: A1 (credential store); B3 (PoC headless Linux).
**Attività**:
- [ ] Documentare in `docs/HOST-SETUP.md` il problema noto: keyring Win sotto SYSTEM e keyring Linux sotto systemd richiedono soluzioni dedicate
- [ ] In Fase A: documentare l'uso corrente (Credential Manager con utente di servizio + DPAPI machine scope)
- [ ] In B3: PoC + decisione finale (Secret Service via D-Bus user@.service, oppure file vault `age`/`sops`)
- [ ] Implementare backend `keyring` file-based come opzione `KeyringFileBackend(vault_path, age_key_path)` se serve
- [ ] Test pytest che simulano contesto headless (mock `keyring.get_credential` ritorna `None` → fallback a file vault)
**Hook futuri Fase C**: stesso `CredentialStore` userà credenziali
vSphere — mantenere API generica.
**Test / validazione**:
- Lettura credenziale da contesto headless PASS in entrambi gli OS
- Test pytest fallback PASS
**Rollback**: usare credenziali in file plaintext non accettabile —
nessun rollback se PoC fallisce, va risolto come blocker.
**Definizione di fatto step X3**:
- [ ] Strategia documentata
- [ ] Test pytest fallback verde
- [ ] PoC Linux PASS
### X4 — Documentazione consolidata
**Obiettivo**: documentazione `docs/` allineata allo stato finale (host
Linux + Python).
**Input / dipendenze**: A5 e B8.
**Attività**:
- [ ] Aggiornare `docs/ARCHITECTURE.md`: layout `src/ci_orchestrator/`, Protocol `VmBackend`, hook ESXi
- [ ] Aggiornare `docs/HOST-SETUP.md`: setup Linux Mint, Workstation Pro Linux, ACL, keyring headless
- [ ] Aggiornare `docs/CI-FLOW.md`: nuovo entry point Python, env vars
- [ ] Aggiornare `docs/RUNBOOK.md`: comandi systemd, journalctl, troubleshooting
- [ ] Aggiornare `docs/BEST-PRACTICES.md`: convenzioni Python (no path hardcoded, ruff, mypy strict)
- [ ] Aggiornare `AGENTS.md`: sezione "Python development" + nota errore #12 N/A in Python
- [ ] Aggiornare `README.md`: quick-start Python + Linux
**Hook futuri Fase C**: lasciare placeholder "Backend ESXi (futuro)" in
`docs/ARCHITECTURE.md` e `docs/HOST-SETUP.md`.
**Test / validazione**:
- Lettura manuale: un nuovo operatore deve poter setuppare host Linux + lanciare un job seguendo solo `docs/`
- Link interni risolvono (`grep_search` su `\[.*\]\(.*\.md\)`)
**Rollback**: revert doc da git.
**Definizione di fatto step X4**:
- [ ] Docs aggiornate
- [ ] Link interni validi
- [ ] Quick-start testato da operatore esterno
## 4. Hook architetturali per Fase C
Punti di design da rispettare durante A e B per non bloccare un futuro
backend ESXi senza implementarlo:
- **Protocol `VmBackend`** in `src/ci_orchestrator/backends/protocol.py`:
metodi `clone_linked(template, snapshot, name) -> VmHandle`, `start(vm)`,
`stop(vm, hard=True)`, `delete(vm)`, `get_ip(vm, timeout) -> str`,
`list_snapshots(template) -> list[str]`. Firma neutra, nessun riferimento
a `vmrun`, `vmx`, `esxi` nei nomi pubblici.
- **`VmHandle` dataclass**: campo `id` opaco (string). Per Workstation
contiene il path VMX, per ESXi conterrà il MoRef. Chi consuma `VmHandle`
non può fare assunzioni sul contenuto.
- **Selezione backend via config**: `config.toml` ha sezione `[backend]`
con `type = "workstation"` (default) o `"esxi"` (futuro). Factory
`backends.load_backend(config)` istanzia il backend corretto. Niente
import diretto di `WorkstationVmrunBackend` nei comandi.
- **Separazione control plane / compute**: il control plane (host con
act_runner + Python) **non** deve assumere che le VM girino in locale.
Le funzioni `vm new` / `vm cleanup` ricevono il backend come parametro,
non leggono filesystem locale (eccetto per `WorkstationVmrunBackend`
internamente).
- **Naming neutro nelle API pubbliche**: i sottocomandi CLI sono
`vm new`, `vm remove`, `vm cleanup` (non `vmrun-clone`). Le env vars
sono `CI_BUILD_VMS`, `CI_TEMPLATES` (non `VMRUN_*`).
- **Credenziali**: `CredentialStore` accetta target name arbitrario.
Per ESXi si aggiungerà target `EsxiHost` senza modifiche al codice.
- **Logging strutturato**: include sempre `backend=<type>` e
`vm_id=<handle.id>` nei log per tracciabilità multi-backend.
Riferimento esplicito: [idea-3-esxi-support.md](idea-3-esxi-support.md)
§3 (interfaccia `EsxiBackend`) e §4 (mappa operazioni).
## 5. Cronoprogramma e dipendenze
```mermaid
flowchart TD
P0[0. Prerequisiti] --> A1
A1[A1 Bootstrap + core] --> A2[A2 Script foglia]
A2 --> A3[A3 Pipeline build]
A3 --> A4[A4 Orchestratore + workflow]
A4 --> A5[A5 Test + docs + cleanup]
A1 --> X1[X1 Lint dual-stack]
A1 --> X2[X2 Observability]
A1 --> X3[X3 Credenziali headless]
A3 -.parallelo.-> B1[B1 Setup host Linux]
A4 -.parallelo.-> B1
B1 --> B2[B2 Trasferimento template]
B1 --> B3[B3 Credenziali Linux]
A5 --> B4
B2 --> B4[B4 act_runner systemd]
B3 --> B4
B4 --> B5[B5 Timer systemd]
B5 --> B6[B6 Cutover]
B6 --> B7[B7 Burn-in Linux]
B7 --> B8[B8 Decommissioning Win]
A5 --> X4[X4 Docs consolidate]
B8 --> X4
X3 --> B3
X1 --> A5
X2 --> B4
```
## 6. Matrice rischi consolidata
Ogni voce: **Rischio** — Fase / Severità / Mitigazione / Owner action item.
- **`pypsrp` edge case con WinRM HTTPS self-signed**
- Fase: A1 — Severità: Media
- Mitigazione: PoC `wait-ready` come primo deliverable A1 prima di
committare al resto.
- Owner: A1 attività #10.
- **`keyring` sotto SYSTEM (act_runner service) non vede credenziali utente**
- Fase: A1, B3 — Severità: Alta
- Mitigazione: DPAPI machine scope su Win; PoC vault file `age`/`sops`
su Linux in B3.
- Owner: X3 attività complete.
- **Perdita di know-how errori `AGENTS.md` #9#12 durante refactor**
- Fase: A5 — Severità: Media
- Mitigazione: ognuno convertito in test pytest **prima** di rimuovere
il `.ps1` corrispondente.
- Owner: A5 attività #1.
- **Doppio mantenimento PS+Python durante migrazione**
- Fase: A2A4 — Severità: Alta
- Mitigazione: strategia shim minimizza la finestra; `lint.yml`
dual-stack.
- Owner: X1 attività complete.
- **Astrazione `VmBackend` over-engineered se Fase C non parte mai**
- Fase: A1 — Severità: Bassa
- Mitigazione: 1 implementazione concreta + Protocol = ~50 LOC extra;
costo trascurabile.
- Owner: accettato.
- **act_runner cattura male output Python (encoding cp1252)**
- Fase: A4 — Severità: Media
- Mitigazione: `PYTHONIOENCODING=utf-8` in `runner/config.yaml`.
- Owner: A4 attività #4.
- **`vmrun` su Linux ha differenze parsing output `list` / `getGuestIPAddress`**
- Fase: B1 — Severità: Media
- Mitigazione: test modulo `vmrun.py` su Linux subito in B1.
- Owner: B1 attività #3.
- **Performance I/O linked clone su ext4 vs NTFS**
- Fase: B7 — Severità: Bassa
- Mitigazione: misurare con burn-in B7; eventualmente XFS/BTRFS.
- Owner: B7 attività #5.
- **`vmnet8` NAT range diverso → IP collision LAN**
- Fase: B1 — Severità: Media
- Mitigazione: allineare `192.168.79.0/24` in B1.
- Owner: B1 attività #5.
- **WinRM HTTPS da Linux: TLS 1.2 cipher non supportati dal Windows guest**
- Fase: B2 — Severità: Media
- Mitigazione: smoke test prima del cutover; stesso codice già testato
in A1 da Win host.
- Owner: B2 attività #7.
- **Permessi `/var/lib/ci/build-vms/` per `ci-runner`**
- Fase: B1 — Severità: Media
- Mitigazione: ACL POSIX con `setfacl -d -m`.
- Owner: B1 attività #8.
- **`secret-tool` headless senza D-Bus session**
- Fase: B3 — Severità: Alta
- Mitigazione: PoC + fallback file vault `age`.
- Owner: B3 attività #5.
- **Errore `AGENTS.md` #11 (machine-id Linux) si ripropone**
- Fase: B2 — Severità: Bassa
- Mitigazione: già documentato; fix nel template prima dello snapshot.
- Owner: preservato.
- **Errore `AGENTS.md` #12 (stderr nativa + `'Stop'`)**
- Fase: A — Severità: N/A
- Mitigazione: sparisce in Python.
- Owner: dichiarato in A5.
- **Cutover gap window (job in errore tra stop Win e start Linux)**
- Fase: B6 — Severità: Media
- Mitigazione: finestra a zero traffic + Gitea "paused" preventivo.
- Owner: B6 attività #2.
- **Decommissioning prematuro elimina rollback**
- Fase: B8 — Severità: Media
- Mitigazione: gating ≥1 settimana + ≥1 mese spento prima di
riallocare.
- Owner: B8 attività #3.
- **(integrazione A↔B) Shim PS che assume path Windows si rompe in Fase B**
- Fase: A2, B6 — Severità: Media
- Mitigazione: shim deve invocare Python via path da env
(`%CI_PYTHON_VENV%`), non hardcoded `F:\`.
- Owner: A2 attività #7.
- **(integrazione A↔B) `config.toml` con default Win-only blocca B1**
- Fase: A1, B1 — Severità: Bassa
- Mitigazione: A1 deve già includere `[paths.linux]` con default POSIX.
- Owner: A1 attività #4.
- **(integrazione A↔B) Test pytest assumono `Path('F:\\CI')` e falliscono su Linux**
- Fase: A1, B1 — Severità: Media
- Mitigazione: fixture parametrizzata `tmp_path` in tutti i test, no
path hardcoded.
- Owner: A1 attività #11.
- **Licenza Workstation Pro Linux cambia post-Broadcom**
- Fase: B1 — Severità: Bassa
- Mitigazione: free per uso personale e commerciale dal 2024;
monitorare; piano B = KVM (rimandato).
- Owner: B1 monitoring continuo.
## 7. Definizione di "fatto" globale (A+B)
- [x] Tutti gli script `scripts/*.ps1` portati a Python o ridotti a shim (eccetto `template/*.ps1` e `Install-CIToolchain-*.ps1` che restano PS by design)
- [x] `Invoke-CIJob.ps1` non più referenziato nei workflow YAML (action invoca la CLI Python)
- [x] `pytest` verde con coverage ≥70% su `src/ci_orchestrator/`
- [x] `ruff check` + `mypy --strict` puliti su tutto `src/`
- [x] `lint.yml` dual-stack (PSSA per PS legacy + ruff/mypy/pytest per Python) PASS
- [ ] act_runner gira come `act-runner.service` systemd su Linux Mint
- [ ] Tutti i template VM (`WinBuild2025`, `WinBuild2022`, `LinuxBuild2404`) operativi dal nuovo host con snapshot integri
- [ ] Workflow `build-ns7zip.yml` matrix Win+Linux PASS dal nuovo host
- [ ] Burn-in capacity 4 job concorrenti × 10 round PASS, tempi entro ±20% baseline Windows
- [ ] Storage migrato a `/var/lib/ci/`, host Windows spento come rollback
- [ ] Tutti i timer systemd attivi e schedulati equivalenti ai Task Scheduler Windows
- [ ] Credenziali guest accessibili da `ci-runner` headless (strategia documentata)
- [ ] `README.md`, `AGENTS.md`, `docs/ARCHITECTURE.md`, `docs/HOST-SETUP.md`, `docs/CI-FLOW.md`, `docs/RUNBOOK.md` aggiornati allo stato finale
- [ ] ≥1 settimana di esercizio in produzione su host Linux senza incidenti
- [ ] Backup `F:\CI\` archiviato e validato
- [x] Hook §4 (Protocol `VmBackend`, factory backend, naming neutro) presenti nel codice e referenziati in `docs/ARCHITECTURE.md`
## 7.1 Auto-check
Domande di auto-verifica a cui il documento risponde "sì":
- **Ogni voce della checklist master compare almeno una volta come `- [ ]` dentro lo step corrispondente?** Sì: voci `[A1]…[A5]` mappate alle attività in §1, `[B1]…[B8]` in §2, `[X1]…[X4]` in §3.
- **Ogni step `A<n>`/`B<n>`/`X<n>` ha tutte le sottosezioni richieste?** Sì: Obiettivo, Input/dipendenze, Attività, Hook futuri Fase C, Test/validazione, Rollback, Definizione di fatto presenti per ogni step (B*: in più rischi specifici).
- **I 12 errori frequenti di `AGENTS.md` sono stati indirizzati o dichiarati non applicabili?**
- #1 (sintassi PS 7+): si applica solo agli shim PS residui in A2/A3 — vincolo PS 5.1 mantenuto
- #2 (`$LASTEXITCODE` non controllato): N/A in Python (`subprocess.returncode` esplicito)
- #3 (path hardcoded): risolto da A1 attività #4 (env vars + `config.toml`)
- #4 (mix branch WinRM/SSH): risolto dalla separazione `transport/winrm.py` vs `transport/ssh.py`
- #5 (WinRM verso Linux): impossibile per design — backend selezionato per `guest_os`
- #6 (seed ISO Linux): preservato nei template, non toccato in A/B
- #7 (`[ordered]@{}` PS 2): N/A
- #8 (`ForEach-Object -Parallel`): N/A in Python (concorrenza via `concurrent.futures` o `asyncio` se serve)
- #9 (snapshot powered-off): coperto da test pytest in A5 + procedura B2 attività #1
- #10 (`vmrun getGuestIPAddress`): coperto da test pytest in A1 attività #11
- #11 (machine-id Linux): preservato nel template, citato in B2 e in §6 rischi
- #12 (stderr nativa + `'Stop'`): N/A in Python, dichiarato in A5 e §6
- **Gli hook §4 sono coerenti con `idea-3-esxi-support.md` §3?** Sì: Protocol `VmBackend` ha esattamente i metodi `clone_linked`, `start`, `stop`, `delete`, `get_ip` previsti dalla firma `EsxiBackend`; selettore `[backend]` in `config.toml` come da §3 di idea-3.
## 8. Riferimenti
- [AGENTS.md](../AGENTS.md) — vincoli host, PowerShell 5.1, errori frequenti #1#12
- [plans/ideas-overview.md](ideas-overview.md) — razionale ordine fasi, criteri "fase completata", rollback
- [plans/idea-1-python-rewrite.md](idea-1-python-rewrite.md) — Fase A: design, mappa traduzione, step A1A5, layout repo, rischi
- [plans/idea-2-linux-host.md](idea-2-linux-host.md) — Fase B: hypervisor, mappa adattamento, step B1B8, rischi
- [plans/idea-3-esxi-support.md](idea-3-esxi-support.md) — Fase C: §2 architettura, §3 backend pyVmomi (solo per hook)
- [docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) — layout attuale del sistema
- [docs/CI-FLOW.md](../docs/CI-FLOW.md) — flusso job CI corrente
- [docs/HOST-SETUP.md](../docs/HOST-SETUP.md) — setup host Windows attuale
- [docs/RUNBOOK.md](../docs/RUNBOOK.md) — operatività corrente
- [scripts/](../scripts/) — inventario PowerShell da portare/preservare
- [template/](../template/) — script provisioning template (preservati in PS)