Set-OPS-Public/scripts/tests/test_inventory_host.py

142 lines
4.8 KiB
Python
Raw Normal View History

#!/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": {}}},
}
}
}
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59) Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur infra-pki-01, et les treize suivantes auraient suivi. deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm : SETOPS_PONT='t11appl', SETOPS_VLAN=''. Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire est tombé, à raison — il couvre maintenant cette distinction. D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un tenant est à deux valeurs de la portabilité complète (noeud, stockage). D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint l'administration que depuis son propre domaine de diffusion. Ça tranche la question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une interface dédiée, ce qui lève l'objection qui la bloquait. D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du transport, soit le domaine qu'on venait d'en sortir. D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont. Régression créée puis corrigée : le modèle public, qui ne déclare aucun hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout l'underlay en disant que c'est un repli. 30 preuves OK, 4 tests unitaires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
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()
hote = inv["all"]["children"]["hotes_actifs"]["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'",
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59) Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur infra-pki-01, et les treize suivantes auraient suivi. deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm : SETOPS_PONT='t11appl', SETOPS_VLAN=''. Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire est tombé, à raison — il couvre maintenant cette distinction. D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un tenant est à deux valeurs de la portabilité complète (noeud, stockage). D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint l'administration que depuis son propre domaine de diffusion. Ça tranche la question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une interface dédiée, ce qui lève l'objection qui la bloquait. D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du transport, soit le domaine qu'on venait d'en sortir. D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont. Régression créée puis corrigée : le modèle public, qui ne déclare aucun hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout l'underlay en disant que c'est un repli. 30 preuves OK, 4 tests unitaires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
"SETOPS_PONT=''",
"SETOPS_STOCKAGE='stockage-a'",
"SETOPS_DISQUE='160G'",
"SETOPS_NOEUD='noeud-a'",
"SETOPS_COEURS='4'",
"SETOPS_MEMOIRE='2048'",
}
assert set(lignes) == attendu, lignes
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_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:
tests = [
test_hote_complet_avec_noeud,
test_hote_sans_noeud_emet_noeud_vide,
test_hote_absent_refuse,
test_champ_requis_manquant_refuse,
]
for test in tests:
test()
print(f"ok {test.__name__}")
print(f"{len(tests)} tests passes.")
return 0
if __name__ == "__main__":
raise SystemExit(main())