#!/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'", "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())