Files
local-ci-cd-system/plans/PhaseB-user-checklist.md
T
Simone 4366ba6b04 docs: close Phase B Passo 9 — dual-boot procedure (RUNBOOK §15)
RUNBOOK §15: dual-boot operation — Linux⇄Windows switch procedure, per-OS
CI-stack table (roots/runner/vmrun), current GRUB behaviour (DEFAULT=0,
TIMEOUT=0 hidden → boots straight to Linux), post-boot verification commands
for both OSes, and an optional GRUB tweak (left unapplied — boot-to-Linux is
intentional so the box returns as the Linux CI host after any unattended
reboot).

Checklist: Passo 9 done; fixed the stale global tracking table (Passi 6–9
were still [ ] despite being complete). Phase B is now fully closed (1–9 [x]).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:38:32 +02:00

22 KiB
Raw Blame History

Fase B — Checklist finale per l'utente

La Fase B è la migrazione del runner CI dall'host Windows attuale a un nuovo host Linux Mint LTS con VMware Workstation Pro Linux.

Stato repo: solo lo step B5 (unit systemd per i task periodici) è già committato in deploy/systemd/ sul branch feature/python-rewrite-and-linux-migration (commit 50d37b5). Tutti gli altri step (B1B4, B6B8) sono operazioni hardware sull'host Linux nuovo e devono essere eseguiti dall'utente.

Avanzamento: B1 B2 B3 B4 — in corso: B5 (timer systemd).

Pre-requisito: la Fase A deve essere done (vedi plans/PhaseA-user-checklist.md Passo 9, merge + tag). B1, B2, B3 possono partire in parallelo a A3/A4/A5; B4B8 sono sequenziali e iniziano solo dopo "A done".

Tempo stimato totale: 12 giorni di lavoro distribuiti su una finestra di 12 settimane (per consentire il burn-in tra B6 e B8).


Passo 1 — Setup host Linux Mint (B1)

Obiettivo: hardware target con Linux Mint LTS, VMware Workstation Pro Linux, layout storage e venv Python pronti.

  • Installare Linux Mint LTS sull'hardware target.

  • sudo apt update && sudo apt full-upgrade -y && sudo reboot

  • Scaricare e installare VMware Workstation Pro Linux (bundle .bundle da Broadcow):

    sudo bash VMware-Workstation-Full-*.bundle
    vmrun -T ws --version    # deve ritornare versione coerente
    
  • Smoke test Workstation (UI): creare VM trivia, clone, start, stop, delete.

  • Configurare vmnet8 NAT range 192.168.79.0/24 (allineato a Windows) editando /etc/vmware/vmnet8/dhcpd/dhcpd.conf o via vmware-netcfg.

  • Creare utente di servizio ci-runner:

    sudo useradd -r -m -s /bin/bash ci-runner
    
  • Eseguire setup-host-linux.sh (copre mount disco, layout, Python venv, clone repo, PowerShell Core):

    # Clonare il repo prima di eseguire lo script
    git clone https://gitea.emulab.it/Simone/local-ci-cd-system.git /tmp/local-ci-cd-system
    
    sudo bash /tmp/local-ci-cd-system/setup-host-linux.sh -d /dev/sdf1
    

    Lo script:

    • Monta /dev/sdf1 su /var/lib/ci via fstab (boot automatico). Se la partizione ha una sottocartella ci/, ne sposta i contenuti alla root della partizione prima del mount definitivo.
    • Crea il layout build-vms/, artifacts/, templates/, keys/, logs/, runner/ con permessi e ACL corretti.
    • Installa Python 3.11, crea /opt/ci/venv, clona il repo in /opt/ci/local-ci-cd-system e installa il package.
    • Installa PowerShell Core.

    Flag utili se alcune dipendenze sono già presenti:

    sudo bash setup-host-linux.sh -d /dev/sdf1 --skip-pwsh --skip-clone
    

    Atteso a fine script: ci_orchestrator --help lista gli 11 sub-comandi.


Passo 2 — Trasferimento template VM (B2)

Obiettivo: copiare i template VMware da F:\CI\Templates\ su /var/lib/ci/templates/ mantenendo gli snapshot.

