#!/usr/bin/env python3 """Attend qu'une VM soit MATERIALISEE : demarree, et portant l'adresse que le plan lui destine. POURQUOI PAS SSH (mesure du 2026-08-27). `creer-vm` confirmait son succes en attendant une reponse SSH. Deux choses clochaient. La premiere est une question de PERIMETRE. Le runner du SITE materialise le terrain de tous les tenants, mais la frontiere lui refuse — deliberement — d'entrer chez eux : c'est le sens meme de l'isolation inter-tenant. La premiere VM creee par le runner a donc ete declaree en echec apres 600 secondes alors qu'elle tournait parfaitement. Ouvrir le SSH du runner vers tous les tenants aurait « repare » la mesure en detruisant ce qu'elle mesurait : une machine capable d'entrer chez chaque locataire est exactement ce que l'architecture cherche a ne pas avoir. La seconde est que SSH PROUVE MOINS. « Quelque chose ecoute sur le port 22 » ne dit ni quel systeme a demarre, ni si cloud-init a pose la bonne adresse. L'agent invite, lui, rapporte le systeme, le noyau et l'adresse EFFECTIVEMENT portee par l'interface — on verifie donc que la machine est celle que le plan decrivait, pas seulement qu'une machine repond. Et il n'ouvre rien : l'API des hyperviseurs est deja le flux par lequel on a cree la VM. Qui peut la creer peut la voir naitre. ATTENDRE LA DISPONIBILITE EST UN AUTRE GESTE, qui appartient a qui va CONFIGURER la machine : `make deployer` attend desormais SSH, cloud-init et la liberation de dpkg, juste avant d'en avoir besoin. Materialiser et configurer sont deux metiers. """ from __future__ import annotations import argparse import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from proxmox_api import Cluster # noqa: E402 def _noeud_de(api: Cluster, vmid: str) -> str | None: """Sur quel noeud vit ce VMID. Le plan ne le dit pas toujours — la fabric, si.""" noeuds = api("/nodes") if not isinstance(noeuds, list): return None for n in noeuds: nom = n.get("node") vms = api(f"/nodes/{nom}/qemu") if isinstance(vms, list) and any(str(v.get("vmid")) == str(vmid) for v in vms): return nom return None def _adresses(api: Cluster, noeud: str, vmid: str) -> list[str]: """Les IPv4 que l'agent invite declare, hors boucle locale. Un `_erreur` n'est pas un echec : tant que la machine demarre, l'agent ne repond pas encore. C'est la difference entre « pas encore » et « pas du tout », et seule l'expiration du delai tranche. """ rep = api(f"/nodes/{noeud}/qemu/{vmid}/agent/network-get-interfaces") if not isinstance(rep, dict) or "result" not in rep: return [] out = [] for i in rep["result"]: if i.get("name") == "lo": continue for a in i.get("ip-addresses") or []: if a.get("ip-address-type") == "ipv4": out.append(str(a.get("ip-address"))) return out def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--vmid", required=True) ap.add_argument("--ip", required=True, help="l'adresse que le plan destine a cette VM") ap.add_argument("--hote", default="", help="son nom, pour les messages") ap.add_argument("--delai", type=int, default=600) ap.add_argument("--noeud", default="") args = ap.parse_args(argv) api, _ = Cluster.depuis_hebergeur() nom = args.hote or f"VMID {args.vmid}" noeud = args.noeud or _noeud_de(api, args.vmid) if not noeud: print(f"ECHEC: VMID {args.vmid} introuvable sur la fabric.", file=sys.stderr) return 4 print(f"Attente de la materialisation de {nom} ({args.ip}) ", end="", flush=True) fin = time.time() + args.delai vues: list[str] = [] while time.time() < fin: vues = _adresses(api, noeud, args.vmid) if args.ip in vues: print(" ok") osinfo = api(f"/nodes/{noeud}/qemu/{args.vmid}/agent/get-osinfo") o = osinfo.get("result", {}) if isinstance(osinfo, dict) else {} print(f" {nom} : {args.ip} portee sur {noeud} — " f"{o.get('pretty-name') or o.get('name') or 'systeme inconnu'} " f"{o.get('kernel-release', '')}".rstrip()) return 0 print(".", end="", flush=True) time.sleep(5) print(f"\nECHEC: {nom} n'a pas porte {args.ip} apres {args.delai}s.", file=sys.stderr) if vues: print(f" L'agent invite rapporte pourtant : {', '.join(vues)}. " f"La machine demarre mais son adressage n'est pas celui du plan — " f"cloud-init, ou le pont/VLAN sur lequel elle est posee.", file=sys.stderr) else: print(" L'agent invite n'a rien rapporte : la machine ne demarre pas, ou " "`qemu-guest-agent` est absent du gabarit dore.", file=sys.stderr) return 4 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))