Set-OPS-Public/scripts/attendre_materialisation.py
Daniel Allaire 6fe62f74e1 creer-vm : prouver la materialisation sans entrer chez le tenant
`creer-vm` confirmait son succes en attendant une reponse SSH. Le runner du SITE
materialise le terrain de TOUS les tenants, mais la frontiere lui refuse d'entrer
chez eux — c'est le sens meme de leur isolation. La premiere VM qu'il a creee a
donc ete declaree en echec apres 600 secondes alors qu'elle tournait, avec
l'adresse exacte que le plan lui destinait :

    Attente de SSH sur ops-01 ............ ECHEC: injoignable apres 600s.

LA TENTATION ETAIT D'OUVRIR LE SSH du runner vers tous les tenants. Ca aurait
repare la mesure en detruisant ce qu'elle protege : une machine capable d'entrer
chez chaque locataire est precisement ce que cette architecture refuse d'avoir.

L'agent invite repond sans rien ouvrir — l'API des hyperviseurs est deja le flux
par lequel la VM vient d'etre creee, donc qui peut la creer peut la voir naitre —
et il PROUVE DAVANTAGE. « Quelque chose ecoute sur le port 22 » ne dit ni quel
systeme a demarre, ni si cloud-init a pose la bonne adresse. Sur ops-01 :

    10.17.19.41 portee sur asgard — Debian GNU/Linux 13 (trixie) 6.12.101

Ses echecs distinguent deux causes tres differentes : « la machine demarre mais
rapporte une AUTRE adresse — cloud-init, ou le pont sur lequel elle est posee »,
et « introuvable sur la fabric ».

ATTENDRE LA DISPONIBILITE A CHANGE DE MAIN. Guetter cloud-init et la liberation de
dpkg appartient a qui va CONFIGURER : `deployer` le fait desormais, la ou il se
contentait d'un `ping` unique. Il y gagne ce qu'il n'avait pas — attendre le verrou
APT, faute de quoi la premiere couche echouait dessus. Contrepartie assumee : un
nom d'hote errone patiente au lieu d'echouer vite ; echouer vite interdirait de
chainer creation et deploiement, le geste central d'une reconstruction.

P52 garde le couplage ferme. Deux controles negatifs verifies.

make prouver : CONFORME, 52 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:48:49 -04:00

119 lines
4.9 KiB
Python

#!/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:]))