⚠️ AGENTS.md errore #9: i template devono essere fully powered-off prima della copia. Verificare assenza di *.vmem / *.vmsn di runtime.

  • Sull'host Windows:

    Get-ChildItem 'F:\CI\Templates' -Recurse -Include *.vmem,*.vmsn
    # atteso: nessun risultato
    
  • Sull'host Linux, eseguire rsync via SSH dall'host Windows (richiede OpenSSH Server attivo su Windows o, in alternativa, scp -r lanciato da Windows verso Linux):

    sudo -u ci-runner rsync -av --progress \
      user@windows-host:/cygdrive/f/CI/Templates/ \
      /var/lib/ci/templates/
    
  • Validare integrità snapshot:

    find /var/lib/ci/templates -name '*.vmx' -exec vmrun -T ws listSnapshots {} \;
    

    Atteso: BaseClean su WinBuild2025.vmx e WinBuild2022.vmx, BaseClean-Linux su LinuxBuild2404.vmx.

  • Aprire ciascun .vmx su Workstation Linux per registrarli; al prompt rispondere "I copied it".

  • Prerequisito smoke test Windows: il keyring file-based deve essere configurato e la credenziale BuildVMGuest deve essere presente prima di eseguire wait-ready (che prova WinRM). Questo è formalmente il Passo 3 (B3), ma il minimo indispensabile per lo smoke test è:

    # Configurare il backend file keyring per ci-runner (una sola volta)
    sudo -u ci-runner mkdir -p ~/.local/share/python_keyring
    sudo -u ci-runner bash -c "cat > ~/.local/share/python_keyring/keyringrc.cfg << 'EOF'
    [backend]
    default-keyring=keyrings.alt.file.PlaintextKeyring
    EOF"
    sudo -u ci-runner /opt/ci/venv/bin/pip install keyrings.alt -q
    
    # Salvare la credenziale guest Windows
    sudo -u ci-runner /opt/ci/venv/bin/python /opt/ci/local-ci-cd-system/tools/ci-cred-set.py BuildVMGuest "WINBUILD-2025\\ci_build"
    
    # Verificare
    sudo -u ci-runner /opt/ci/venv/bin/python /opt/ci/local-ci-cd-system/tools/ci-cred-check.py BuildVMGuest
    # atteso: OK (file)  username='WINBUILD-2025\ci_build'  password=****
    

    Il setup completo delle credenziali (chiavi SSH, GiteaPAT, PoC headless systemd) è nel Passo 3.

  • Smoke test pipeline VM:

    # Windows smoke
    CLONE_VMX=$(sudo -u ci-runner /opt/ci/venv/bin/python -m ci_orchestrator vm new \
      --template       /var/lib/ci/templates/WinBuild2025/WinBuild2025.vmx \
      --snapshot       BaseClean \
      --clone-base-dir /var/lib/ci/build-vms \
      --job-id         smoke-b2-win \
      --start)
    echo "Clone VMX: $CLONE_VMX"
    
    sudo -u ci-runner /opt/ci/venv/bin/python -m ci_orchestrator wait-ready \
      --vmx "$CLONE_VMX" --guest-os windows --timeout 300
    
    sudo -u ci-runner /opt/ci/venv/bin/python -m ci_orchestrator vm remove \
      --vmx "$CLONE_VMX" --force
    

    Ripetere per LinuxBuild2404 (snapshot BaseClean-Linux, --guest-os linux, --job-id smoke-b2-linux).


Passo 3 — Trasferimento credenziali e chiavi (B3)

Obiettivo: chiavi SSH e credenziali guest disponibili al user ci-runner headless.

  • Chiavi SSH guest Linux già presenti in /var/lib/ci/keys/ (copiate insieme ai template). Correggere solo i permessi:

    sudo chown ci-runner:ci-runner /var/lib/ci/keys/ci_linux /var/lib/ci/keys/ci_linux.pub
    sudo chmod 600 /var/lib/ci/keys/ci_linux
    sudo chmod 644 /var/lib/ci/keys/ci_linux.pub
    # verifica
    ls -la /var/lib/ci/keys/
    
  • Credenziale guest Windows BuildVMGuest già salvata nel prerequisito del Passo 2 con keyrings.alt.file.PlaintextKeyring (schema a due entry — secret-tool scartato perché non funziona headless senza D-Bus session).

    Per riscrivere la credenziale se necessario:

    sudo -u ci-runner /opt/ci/venv/bin/python /opt/ci/local-ci-cd-system/tools/ci-cred-set.py BuildVMGuest "WINBUILD-2025\\ci_build"
    sudo -u ci-runner /opt/ci/venv/bin/python /opt/ci/local-ci-cd-system/tools/ci-cred-check.py BuildVMGuest
    
  • Re-store Gitea PAT con lo stesso schema:

    sudo -u ci-runner /opt/ci/venv/bin/python /opt/ci/local-ci-cd-system/tools/ci-cred-set.py GiteaPAT ci-runner-linux
    sudo -u ci-runner /opt/ci/venv/bin/python /opt/ci/local-ci-cd-system/tools/ci-cred-check.py GiteaPAT
    
  • PoC headless: il backend PlaintextKeyring non usa D-Bus — funziona già da sudo -u ci-runner e funzionerà da act-runner.service. Verificato durante smoke test Passo 2.


