instancier le derivait, le playbook savait le poser, et la table qui les relie ne transmettait rien. Quatorze machines allaient naitre avec le ballooning desactive sans qu une seule etape echoue : variable vide, extra-var non passee, garde when: qui saute. Trois silences en file. P75 lit la cible creer-vm, releve chaque $SETOPS_X qu elle consomme et exige que la table qui les emet le declare. Eprouvee dans les deux sens. 74 OK, 0 echec, 1 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
236 lines
9.5 KiB
Python
236 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests stdlib (sans pytest) pour inventory_host.parametres_proxmox_hote.
|
|
|
|
Lancer : python3 scripts/tests/test_inventory_host.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from inventory_host import parametres_proxmox_hote # noqa: E402
|
|
|
|
|
|
def _inventaire_factice() -> dict:
|
|
return {
|
|
"all": {
|
|
"children": {
|
|
"hotes_planifies": {
|
|
"hosts": {
|
|
"app-01": {
|
|
"ansible_host": "10.0.2.11",
|
|
"ansible_user": "ansible",
|
|
"proxmox_cidr": 24,
|
|
"proxmox_coeurs": 4,
|
|
"proxmox_disque_taille": "160G",
|
|
"proxmox_memoire": 2048,
|
|
"proxmox_noeud": "noeud-a",
|
|
"proxmox_passerelle": "10.0.2.1",
|
|
"proxmox_stockage": "stockage-a",
|
|
"proxmox_vlan": 13,
|
|
"proxmox_vmid": 93101,
|
|
},
|
|
# app-02 : sans proxmox_noeud (champ optionnel).
|
|
"app-02": {
|
|
"ansible_host": "10.0.2.21",
|
|
"proxmox_cidr": 24,
|
|
"proxmox_disque_taille": "16G",
|
|
"proxmox_passerelle": "10.0.2.1",
|
|
"proxmox_stockage": "stockage-a",
|
|
"proxmox_vlan": 15,
|
|
"proxmox_vmid": 95201,
|
|
},
|
|
# incomplet-01 : il manque l'IP (champ requis).
|
|
"incomplet-01": {
|
|
"proxmox_vmid": 99901,
|
|
"proxmox_cidr": 24,
|
|
"proxmox_passerelle": "10.0.2.1",
|
|
"proxmox_vlan": 99,
|
|
},
|
|
}
|
|
},
|
|
"serveur_postgresql": {"hosts": {"app-01": {}}},
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def test_etiquette_vlan_repli_et_vide_explicite() -> None:
|
|
"""Absent -> repli sur proxmox_vlan ; present et VIDE -> reste vide.
|
|
|
|
C'est la distinction qui protege du double etiquetage : en SDN le VNet porte
|
|
deja le tag, et `proxmox_etiquette_vlan: ""` DIT qu'il ne faut pas en poser un
|
|
second. Un repli naif (`a or b`) ferait revenir l'etiquette et casserait le
|
|
rattachement — panne muette, decouverte au premier paquet.
|
|
"""
|
|
inv = _inventaire_factice()
|
|
# LA FIXTURE RANGE SES HOTES DANS `hotes_planifies`. Ce test cherchait
|
|
# `hotes_actifs` et levait donc un KeyError — mais il n'etait inscrit dans aucune
|
|
# liste, donc il n'a jamais tourne (corrige le 2026-08-28).
|
|
hote = inv["all"]["children"]["hotes_planifies"]["hosts"]["app-01"]
|
|
|
|
# 1. Cle absente : on retombe sur proxmox_vlan.
|
|
lignes = parametres_proxmox_hote(inv, "app-01")
|
|
assert "SETOPS_VLAN='13'" in lignes, lignes
|
|
|
|
# 2. Cle presente et vide (SDN) : aucune etiquette, malgre proxmox_vlan renseigne.
|
|
hote["proxmox_etiquette_vlan"] = ""
|
|
hote["proxmox_pont"] = "t17serv"
|
|
lignes = parametres_proxmox_hote(inv, "app-01")
|
|
assert "SETOPS_VLAN=''" in lignes, lignes
|
|
assert "SETOPS_PONT='t17serv'" in lignes, lignes
|
|
|
|
|
|
def test_hote_complet_avec_noeud() -> None:
|
|
lignes = parametres_proxmox_hote(_inventaire_factice(), "app-01")
|
|
attendu = {
|
|
"SETOPS_VMID='93101'",
|
|
"SETOPS_IP='10.0.2.11'",
|
|
"SETOPS_CIDR='24'",
|
|
"SETOPS_PASSERELLE='10.0.2.1'",
|
|
"SETOPS_VLAN='13'",
|
|
"SETOPS_PONT=''",
|
|
"SETOPS_STOCKAGE='stockage-a'",
|
|
"SETOPS_DISQUE='160G'",
|
|
"SETOPS_NOEUD='noeud-a'",
|
|
"SETOPS_COEURS='4'",
|
|
"SETOPS_MEMOIRE='2048'",
|
|
# Ajoute le 2026-09-13 : le PLANCHER de memoire (`balloon`), ce que l'hote a
|
|
# le droit de reprendre. Vide ici — la fixture ne le declare pas — et c'est le
|
|
# point : ce test epingle le CONTRAT, pas les valeurs. Il a signale l'ajout.
|
|
"SETOPS_MEMOIRE_MIN=''",
|
|
"SETOPS_DNS=''",
|
|
# Ajoute le 2026-08-09 : le domaine de recherche cloud-init se derive de
|
|
# `domaine_interne`, comme le DNS. Ce test epingle le CONTRAT de
|
|
# `parametres-proxmox` — un export de plus est un changement de contrat, et
|
|
# c'est bien lui qui l'a signale.
|
|
"SETOPS_DOMAINE=''",
|
|
# Ajoute le 2026-08-28 : la cle publique du runner de SITE, posee sur le SEUL
|
|
# hote qui porte `serveur_ops_tenant` (insemination). Vide ici, et c'est le
|
|
# point — `app-01` n'est le runner de personne.
|
|
"SETOPS_CLES_AMORCAGE=''",
|
|
}
|
|
assert set(lignes) == attendu, lignes
|
|
|
|
|
|
def test_cle_d_amorcage_seulement_pour_le_runner_du_tenant() -> None:
|
|
"""La cle du runner de SITE ne se pose QUE sur le runner du tenant.
|
|
|
|
C'EST LA GARDE QUI EMPECHE LE GESTE DE DERAPER. `creer-vm` cree TOUTES les machines
|
|
d'un tenant ; si cette cle sortait pour chacune, l'hebergeur obtiendrait un acces SSH
|
|
a la flotte entiere de chaque locataire — en silence, et sans qu'aucune regle de
|
|
pare-feu ne le dise. Le critere doit rester le meme que celui du flux : porter
|
|
`serveur_ops_tenant`.
|
|
"""
|
|
import inventory_host as ih
|
|
|
|
inv = _inventaire_factice()
|
|
inv["all"]["children"]["serveur_ops_tenant"] = {"hosts": {"app-01": None}}
|
|
|
|
cle = "ssh-ed25519 AAAATEST commentaire@avec-espaces"
|
|
lu = getattr(ih, "_cle_amorcage")
|
|
|
|
import underlay as u
|
|
original = u.lire_plan_site
|
|
u.lire_plan_site = lambda nom: {"runner_cle_publique": cle}
|
|
try:
|
|
assert lu(inv, "app-01") == cle, "le runner du tenant doit recevoir la cle"
|
|
assert lu(inv, "app-02") == "", "une machine ordinaire ne doit RIEN recevoir"
|
|
finally:
|
|
u.lire_plan_site = original
|
|
|
|
|
|
def test_cle_d_amorcage_absente_du_site_ne_casse_rien() -> None:
|
|
"""Un ecosysteme dont l'hebergeur ne declare pas de runner se materialise comme
|
|
avant. Degrader, jamais deviner."""
|
|
import inventory_host as ih
|
|
import underlay as u
|
|
|
|
inv = _inventaire_factice()
|
|
inv["all"]["children"]["serveur_ops_tenant"] = {"hosts": {"app-01": None}}
|
|
original = u.lire_plan_site
|
|
u.lire_plan_site = lambda nom: {}
|
|
try:
|
|
assert getattr(ih, "_cle_amorcage")(inv, "app-01") == ""
|
|
finally:
|
|
u.lire_plan_site = original
|
|
|
|
|
|
def test_hote_sans_noeud_emet_noeud_vide() -> None:
|
|
lignes = parametres_proxmox_hote(_inventaire_factice(), "app-02")
|
|
assert "SETOPS_NOEUD=''" in lignes, lignes
|
|
assert "SETOPS_VMID='95201'" in lignes, lignes
|
|
|
|
|
|
def test_intrant_commun_lu_a_cote_de_l_inventaire_recu() -> None:
|
|
"""`dns_amorcage` vit dans group_vars/all, pas dans les vars d'hote.
|
|
|
|
Et il doit etre lu A COTE de l'inventaire passe en argument : lire `instance/` en
|
|
dur ferait dependre le resultat de l'environnement, et un test synthetique irait
|
|
chercher les intrants de la production.
|
|
"""
|
|
import tempfile
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
base = Path(tmp)
|
|
(base / "group_vars" / "all").mkdir(parents=True)
|
|
(base / "group_vars" / "all" / "10-intrants.yml").write_text(
|
|
"dns_amorcage: 9.9.9.9\n", encoding="utf-8")
|
|
inv = base / "hosts.yml"
|
|
inv.write_text("all: {children: {}}\n", encoding="utf-8")
|
|
lignes = parametres_proxmox_hote(_inventaire_factice(), "app-01", inv)
|
|
assert "SETOPS_DNS='9.9.9.9'" in lignes, lignes
|
|
# Sans inventaire, aucun intrant commun n'est devine.
|
|
assert "SETOPS_DNS=''" in parametres_proxmox_hote(_inventaire_factice(), "app-01")
|
|
|
|
|
|
def test_hote_absent_refuse() -> None:
|
|
try:
|
|
parametres_proxmox_hote(_inventaire_factice(), "inconnu-01")
|
|
except ValueError as exc:
|
|
assert "absent" in str(exc), exc
|
|
else:
|
|
raise AssertionError("un hote absent doit lever ValueError")
|
|
|
|
|
|
def test_champ_requis_manquant_refuse() -> None:
|
|
try:
|
|
parametres_proxmox_hote(_inventaire_factice(), "incomplet-01")
|
|
except ValueError as exc:
|
|
assert "manquants" in str(exc) and "ansible_host" in str(exc), exc
|
|
else:
|
|
raise AssertionError("un champ requis manquant doit lever ValueError")
|
|
|
|
|
|
def main() -> int:
|
|
# LA LISTE EST EXPLICITE, DONC ELLE PEUT OUBLIER. Deux tests ecrits le 2026-08-28
|
|
# ont ete definis sans etre inscrits ici : ils passaient pour verts en n'etant
|
|
# jamais joues. On verifie donc que la liste couvre TOUT ce que ce fichier definit —
|
|
# un test non inscrit est pire qu'un test absent, parce qu'on croit l'avoir.
|
|
tests = [
|
|
test_hote_complet_avec_noeud,
|
|
# Jamais inscrit depuis sa creation, donc jamais joue : trouve par la garde
|
|
# ci-dessous, le jour meme ou elle a ete ecrite.
|
|
test_etiquette_vlan_repli_et_vide_explicite,
|
|
test_cle_d_amorcage_seulement_pour_le_runner_du_tenant,
|
|
test_cle_d_amorcage_absente_du_site_ne_casse_rien,
|
|
test_hote_sans_noeud_emet_noeud_vide,
|
|
test_intrant_commun_lu_a_cote_de_l_inventaire_recu,
|
|
test_hote_absent_refuse,
|
|
test_champ_requis_manquant_refuse,
|
|
]
|
|
definis = {n for n, v in sorted(globals().items())
|
|
if n.startswith("test_") and callable(v)}
|
|
oublies = definis - {t.__name__ for t in tests}
|
|
assert not oublies, f"test(s) definis mais jamais joues : {sorted(oublies)}"
|
|
for test in tests:
|
|
test()
|
|
print(f"ok {test.__name__}")
|
|
print(f"{len(tests)} tests passes.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|