1034 lines
52 KiB
Markdown
1034 lines
52 KiB
Markdown
# 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-nsInnoUnp.yml` PASS.
|
||
- Stato: da iniziare.
|
||
- **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: da iniziare (gating: A done).
|
||
- **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
|
||
|
||
- [ ] [A1] Creare `pyproject.toml` + package `src/ci_orchestrator/` + venv `F:\CI\python\venv\`
|
||
- [ ] [A1] Implementare `config.py` (env vars + `config.toml`) con default Windows e hook path Linux
|
||
- [ ] [A1] Implementare `backends/protocol.py` con Protocol `VmBackend` (firma neutra, no `vmrun_*` nei nomi pubblici)
|
||
- [ ] [A1] Implementare `backends/workstation.py` (`WorkstationVmrunBackend`) usando `subprocess` + `shutil.which('vmrun')`
|
||
- [ ] [A1] Implementare `transport/winrm.py` con wrapper `pypsrp.client.Client`
|
||
- [ ] [A1] Implementare `transport/ssh.py` con wrapper `paramiko.SSHClient` / `SFTPClient`
|
||
- [ ] [A1] Implementare `credentials.py` con Protocol `CredentialStore` + `KeyringCredentialStore`
|
||
- [ ] [A1] PoC `wait-ready` end-to-end via `pypsrp` contro un guest Windows reale
|
||
- [ ] [A1] Test pytest unitari per `vmrun.py`, `winrm.py`, `ssh.py`, `credentials.py` con mock
|
||
- [ ] [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
|
||
- [ ] [A3] Portare `New-BuildVM.ps1` → `vm new`
|
||
- [ ] [A3] Portare `Invoke-RemoteBuild.ps1` → `build run`
|
||
- [ ] [A3] Portare `Get-BuildArtifacts.ps1` → `artifacts collect`
|
||
- [ ] [A3] Convertire test Pester `New-BuildVM.Tests.ps1`, `Wait-VMReady.Tests.ps1`, `Remove-BuildVM.Tests.ps1` in pytest
|
||
- [ ] [A4] Portare `Invoke-CIJob.ps1` → `python -m ci_orchestrator job`
|
||
- [ ] [A4] Aggiornare `gitea/actions/local-ci-build/action.yml` per invocare la CLI Python direttamente
|
||
- [ ] [A4] Forzare `PYTHONIOENCODING=utf-8` in `runner/config.yaml`
|
||
- [ ] [A4] Eseguire workflow `build-nsInnoUnp.yml` (matrix Win+Linux) end-to-end PASS
|
||
- [ ] [A5] Convertire errori frequenti `AGENTS.md` #9, #10, #11, #12 in test pytest dedicati
|
||
- [ ] [A5] Aggiornare `AGENTS.md` con sezione "Python development" (venv, ruff, mypy, pytest)
|
||
- [ ] [A5] Aggiornare `docs/ARCHITECTURE.md` con nuovo layout package
|
||
- [ ] [A5] Aggiornare `README.md` con setup Python
|
||
- [ ] [A5] Eseguire `Test-CapacityBurnIn` (versione Python) 4 job concorrenti PASS
|
||
- [ ] [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`
|
||
- [ ] [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-nsInnoUnp.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] Backup finale di `F:\CI\` su archivio
|
||
- [ ] [B8] Lasciare host Windows spento ma reinstallato per ≥1 mese come rollback
|
||
- [ ] [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:
|
||
|
||
- [ ] Repo `local-ci-cd-system` clonato e workflow `lint.yml` + `self-test.yml` verdi sull'host Windows attuale
|
||
- [ ] Backup integro di `F:\CI\Templates\` (snapshot `BaseClean` validato con `vmrun listSnapshots`)
|
||
- [ ] Python 3.11+ installato sull'host Windows e disponibile come `python` nel PATH dell'utente che gira act_runner
|
||
- [ ] Accesso amministrativo a Gitea per registrare/de-registrare runner durante A4 e B4
|
||
- [ ] Credenziali `BuildVMGuest` e `GiteaPAT` esistenti nel Credential Manager Windows e leggibili dall'utente di servizio
|
||
- [ ] `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)
|
||
|
||
### 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à**:
|
||
|
||
- [ ] Creare `pyproject.toml` (build backend `setuptools` o `hatchling`) con dipendenze `pypsrp`, `paramiko`, `keyring`, `click`, `tomli` (se Python <3.11), `rich` opzionale per logging
|
||
- [ ] Creare layout `src/ci_orchestrator/` con `__init__.py`, `__main__.py` (entry point `click`)
|
||
- [ ] Creare venv in `F:\CI\python\venv\` e installare il package in editable (`pip install -e .[dev]`)
|
||
- [ ] 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/...`)
|
||
- [ ] Implementare `backends/protocol.py` con Protocol `VmBackend` (metodi `clone_linked`, `start`, `stop`, `delete`, `get_ip`, `list_snapshots`) e dataclass `VmHandle` (path/identificatore opaco)
|
||
- [ ] 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`
|
||
- [ ] Implementare `transport/winrm.py`: wrapper su `pypsrp.client.Client(host, username, password, ssl=True, cert_validation=False)` con metodi `run`, `copy`, `fetch`
|
||
- [ ] 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)
|
||
- [ ] 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)
|
||
- [ ] Setup `pytest`, `pytest-mock`, `ruff`, `mypy` come dev dependencies; configurare `pyproject.toml` con `[tool.ruff]` e `[tool.mypy]` strict
|
||
- [ ] 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`)
|
||
- [ ] 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
|
||
- [ ] Coverage pytest ≥70% sui moduli core
|
||
- [ ] `lint.yml` aggiornato e verde
|
||
- [ ] Protocol `VmBackend` reviewato e congelato
|
||
- [ ] 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à**:
|
||
|
||
- [ ] Implementare `vm new` in `commands/vm.py` (parametri `--template`, `--snapshot`, `--name`, `--guest-os`)
|
||
- [ ] Integrare `WorkstationVmrunBackend.clone_linked` + `start` + attesa IP via `get_ip`
|
||
- [ ] Implementare `commands/build.py` con `build run` (parametri `--vmx`, `--script`, `--workdir`, `--guest-os`); usa `transport.winrm` per Windows, `transport.ssh` per Linux
|
||
- [ ] Gestione streaming output build verso stdout (act_runner cattura stdout — preservare comportamento `Write-Host`)
|
||
- [ ] Implementare `commands/artifacts.py` con `artifacts collect` (parametri `--vmx`, `--remote-path`, `--local-dir`)
|
||
- [ ] Sostituire `scripts/New-BuildVM.ps1`, `Invoke-RemoteBuild.ps1`, `Get-BuildArtifacts.ps1` con shim
|
||
- [ ] Convertire `tests/New-BuildVM.Tests.ps1` in `tests/test_commands_vm_new.py`
|
||
- [ ] Aggiungere test pytest per `build run` (mock transport + capture stdout)
|
||
- [ ] 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**:
|
||
|
||
- [ ] Pipeline build completa accessibile via Python CLI
|
||
- [ ] Shim PS preservano l'API per i caller esistenti
|
||
- [ ] Smoke build PASS contro VM reale Windows e Linux
|
||
- [ ] 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à**:
|
||
|
||
- [ ] Implementare `commands/job.py` con sottocomando `job` (entry point completo: parsing parametri job, clone, wait, build, collect, cleanup)
|
||
- [ ] Gestione errori e cleanup garantito (try/finally con `vm remove` anche su failure)
|
||
- [ ] Aggiornare `gitea/actions/local-ci-build/action.yml`: cambiare `shell: powershell` → `shell: cmd` o invocazione diretta `python -m ci_orchestrator job ...`
|
||
- [ ] Aggiungere `PYTHONIOENCODING=utf-8` in `runner/config.yaml` env
|
||
- [ ] Lasciare `Invoke-CIJob.ps1` come shim per scheduled task / chiamate manuali esterne (rimosso in A5)
|
||
- [ ] Test pytest end-to-end con mock backend + transport: `test_commands_job.py`
|
||
- [ ] Eseguire workflow `build-nsInnoUnp.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-nsInnoUnp.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**:
|
||
|
||
- [ ] `action.yml` chiama Python direttamente
|
||
- [ ] Workflow matrix Win+Linux PASS
|
||
- [ ] `runner/config.yaml` ha `PYTHONIOENCODING=utf-8`
|
||
- [ ] 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à**:
|
||
|
||
- [ ] Implementare test pytest dedicati per ognuno degli errori frequenti `AGENTS.md` #9 (snapshot powered-off), #10 (`vmrun list` vs `getGuestIPAddress`), #11 (machine-id reset), #12 (stderr handling — N/A per Python ma documentare)
|
||
- [ ] Rimuovere shim PS che non hanno call site esterni (verificare con `grep_search` che nessun task / doc li referenzia)
|
||
- [ ] 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))
|
||
- [ ] Aggiornare `AGENTS.md`: aggiungere sezione "Python development" (venv path, ruff, mypy strict, pytest, encoding utf-8)
|
||
- [ ] Aggiornare `docs/ARCHITECTURE.md` con nuovo layout `src/ci_orchestrator/`
|
||
- [ ] Aggiornare `README.md` con setup Python e quick-start CLI
|
||
- [ ] Portare `Test-CapacityBurnIn.ps1` in `commands/burnin.py` (o lasciare in PS se ancora utile come driver esterno)
|
||
- [ ] Eseguire burn-in 4 job concorrenti × 10 round PASS sull'host Windows
|
||
- [ ] Aggiornare `docs/RUNBOOK.md` con sezione "Operare da Python CLI"
|
||
|
||
**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**:
|
||
|
||
- [ ] Errori #9–#12 coperti da test pytest
|
||
- [ ] Documentazione aggiornata
|
||
- [ ] Shim non referenziati rimossi
|
||
- [ ] Burn-in PASS
|
||
- [ ] **Fase A done**: tutti i criteri di "fatto" §7 soddisfatti per la parte A
|
||
|
||
## 2. Fase B — Migrazione host Linux Mint
|
||
|
||
> **Parallelizzazione**: B1 può iniziare durante A3/A4 (setup hardware
|
||
> indipendente dal codice). B2, B3 possono iniziare appena B1 è done e
|
||
> A5 è done. B4–B6 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à**:
|
||
|
||
- [ ] Inventariare i task in `Register-CIScheduledTasks.ps1` (identificare cadenza e comando per ognuno)
|
||
- [ ] 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`)
|
||
- [ ] Per `retention-policy`: creare `ci-retention-policy.service` + `.timer` (`OnCalendar=daily`)
|
||
- [ ] Per `watch-disk-space`: `ci-watch-disk-space.service` + `.timer` (`OnCalendar=*:0/15`)
|
||
- [ ] Per `watch-runner-health`: `ci-watch-runner-health.service` + `.timer` (`OnCalendar=*:0/5`)
|
||
- [ ] 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
|
||
- [ ] 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, B1–B5 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-nsInnoUnp.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-nsInnoUnp.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 B1–B5 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 (opzionale)
|
||
|
||
**Obiettivo**: dopo ≥1 settimana di stabilità, dismettere host Windows.
|
||
|
||
**Input / dipendenze**: B7 done + ≥1 settimana di esercizio Linux
|
||
senza incidenti.
|
||
|
||
**Attività**:
|
||
|
||
- [ ] Backup finale di `F:\CI\` su archivio esterno (`tar` + checksum)
|
||
- [ ] Spegnimento runner Windows (`Stop-Service actions-runner`, runner già fermo da B6)
|
||
- [ ] Lasciare host Windows installato ma spento per ≥1 mese come rollback
|
||
- [ ] Documentare procedura di riaccensione in `docs/RUNBOOK.md`
|
||
- [ ] Dopo 1 mese: riallocare hardware se non ci sono stati rollback
|
||
|
||
**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: A2–A4 — 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)
|
||
|
||
- [ ] Tutti gli script `scripts/*.ps1` portati a Python o ridotti a shim (eccetto `template/*.ps1` e `Install-CIToolchain-*.ps1` che restano PS by design)
|
||
- [ ] `Invoke-CIJob.ps1` non più referenziato nei workflow YAML
|
||
- [ ] `pytest` verde con coverage ≥70% su `src/ci_orchestrator/`
|
||
- [ ] `ruff check` + `mypy --strict` puliti su tutto `src/`
|
||
- [ ] `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-nsInnoUnp.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
|
||
- [ ] 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 A1–A5, layout repo, rischi
|
||
- [plans/idea-2-linux-host.md](idea-2-linux-host.md) — Fase B: hypervisor, mappa adattamento, step B1–B8, 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)
|