Passo 4 — Setup act_runner come systemd service (B4)

Obiettivo: act_runner Linux registrato verso Gitea, gestito da systemd.

  • Scaricare il binario act_runner Linux ≥ v1.0.4:

    sudo mkdir -p /opt/ci/act_runner
    sudo wget -O /opt/ci/act_runner/act_runner \
      https://gitea.com/gitea/runner/releases/download/v1.0.4/gitea-runner-1.0.4-linux-amd64
    sudo chmod +x /opt/ci/act_runner/act_runner
    sudo chown -R ci-runner:ci-runner /opt/ci/act_runner
    
  • Generare token registrazione su Gitea (Admin → Runners → Create new runner).

  • Registrare il runner (sostituire <TOKEN>):

    sudo -u ci-runner mkdir -p /var/lib/ci/runner
    cd /var/lib/ci/runner
    sudo -u ci-runner /opt/ci/act_runner/act_runner register \
      --no-interactive \
      --instance https://gitea.emulab.it \
      --token <TOKEN> \
      --name ci-linux \
      --labels windows-build:host,linux-build:host
    

    ⚠️ Lasciare il runner in stato paused lato Gitea UI fino al cutover (Passo 6), per non intercettare job di produzione.

  • Creare /etc/systemd/system/act-runner.service:

    [Unit]
    Description=Gitea Act Runner
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=simple
    User=ci-runner
    WorkingDirectory=/var/lib/ci/runner
    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"
    ExecStart=/opt/ci/act_runner/act_runner daemon --config /var/lib/ci/runner/config.yaml
    Restart=on-failure
    RestartSec=10
    
    [Install]
    WantedBy=multi-user.target
    
  • Creare /etc/ci/environment (letto dalle unit dei task periodici di B5; vedi deploy/systemd/README.md):

    sudo tee /etc/ci/environment <<'EOF'
    

PYTHONIOENCODING=utf-8 CI_ROOT=/var/lib/ci CI_TEMPLATES=/var/lib/ci/templates CI_BUILD_VMS=/var/lib/ci/build-vms CI_ARTIFACTS=/var/lib/ci/artifacts CI_KEYS=/etc/ci/keys EOF sudo chmod 644 /etc/ci/environment


