Files
local-ci-cd-system/plans/archived/2026-05-23/implementation-plan-A-B.md
T
Simone 0d6486b19b chore(plans): archive Phase A and B completed documents
Moves all A/B closeouts, checklists, idea docs, and implementation plans
to plans/archived/2026-05-23/. Both phases are production-stable.

Active plans remaining in plans/:
  - idea-3-powershell-removal.md  (Phase C — in progress)
  - idea-3-esxi-support.md        (Phase D — future)
  - ideas-overview.md             (roadmap reference)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 19:52:09 +02:00

57 KiB
Raw Blame History

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 — Eliminazione dipendenza pwsh dall'host Linux
    • Output verificabile: bench run, validate host, smoke run portati in Python; pwsh disinstallabile senza impatti.
    • Stato: non in scope (da avviare dopo B stabile). Design in plans/idea-3-powershell-removal.md.
  • Fase D — (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 (smoke self-test PASS)
  • [A1] Test pytest unitari per vmrun.py, winrm.py, ssh.py, credentials.py con mock
  • [A1] Aggiungere job pytest a gitea/workflows/lint.yml
  • [A2] Portare Wait-VMReady.ps1python -m ci_orchestrator wait-ready
  • [A2] Portare Remove-BuildVM.ps1vm remove
  • [A2] Portare Cleanup-OrphanedBuildVMs.ps1vm cleanup
  • [A2] Portare Watch-DiskSpace.ps1monitor disk
  • [A2] Portare Watch-RunnerHealth.ps1monitor runner
  • [A2] Portare Get-CIJobSummary.ps1report job
  • [A2] Sostituire ognuno dei .ps1 portati con shim a 3 righe verso la CLI Python
  • [A3] Portare New-BuildVM.ps1vm new
  • [A3] Portare Invoke-RemoteBuild.ps1build run
  • [A3] Portare Get-BuildArtifacts.ps1artifacts collect
  • [A3] Convertire test Pester New-BuildVM.Tests.ps1, Wait-VMReady.Tests.ps1, Remove-BuildVM.Tests.ps1 in pytest
  • [A4] Portare Invoke-CIJob.ps1python -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-ns7zip.yml (matrix Win+Linux) end-to-end PASS (+ job release validato con tag throwaway)
  • [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 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*/var/lib/ci/keys/ con perms 600 owner ci-runner
  • [B3] Re-store credenziali BuildVMGuest e GiteaPAT con keyrings.alt.file.PlaintextKeyring (headless, no D-Bus — secret-tool scartato)
  • [B3] PoC accesso headless al keyring sotto systemd — PlaintextKeyring verificato PASS
  • [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; config.yaml con runner.envs per i path template
  • [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-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\
  • [B8] Decommissioning host Windows N/A — la macchina è dual-boot: Windows e Linux sullo stesso hardware, non girano contemporaneamente. Nessuna dismissione.
  • [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.

  • 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 (venv produzione F:\CI\python\venv, install non-editable)
  • Accesso amministrativo a Gitea per registrare/de-registrare runner durante A4 e B4
  • Credenziali BuildVMGuest e GiteaPAT nel vault SYSTEM (LocalSystem), username host-qualificato
  • 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à:

  • 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 D: 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à:

  • Implementare commands/wait.py con sottocomando wait-ready (parametri --vmx, --timeout, --guest-os)
  • Implementare commands/vm.py con sottocomando vm remove (parametri --vmx, --force)
  • Implementare vm cleanup (scan CI_BUILD_VMS, riconoscimento clone orfani per pattern naming + età)
  • Implementare commands/monitor.py con monitor disk (soglie configurabili, output JSON o human)
  • Implementare monitor runner (controllo processo act_runner attivo + ultimo heartbeat)
  • Implementare commands/report.py con report job (read-only su artifact dir + log job)
  • 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
  • Idem per Remove-BuildVM.ps1, Cleanup-OrphanedBuildVMs.ps1, Watch-DiskSpace.ps1, Watch-RunnerHealth.ps1, Get-CIJobSummary.ps1
  • 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 D: vm cleanup non deve assumere scan locale del filesystem — la firma deve accettare un VmBackend come dipendenza, così che in Fase D 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-readyvm 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 D: 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: powershellshell: 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-ns7zip.yml matrix (Win + Linux) PASS
  • Eseguire workflow self-test.yml PASS
  • Eseguire workflow lint.yml PASS

Hook futuri Fase D: 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:

  • 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 → unique clone-name guard), #12 (stderr handling → AutoAddPolicy + no shell-out)
  • 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)
  • Lasciare Install-CIToolchain-*.ps1, Prepare-*.ps1, Deploy-*.ps1 in PowerShell (girano dentro guest o per ricostruzione template — vedi §4 di idea-1)
  • Aggiornare AGENTS.md: aggiungere sezione "Python development" (venv path, ruff, mypy strict, pytest, encoding utf-8, mappatura PS → Python)
  • Aggiornare docs/ARCHITECTURE.md con nuovo layout src/ci_orchestrator/ + Phase C extension point
  • 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 D: 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 (tests/python/test_agents_errors.py, 12 test)
  • Documentazione aggiornata (AGENTS.md, docs/ARCHITECTURE.md, README.md)
  • Shim non referenziati rimossi → nessuno candidato (tutti referenziati da Register-CIScheduledTasks.ps1/doc/Test-*.ps1); rimozione differita a B5 con sostituzione systemd
  • Burn-in PASS — Start-BurnInTest.ps1 3 round × 4 job = 12/12, 0 orfani
  • 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 D: 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: COMPLETATO (2026-05-20)

  • Workstation Pro Linux operativa (/usr/bin/vmrun)
  • Storage layout /var/lib/ci/ creato con ACL, owner ci-runner
  • Python venv /opt/ci/venv/ pronto, ci_orchestrator installato
  • vmnet8 configurato su 192.168.79.0/24

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 D: 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: COMPLETATO (2026-05-21)

  • 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 D: 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: COMPLETATO (2026-05-21)

  • Chiavi SSH copiate con perms corretti
  • Credenziali nel keyring leggibili headless (PlaintextKeyringsecret-tool/D-Bus scartato)
  • Strategia documentata in docs/HOST-SETUP.md (pendente X3/X4)

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 D: 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-runneractive (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: COMPLETATO (2026-05-21)

  • act-runner.service attivo e abilitato
  • Runner online in Gitea
  • self-test.yml PASS dal nuovo runner (Win + Linux, entrambi i transport)
  • 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 D: 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 D: 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 D: 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 D: 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 D: 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 D: 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 D: 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 D: 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 D

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 §3 (interfaccia EsxiBackend) e §4 (mappa operazioni).

5. Cronoprogramma e dipendenze

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 D 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 (action invoca la CLI Python)
  • 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-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
  • 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 D, 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