`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>
119 lines
4.9 KiB
Python
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:]))
|