- [x] Abilitare e avviare:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now act-runner.service
sudo systemctl status act-runner
sudo journalctl -u act-runner -f    # Ctrl+C dopo conferma OK
  • Generare e configurare config.yaml del runner:

    # 1. Generare il file di default (conserva la registrazione in .runner)
    sudo -u ci-runner /opt/ci/act_runner/act_runner generate-config \
      | sudo -u ci-runner tee /var/lib/ci/runner/config.yaml > /dev/null
    
    # 2. Sostituire i label Docker generati di default con quelli host corretti
    #    (generate-config scrive label ubuntu-*:docker:// che fanno cercare Docker)
    sudo -u ci-runner sed -i \
      -e 's|    - "ubuntu-latest:docker://.*"|    - "windows-build:host"|' \
      -e '/ubuntu-24.04:docker:\/\//d' \
      -e '/ubuntu-22.04:docker:\/\//d' \
      /var/lib/ci/runner/config.yaml
    sudo -u ci-runner sed -i \
      '/    - "windows-build:host"/a\    - "linux-build:host"' \
      /var/lib/ci/runner/config.yaml
    
    # 3. Sostituire le variabili di esempio nella sezione runner.envs con le variabili CI
    #    (generate-config scrive due righe A_TEST_ENV_NAME_* come placeholder)
    sudo -u ci-runner sed -i \
      -e 's/    A_TEST_ENV_NAME_1: a_test_env_value_1/    CI_PYTHON_LAUNCHER: \/opt\/ci\/venv\/bin\/python/' \
      -e 's/    A_TEST_ENV_NAME_2: a_test_env_value_2/    GITEA_CI_TEMPLATE_PATH: \/var\/lib\/ci\/templates\/WinBuild2025\/WinBuild2025.vmx\n    GITEA_CI_LINUX_TEMPLATE_PATH: \/var\/lib\/ci\/templates\/LinuxBuild2404\/LinuxBuild2404.vmx/' \
      /var/lib/ci/runner/config.yaml
    
    # 4. Verificare
    grep -A3 'labels:' /var/lib/ci/runner/config.yaml | head -5
    grep -A4 'envs:' /var/lib/ci/runner/config.yaml
    

    Atteso:

      labels:
        - "windows-build:host"
        - "linux-build:host"
      envs:
        CI_PYTHON_LAUNCHER: /opt/ci/venv/bin/python
        GITEA_CI_TEMPLATE_PATH: /var/lib/ci/templates/WinBuild2025/WinBuild2025.vmx
        GITEA_CI_LINUX_TEMPLATE_PATH: /var/lib/ci/templates/LinuxBuild2404/LinuxBuild2404.vmx
    

    Le variabili di sistema (CI_ROOT, PYTHONIOENCODING, ecc.) restano nel service unit. runner.envs è per la configurazione specifica del runner CI (path template, launcher Python).

    # 5. Ricaricare e riavviare
    sudo systemctl daemon-reload
    sudo systemctl restart act-runner.service
    sudo journalctl -u act-runner -n 20    # confermare avvio OK — nessun errore Docker
    
  • Smoke job: triggerare manualmente self-test.yml selezionando il runner Linux (mettere temporaneamente in pausa quello Windows o usare label distinte). Atteso: PASS.

    ⚠️ Prerequisito: le variabili GITEA_CI_TEMPLATE_PATH e GITEA_CI_LINUX_TEMPLATE_PATH devono essere presenti nel service unit prima di avviare lo smoke test, altrimenti il job fallisce con "Got unexpected extra argument".


Passo 5 — Installare i timer systemd (B5)

Obiettivo: attivare le coppie .service+.timer già committate in deploy/systemd/ per replicare i task periodici di Register-CIScheduledTasks.ps1.

Nota: ci-retention-policy e ci-backup-template sono stati portati in Python puro (retention run / template backup) — pwsh non è richiesto. I backup template vengono compressi con 7z -mx=1 in /var/lib/ci/backups/. ci-watch-runner-health usa --service-name act-runner (nome Linux, non Windows).

Prerequisiti creati sull'host:

sudo mkdir -p /var/log/ci /var/lib/ci/backups
sudo chown ci-runner:ci-runner /var/log/ci
  • Copiare le unit:

    cd /opt/ci/local-ci-cd-system
    sudo cp deploy/systemd/ci-*.service deploy/systemd/ci-*.timer /etc/systemd/system/
    sudo systemctl daemon-reload
    
  • Abilitare e avviare tutti i timer:

    for t in ci-cleanup-orphans ci-retention-policy \
             ci-watch-disk-space ci-watch-runner-health \
             ci-backup-template; do
      sudo systemctl enable --now "${t}.timer"
    done
    
  • Verificare lo schedule:

    systemctl list-timers --all 'ci-*'
    

    Atteso: 5 timer in stato active. PASS

  • Trigger manuale di ciascun service per validare l'esecuzione one-shot:

    for s in ci-cleanup-orphans ci-retention-policy \
             ci-watch-disk-space ci-watch-runner-health \
             ci-backup-template; do
      sudo systemctl start "${s}.service"
      sudo systemctl status "${s}.service" --no-pager
    done
    

    Tutti e 5 PASS.

  • Per maggiori dettagli (mapping Windows→Linux, troubleshooting, rollback) vedi deploy/systemd/README.md.


Passo 6 — Cutover (B6)

Obiettivo: passare all'uso normale di Linux come OS primario di produzione CI. Il cutover è un semplice riavvio — i due sistemi non coesistono mai.

