# Fase A — Rewrite in Python > **Stato**: piano esecutivo (parte committed della roadmap). > Prerequisito di [Fase B](idea-2-linux-host.md). Vedi [overview](ideas-overview.md). > Baseline: ~13.000 righe PowerShell 5.1 (41 file `.ps1` + 2 `.psm1`), 2 script > bash, 5 workflow Gitea Actions. --- ## 1. Obiettivo Riscrivere host orchestrator + script di gestione VM in **Python 3.11+**, **con design cross-platform fin dall'inizio** (la Fase B trasferirà l'host su Linux Mint senza riscrivere il core). Vincoli mantenuti: - runtime act_runner + Gitea (consuma comandi via shell, agnostico al linguaggio) - template VM (Windows con WinRM, Linux con SSH) - VMware Workstation Pro come hypervisor (`vmrun` invariato) - formato artifact e layout di storage - workflow YAML esistenti (`gitea/workflows/*.yml`) — cambia solo la `shell:` e il comando invocato Il punto di ingresso `Invoke-CIJob.ps1` viene sostituito da `python -m ci_orchestrator job ...`, invocato da `gitea/actions/local-ci-build/action.yml`. --- ## 2. Decisioni di design forzate dal target Linux futuro Per evitare un doppio porting in Fase B, il codice Python deve rispettare da subito queste regole: | Regola | Implementazione | | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Nessun path hardcoded | Tutti i path via `pathlib.Path` + variabili ambiente (`CI_ROOT`, `CI_TEMPLATES`, `CI_BUILD_VMS`, `CI_ARTIFACTS`, `CI_KEYS`) con default in un file `config.toml`. | | Path-binari astratti | `vmrun`, `ssh`, `scp` cercati via `shutil.which()` con fallback a env var (`VMRUN_PATH`); default `vmrun.exe` su Windows, `vmrun` su Linux. | | Nessuna API Windows-only nel core | `keyring` (cross-platform), `paramiko` (no openssh client esterno), `pypsrp` (no `New-PSSession`). | | Astrazione hypervisor | Interfaccia `VmBackend` (Protocol) con metodi `clone_linked`, `start`, `stop`, `delete`, `get_ip`. Implementazione iniziale `WorkstationVmrunBackend`. Hook per `EsxiBackend` futuro (Fase C). | | Astrazione credential store | Interfaccia `CredentialStore` con backend `KeyringCredentialStore` (default) — funziona uguale su Win Credential Manager e Linux Secret Service. | | Logging | `logging` con formatter strutturato; nessun colore ANSI obbligatorio (alcuni runner cattura non li gestiscono). | | Path separator nei workflow YAML | Lasciare i workflow esistenti per ora; in Fase B si aggiorneranno i path (variabili env già pronte ad assorbire la modifica). | --- ## 3. Mappa di traduzione | Componente PowerShell | Equivalente Python | | -------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `Invoke-Vmrun` (wrapper `vmrun.exe` su Win / `vmrun` su Linux) | `subprocess.run([vmrun, '-T', 'ws', op, *args], check=False, capture_output=True)` | | `New-PSSession` + `Invoke-Command` (WinRM) | `pypsrp.client.Client(host, username, password, ssl=True, cert_validation=False)` | | `Copy-Item -ToSession / -FromSession` | `pypsrp.client.Client.copy()` / `fetch()` | | `_Transport.psm1` (`ssh.exe` / `scp.exe`) | `paramiko.SSHClient` / `paramiko.SFTPClient` | | Credential Manager (`BuildVMGuest`, `GiteaPAT`) | `keyring.get_credential('BuildVMGuest', None)` | | Pester | `pytest` + `pytest-mock` | | PSScriptAnalyzer | `ruff` + `mypy --strict` | | `Write-Host` | `logging.getLogger(__name__).info(...)` | | `ConvertTo-Json` per stato job | `json.dumps()` su dataclass | | Workflow YAML | invariati in Fase A | | Script `.sh` in `gitea/burnin-dummy/` | invariati (girano nel guest Linux) | --- ## 4. Cosa NON viene portato in Python Gli script `template/` (`Prepare-*.ps1`, `Deploy-*.ps1`, `Install-CIToolchain-*.ps1`) restano in PowerShell: - girano una tantum (ricostruzione template) - sono fortemente legati a Windows (autounattend.xml, registry, sysprep, WinRM enable) - gli script `Install-CIToolchain-WinBuild*.ps1` girano **dentro** il guest Windows e PS è il linguaggio nativo lì In Fase B questi script restano richiamabili da host Linux con `pwsh` 7 quando serve ricostruire un template Windows (operazione rara, anche manuale). --- ## 5. Strategia di migrazione: strangler fig in 4 step Sequenza obbligata. Ad ogni step, gli script PS non ancora portati continuano a funzionare; la coesistenza è garantita dalla stessa CLI `python -m ci_orchestrator` chiamata internamente dagli script `.ps1` man mano che vengono "scavati". ### Step A1 — Bootstrap progetto + moduli core (`_Common`, `_Transport`) Output: - `pyproject.toml`, package `src/ci_orchestrator/` - `venv` in `F:\CI\python\venv\` - moduli: `config.py` (env + TOML), `vmrun.py` (wrapper), `winrm.py` (pypsrp), `ssh.py` (paramiko), `credentials.py` (keyring), `backends/workstation.py` - protocollo `VmBackend` definito - test pytest per ognuno (mock `subprocess`, `paramiko`, `pypsrp`) Validazione: `lint.yml` e `self-test.yml` continuano a passare; nuovi job pytest aggiunti al workflow `lint.yml`. ### Step A2 — Script "foglia" (no state condiviso con orchestratore) Portare in Python (CLI subcommands `python -m ci_orchestrator `): - `Wait-VMReady` → `wait-ready` - `Remove-BuildVM` → `vm remove` - `Cleanup-OrphanedBuildVMs` → `vm cleanup` - `Watch-DiskSpace` → `monitor disk` - `Watch-RunnerHealth` → `monitor runner` - `Get-CIJobSummary` → `report job` Per ognuno: il file `.ps1` viene sostituito da uno **shim** di 3 righe che chiama `python -m ci_orchestrator ...`, così i call site esterni (scheduled tasks, ecc.) non si rompono. ### Step A3 — Pipeline di build Portare in Python: - `New-BuildVM` → `vm new` - `Invoke-RemoteBuild` → `build run` - `Get-BuildArtifacts` → `artifacts collect` Stessa strategia shim per i `.ps1` esistenti. ### Step A4 — Orchestratore + switch workflow Portare `Invoke-CIJob.ps1` → `python -m ci_orchestrator job`. Aggiornare `gitea/actions/local-ci-build/action.yml` per chiamare Python direttamente (non più via shim, per ridurre overhead startup). A questo punto i `.ps1` shim restano come fallback; eliminarli al primo ciclo di pulizia successivo. ### Step trasversale A5 — Test e documentazione Parallelo agli step A1-A4: - ogni feature portata ha test pytest che coprono almeno gli scenari documentati negli "errori frequenti" di `AGENTS.md` (#9, #10, #11, #12) - `AGENTS.md` aggiornato con sezione "Python development" (venv, stile, pytest, ruff) - `docs/ARCHITECTURE.md` aggiornato con il nuovo layout package - `README.md` aggiornato con setup Python --- ## 6. Layout repository post-Fase A ``` src/ci_orchestrator/ __init__.py __main__.py # entry point CLI (click) config.py # env vars + config.toml loader credentials.py # CredentialStore protocol + KeyringStore backends/ __init__.py protocol.py # VmBackend Protocol workstation.py # WorkstationVmrunBackend transport/ __init__.py winrm.py # pypsrp client wrapper ssh.py # paramiko client wrapper commands/ job.py # ex Invoke-CIJob vm.py # new / remove / cleanup build.py # build run artifacts.py # collect wait.py # wait-ready monitor.py # disk / runner report.py # job summary tests/ test_backends_workstation.py test_transport_winrm.py test_transport_ssh.py test_commands_job.py ... pyproject.toml config.example.toml scripts/ # PS legacy (shim sottili durante migrazione) template/ # PS invariati (provisioning template) gitea/ # workflow + actions invariati ``` --- ## 7. Rischi e mitigazioni | Rischio | Severità | Mitigazione | | ---------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ | | `pypsrp` ha edge case con WinRM HTTPS self-signed | Media | PoC su `wait-ready` come primo deliverable di Step A1 prima di committare al resto. | | `keyring` sotto SYSTEM account (act_runner service) può non vedere credenziali utente | Alta | Già un problema oggi con Credential Manager; documentare l'uso di credenziali a livello machine (DPAPI machine scope su Win, file vault con `age` su Linux). | | Perdita di know-how degli "errori frequenti" `AGENTS.md` durante refactor | Media | Convertire ognuno (#9, #10, #11, #12) in un test pytest **prima** di rimuovere il `.ps1` corrispondente. | | Doppio mantenimento durante la migrazione | Alta | Strategia shim minimizza la finestra; `lint.yml` deve passare per **entrambi** i mondi (PSSA + ruff). | | Astrazione `VmBackend` over-engineered se Fase C non parte mai | Bassa | Una sola implementazione concreta + Protocol = ~50 righe extra; costo trascurabile. | | act_runner cattura male l'output Python (encoding UTF-8 vs cp1252 su Win) | Media | Forzare `PYTHONIOENCODING=utf-8` in `runner/config.yaml` env. | --- ## 8. Definizione di "fatto" (Fase A) - [ ] Tutti gli script in `scripts/` portati a Python o ridotti a shim - [ ] `Invoke-CIJob.ps1` non più presente nei workflow YAML - [ ] `pytest` verde con coverage minima 70% su `src/ci_orchestrator/` - [ ] `ruff check` + `mypy --strict` puliti su tutto `src/` - [ ] `lint.yml` aggiornato: PSSA per file PS legacy + ruff/mypy per Python - [ ] Workflow `build-nsInnoUnp.yml` PASS su matrice Windows + Linux - [ ] Smoke test (`Test-Smoke.ps1` o equivalente Python) PASS - [ ] Capacity burn-in 4 job concorrenti PASS - [ ] `README.md`, `AGENTS.md`, `docs/ARCHITECTURE.md`, `docs/HOST-SETUP.md` aggiornati