⚠️ Eseguire solo dopo che i Passi 15 sono tutti [x] PASS.

  • Verificare nessun job in coda lato Gitea (Admin → Actions → Tasks).

  • Attendere la fine di eventuali job in esecuzione su Windows.

  • Sbloccare il runner Linux su Gitea UI (rimuovere paused).

  • Riavviare la macchina in Linux.

  • Verificare runner Linux online su Gitea UI e act-runner attivo:

    sudo systemctl status act-runner
    sudo journalctl -u act-runner -n 20
    
  • Smoke trigger:

    • self-test.yml → PASS dal runner Linux
    • build-ns7zip.yml matrix Win+Linux → PASS dal runner Linux
  • Monitorare per ≥30 min:

    sudo journalctl -u act-runner -f
    

Rollback: riavviare in Windows — il runner Windows riprende automaticamente. Mettere in pausa il runner Linux su Gitea UI, documentare l'incidente in TODO.md.


Passo 7 — Capacity burn-in (B7)

Obiettivo: validare il carico target con tempi paragonabili al baseline Windows (entro ±20%). I due burn-in sono sequenziali (dual-boot: un OS alla volta).

Eseguire come ci-runner (utente di produzione — ha già accesso a /var/lib/ci/, keyring e ip-pool; evita i perm-hack del path simone). Verificato 2026-06-07.

Prerequisiti una-tantum:

  • Repo burnin-dummy esistente in Gitea (Simone/burnin-dummy, privato — l'auth del clone in-VM usa il PAT da keyring GiteaPAT). Deve contenere build.ps1 + build.sh. Push iniziale:
    cd /opt/ci/local-ci-cd-system/gitea/burnin-dummy
    git init && git add . && git commit -m "initial"
    git remote add origin http://10.10.20.11:3100/Simone/burnin-dummy.git
    git push -u origin main
    
  • ip-pool pulito prima di ogni burn-in: un run precedente killato può lasciare lease stale (il pool NON si auto-riconcilia, vedi RUNBOOK §9). Con build-vms/ vuoto, resettare:
    sudo -u ci-runner /opt/ci/venv/bin/python -c \
      "import pathlib; pathlib.Path('/var/lib/ci/ip-pool.json').write_text('{}\n')"
    

(Path alternativo simone): se proprio si vuole girare come simone, oltre a group ci-runner + copia keyring serve anche sudo chmod g+w /var/lib/ci (la root, non solo artifacts/+build-vms/ip-pool.tmp è scritto lì). Sconsigliato.

  • Avviato in Linux — burn-in 4 job concorrenti × 10 round su WinBuild2025 (esito: 40/40 PASS, round ~78.6 s):

    sudo -u ci-runner -H pwsh /opt/ci/local-ci-cd-system/scripts/Test-CapacityBurnIn.ps1 \
      -Parallelism 4 -Rounds 10 \
      -TemplatePath /var/lib/ci/templates/WinBuild2025/WinBuild2025.vmx \
      -RepoUrl 'http://10.10.20.11:3100/Simone/burnin-dummy.git' \
      -CloneBaseDir /var/lib/ci/build-vms/ \
      -ArtifactBaseDir /var/lib/ci/artifacts/ \
      -VmrunPath /usr/bin/vmrun \
      -BuildCommand 'powershell -ExecutionPolicy Bypass -File .\build.ps1'
    
  • Avviato in Linux — burn-in 4 × 10 su LinuxBuild2404 (esito: 40/40 PASS, round ~70.2 s):

    sudo -u ci-runner -H pwsh /opt/ci/local-ci-cd-system/scripts/Test-CapacityBurnIn.ps1 \
      -Parallelism 4 -Rounds 10 \
      -TemplatePath /var/lib/ci/templates/LinuxBuild2404/LinuxBuild2404.vmx \
      -SnapshotName BaseClean-Linux \
      -RepoUrl 'http://10.10.20.11:3100/Simone/burnin-dummy.git' \
      -CloneBaseDir /var/lib/ci/build-vms/ \
      -ArtifactBaseDir /var/lib/ci/artifacts/ \
      -VmrunPath /usr/bin/vmrun \
      -GuestOS Linux \
      -BuildCommand 'bash build.sh'
    

    Nota: -SnapshotName BaseClean-Linux è obbligatorio — il default dello script è BaseClean (Windows); senza, fallisce con Error: Invalid snapshot name 'BaseClean'.

  • Misurare:

    • Tempo medio per round, vs baseline §6–§8 — round near-deterministico (σ < 1.5 s), in linea con static-IP floor (RUNBOOK §9)

    • Tutti i 80 job (2 × 4 × 10) PASS

    • Zero VM orfane in /var/lib/ci/build-vms/:

      sudo -u ci-runner /opt/ci/venv/bin/python -m ci_orchestrator vm cleanup --what-if
      
    • Spazio disco /var/lib/ci/build-vms/ torna al baseline post-cleanup

  • Documentare i risultati in docs/RUNBOOK.md (§9 — Concurrent capacity burn-in).

  • Se delta > 20% vs baseline Windows: aprire issue in TODO.md con dettagli per profiling — N/A: nessun delta, burn-in PASS.


Passo 8 — Stabilità ≥1 settimana

Obiettivo: verificare ≥1 settimana di esercizio con la macchina avviata abitualmente in Linux senza incidenti critici.

Esito 2026-06-07: host live dal cutover B6 (~24 mag) → >2 settimane di esercizio. Salute al 7 giu: systemctl --failed vuoto, tutti i timer ci-* con ultimo run OK, nessun errore reale in act-runner (solo NamedPipeIPC_ServerListenerError cosmetici su exit dei processi pwsh). Nessun rollback. Le sessioni del 6 giu sono state hardening template Linux (no-IP/DHCP), non instabilità dell'host.

  • Esercizio normale per ≥7 giorni con boot in Linux.

  • Verifica giornaliera (bastano 2 minuti):

    sudo systemctl --failed                           # nessun service in failed
    systemctl list-timers --all 'ci-*'                # tutti gli ultimi run OK
    sudo journalctl -u act-runner --since "24h ago" -p err
    
  • Nessun rollback effettuato durante la settimana.


Passo 9 — Gestione dual-boot

La macchina è dual-boot: Windows e Linux coesistono sullo stesso hardware, non girano mai contemporaneamente. Non c'è dismissione — Windows resta pienamente funzionale e si usa riavviando nel boot entry corrispondente.

  • Aggiornare docs/RUNBOOK.md con la procedura dual-boot: vedi RUNBOOK §15 (switch Linux⇄Windows, verifica post-boot per entrambi gli OS, tweak GRUB opzionale).

  • (Facoltativo) GRUB verificato: GRUB_DEFAULT=0, GRUB_TIMEOUT=0, GRUB_TIMEOUT_STYLE=hidden → boot diretto in Linux (intenzionale: la macchina torna host Linux dopo ogni reboot unattended). Per switch a Windows: tasto Esc al boot, oppure grub-reboot con GRUB_DEFAULT=saved. Tweak documentato in RUNBOOK §15, non applicato (default Linux voluto).

    sudo nano /etc/default/grub    # GRUB_DEFAULT, GRUB_TIMEOUT
    sudo update-grub
    

Tracciamento globale

Passo Step Descrizione Stato
1 B1 Setup host Linux Mint (OS, Workstation, venv, ACL) [x]
2 B2 Trasferimento template VM + smoke vm new [x]
3 B3 Chiavi SSH + keyring (PoC headless) [x]
4 B4 act_runner systemd service + self-test.yml PASS [x]
5 B5 Timer systemd installati e attivi [x]
6 B6 Cutover (riavvio in Linux, runner Linux attivo) [x]
7 B7 Capacity burn-in 4 × 10 (Win + Linux, sequenziale) [x]
8 ≥1 settimana di stabilità (boot Linux abituale) [x]
9 Gestione dual-boot + RUNBOOK aggiornato (§15) [x]

Architettura: la macchina è dual-boot (stesso hardware). Avviata in Linux → runner Linux attivo. Avviata in Windows → runner Windows attivo. I due sistemi non girano mai contemporaneamente.

Quando i passi 1-8 sono [x], la Fase B è chiusa (B8 annullato per vincolo) e si può valutare l'apertura della Fase C (eliminazione pwsh dall'host Linux, vedi plans/idea-3-powershell-removal.md). La Fase D (backend ESXi, plans/idea-3-esxi-support.md) viene dopo.