#!/usr/bin/env python3 """`contexte.py` : un tronc commun, deux classes — et le contexte actif, nomme ou devine. Sur une arborescence FABRIQUEE (un site, deux locataires, un depot ambigu), on verifie : - qu'un depot dit ce qu'il est (site, locataire), et qu'un depot qui dit les deux est refuse ; - ce que chaque classe surcharge : index, voute, inventaire ; - les relations : `site.locataires()` (et les absents, nommes), `locataire.site()` ; - le contexte actif : le fichier `contexte` fait foi ; sans lui, les indices d'avant, et le locataire l'emporte quand les deux sont montes (le poste d'aujourd'hui) ; - un contexte nomme qui ne designe rien est un REFUS, pas un contexte vide. Puis, si les vrais depots sont la : chaque locataire declare par un site porte l'index que ce site lui attribue, et nomme un hebergeur qui existe ; la fiche de chaque hebergeur dit exactement ce que son locataire porte, et une fiche alteree (sept facons) est vue en ecart ; la face reseau de chaque locataire dit ce que les consommateurs du site en tirent, et une face alteree (huit facons) est vue en ecart ; machine par machine, le pare-feu du locataire et celui de Proxmox admettent les memes entrees, et des flux alteres (quatre facons) sont vus en ecart ; la frontiere designe chaque locataire par les memes identites, et une identite alteree (cinq facons) se voit ; ce qui entre depuis l'Internet concorde, et une entree alteree (quatre facons) se voit. """ from __future__ import annotations import os import sys import tempfile from pathlib import Path RACINE = Path(__file__).resolve().parents[2] sys.path.insert(0, str(RACINE / "scripts")) import contexte as C # noqa: E402 ECHECS: list[str] = [] def verifier(cond: bool, msg: str) -> None: print(("OK " if cond else "ECHEC ") + msg) if not cond: ECHECS.append(msg) def refuse(fonction, msg: str) -> None: try: fonction() except C.ContexteInvalide: verifier(True, msg) return verifier(False, msg + " (aucun refus)") def ecrire(chemin: Path, texte: str) -> None: chemin.parent.mkdir(parents=True, exist_ok=True) chemin.write_text(texte, encoding="utf-8") def fabriquer(d: Path) -> Path: """Un dossier d'ecosystemes : un moteur, un site, deux locataires, un depot ambigu.""" moteur = d / "Moteur" moteur.mkdir() ecrire(d / "SITE-Essai" / "underlay.yml", "underlay:\n index: 9\n tenants:\n OPS-Un: 5\n OPS-Absent: 6\n") ecrire(d / "SITE-Essai" / "underlay.vault.yml", "$ANSIBLE_VAULT;1.1;AES256\n") ecrire(d / "OPS-Un" / "plan" / "nomenclature.yml", "index: 5\n") ecrire(d / "OPS-Un" / "inventories" / "principal" / "hosts.yml", "all: {}\n") ecrire(d / "OPS-Un" / "inventories" / "principal" / "group_vars" / "all" / "vault.yml", "x\n") ecrire(d / "OPS-Un" / "parente.yml", "parente:\n depots:\n hebergeur:\n nom: SITE-Essai\n") ecrire(d / "OPS-Deux" / "plan" / "nomenclature.yml", "index: 7\n") ecrire(d / "OPS-Fantome" / "plan" / "nomenclature.yml", "index: 8\n") ecrire(d / "OPS-Fantome" / "parente.yml", "parente:\n depots:\n hebergeur:\n nom: SITE-Inexistant\n") ecrire(d / "Ambigu" / "underlay.yml", "underlay: {index: 1}\n") ecrire(d / "Ambigu" / "plan" / "nomenclature.yml", "index: 1\n") return moteur def classes(d: Path) -> None: site = C.Ecosysteme.depuis(d / "SITE-Essai") un = C.Ecosysteme.depuis(d / "OPS-Un") verifier(isinstance(site, C.Site) and site.nature == "site", "un depot a `underlay.yml` est un Site") verifier(isinstance(un, C.Locataire) and un.nature == "locataire", "un depot a nomenclature est un Locataire") refuse(lambda: C.Ecosysteme.depuis(d / "Ambigu"), "un depot qui porte `underlay.yml` ET une nomenclature est refuse") refuse(lambda: C.Ecosysteme.depuis(d / "Moteur"), "un depot qui ne declare rien est refuse") verifier(site.index == 9 and un.index == 5, "chaque classe lit SON index (underlay / nomenclature)") verifier(site.voute == d / "SITE-Essai" / "underlay.vault.yml" and site.a_sa_voute(), "la voute d'un site est `underlay.vault.yml`") verifier(un.voute == d / "OPS-Un" / "inventories" / "principal" / "group_vars" / "all" / "vault.yml" and un.a_sa_voute(), "la voute d'un locataire est dans son inventaire") verifier(un.inventaire() == d / "OPS-Un" / "inventories" / "principal" / "hosts.yml", "l'inventaire d'un locataire est son `hosts.yml` genere") verifier(site.inventaire().name == "site_inventaire.py", "l'inventaire d'un site est un script, pas un `hosts.yml`") verifier(un.cle_voute.name == "setops-vault-ops-un", "la cle de voute est nommee d'apres le depot") verifier(site.allocations() == {"OPS-Un": 5, "OPS-Absent": 6}, "le site rend les index qu'il attribue") verifier([l.nom for l in site.locataires()] == ["OPS-Un"], "`locataires()` rend les locataires presents") verifier(site.locataires_absents() == ["OPS-Absent"], "un locataire declare sans depot est NOMME absent") verifier(un.site() == site, "`locataire.site()` rend l'hebergeur que nomme `parente.yml`") verifier(C.Locataire(d / "OPS-Deux").site() is None, "sans hebergeur nomme, `site()` rend None") refuse(lambda: C.Locataire(d / "OPS-Fantome").site(), "un hebergeur nomme mais absent est refuse") refuse(lambda: C.Site.charger("OPS-Un", d), "`Site.charger` refuse un locataire") refuse(lambda: C.Locataire.charger("SITE-Essai", d), "`Locataire.charger` refuse un site") def contexte_actif(d: Path, moteur: Path) -> None: sauve = {k: os.environ.pop(k) for k in ("SETOPS_INSTANCE", "SETOPS_UNDERLAY") if k in os.environ} try: verifier(C.actif(moteur) is None, "rien de monte, rien de nomme : aucun contexte (None)") (moteur / "underlay.yml").symlink_to(d / "SITE-Essai" / "underlay.yml") verifier(C.actif(moteur) == C.Site(d / "SITE-Essai"), "le lien `underlay.yml` seul designe le site") (moteur / "instance").symlink_to(d / "OPS-Un") verifier(len(C.montes(moteur)) == 2, "les deux liens montes : deux ecosystemes designes") verifier(C.actif(moteur) == C.Locataire(d / "OPS-Un"), "les deux montes : le locataire l'emporte, comme la console d'aujourd'hui") os.environ["SETOPS_INSTANCE"] = str(d / "OPS-Deux") verifier(C.actif(moteur) == C.Locataire(d / "OPS-Deux"), "`SETOPS_INSTANCE` passe avant le lien") del os.environ["SETOPS_INSTANCE"] fichier = moteur / C.NOM_FICHIER_CONTEXTE fichier.write_text("# le contexte de ce poste\nsite:SITE-Essai\n", encoding="utf-8") verifier(C.actif(moteur) == C.Site(d / "SITE-Essai"), "le fichier `contexte` fait foi, meme quand les liens disent autre chose") fichier.write_text("locataire: OPS-Un\n", encoding="utf-8") verifier(C.actif(moteur) == C.Locataire(d / "OPS-Un"), "`locataire:` designe le locataire") for contenu, msg in (("site:SITE-Inexistant\n", "un site nomme qui n'existe pas"), ("locataire:SITE-Essai\n", "une nature qui ne correspond pas au depot"), ("hebergeur:SITE-Essai\n", "une nature inconnue"), ("site:SITE-Essai\nlocataire:OPS-Un\n", "deux valeurs"), ("\n# rien\n", "un fichier vide")): fichier.write_text(contenu, encoding="utf-8") refuse(lambda: C.actif(moteur), f"fichier `contexte` refuse : {msg}") finally: os.environ.pop("SETOPS_INSTANCE", None) os.environ.update(sauve) def vrais_depots() -> None: """Sur les depots reels, s'ils sont la : les deux sens de la relation concordent.""" sites = [C.Site(p.parent) for p in sorted(C.DOSSIER_ECOSYSTEMES.glob("SITE-*/underlay.yml"))] if not sites: print("SAUTE aucun site frere : rien de reel a confronter") return for site in sites: for loc in site.locataires(): attendu = site.allocations().get(loc.nom) verifier(attendu is None or loc.index == attendu, f"{site.nom} attribue {attendu} a {loc.nom}, qui porte l'index {loc.index}") heb = loc.nom_hebergeur() if heb: verifier((C.DOSSIER_ECOSYSTEMES / heb / "underlay.yml").is_file(), f"{loc.nom} nomme l'hebergeur {heb}, qui existe") verifier(not site.locataires_absents(), f"{site.nom} : aucun locataire declare sans depot ({site.locataires_absents() or 'ok'})") def monter_restaure() -> None: avant = {k: os.environ.get(k) for k in ("SETOPS_INSTANCE", "SETOPS_UNDERLAY")} try: with C._monter(C.Site(Path("/nulle/SITE")), C.Locataire(Path("/nulle/OPS"))): dedans = (os.environ.get("SETOPS_UNDERLAY"), os.environ.get("SETOPS_INSTANCE")) raise RuntimeError("sortie par exception") except RuntimeError: pass apres = {k: os.environ.get(k) for k in ("SETOPS_INSTANCE", "SETOPS_UNDERLAY")} verifier(dedans == ("/nulle/SITE/underlay.yml", "/nulle/OPS") and apres == avant, "`_monter` designe le site et le locataire voulus, puis remet l'environnement, meme sur exception") def fiches() -> None: """La fiche du site dit ce que chaque locataire porte — et `verifier_fiche` voit chaque ecart.""" import copy couples = C.couples_hebergeur() if not couples: print("SAUTE aucun site frere : aucune fiche a confronter") return for site, loc in couples: e = C.verifier_fiche(site, loc) verifier(not e, f"fiche {site.nom} -> {loc.nom} : identique a ce que le locataire porte" + (f" ({e[0]})" if e else "")) site, loc = couples[0] juste = site.fiche_pour(loc) alterations = { "index": lambda f: f["attribue"].__setitem__("index", (f["attribue"]["index"] or 0) + 1), "adresse publique": lambda f: f["attribue"].__setitem__("ip_publique", "192.0.2.99"), "intrant de service": lambda f: f["offre"].__setitem__("dns_amorcage", "192.0.2.53"), "racine de confiance": lambda f: f["racine_confiance"].__setitem__("sha256", "0" * 64), "serveurs de temps": lambda f: f["generation"].__setitem__("serveurs_temps", ["192.0.2.123"]), "delegation DNS": lambda f: f["generation"].__setitem__("resolveurs_site", ["192.0.2.1"]), "mode de routage": lambda f: f["generation"].__setitem__( "routage_tenants", "switch" if f["generation"]["routage_tenants"] == "sdn" else "sdn"), } for nom, alterer in alterations.items(): f = copy.deepcopy(juste) alterer(f) verifier(bool(C.verifier_fiche(site, loc, f)), f"une fiche alteree ({nom}) est vue en ecart") def faces() -> None: """La face reseau dit ce que les consommateurs du site en tirent — et chaque ecart se voit.""" import copy couples = C.couples_hebergeur() if not couples: print("SAUTE aucun site frere : aucune face reseau a confronter") return for site, loc in couples: e = C.verifier_face(site, loc) verifier(not e, f"face reseau {loc.nom} -> {site.nom} : identique a ce que le site en tire" + (f" ({e[0]})" if e else "")) site, loc = couples[0] juste = loc.face_reseau() groupe = next(g for g in ("serveur_postfix", "serveur_nginx", "client_backup") if juste["groupes"].get(g)) hote = juste["groupes"][groupe][0] def deplacer_machine(f): f["machines"][hote]["ip"] = "192.0.2.10" def retirer_du_groupe(f): f["groupes"][groupe] = [h for h in f["groupes"][groupe] if h != hote] alterations = { "index": lambda f: f.__setitem__("index", (f["index"] or 0) + 1), "adresse d'une machine": deplacer_machine, f"membres du groupe {groupe}": retirer_du_groupe, "administration declaree": lambda f: f["administration"].__setitem__("intrant", ["192.0.2.0/24"]), "tunnel d'administration": lambda f: f["administration"].__setitem__("tunnel", ["192.0.2.128/25"]), "zones publiques": lambda f: f["dns_public"].__setitem__("zones", ["exemple.invalid"]), "primaire DNS": lambda f: f["dns_public"].__setitem__("primaire", "192.0.2.53"), "cle de sauvegarde": lambda f: f["sauvegarde"].__setitem__("cle_publique", "ssh-ed25519 AAAA-fausse"), } for nom, alterer in alterations.items(): f = copy.deepcopy(juste) alterer(f) verifier(bool(C.verifier_face(site, loc, f)), f"une face reseau alteree ({nom}) est vue en ecart") def flux() -> None: """Machine par machine, le pare-feu du locataire et celui de Proxmox admettent les memes entrees — et une face dont les flux sont alteres se voit.""" import copy couples = C.couples_hebergeur() if not couples: print("SAUTE aucun site frere : aucun flux a confronter") return for site, loc in couples: e = C.verifier_flux(site, loc) verifier(not e, f"flux {loc.nom} : le locataire et Proxmox admettent les memes entrees" + (f" ({e[0]})" if e else "")) site, loc = couples[0] juste = loc.face_reseau() mta = next(h for h, f in juste["flux"].items() if any(x.get("sources_declarees") for x in f["entrees"])) autre = next(h for h, f in juste["flux"].items() if any(len(x.get("sources") or []) > 1 for x in f["entrees"])) def retirer_une_source(f): e = next(x for x in f["flux"][autre]["entrees"] if len(x.get("sources") or []) > 1) e["sources"] = e["sources"][1:] def ajouter_un_port(f): f["flux"][autre]["entrees"].append({"proto": "tcp", "port": "6666", "role": "x", "sources": ["192.0.2.66"]}) def effacer_declarees(f): for x in f["flux"][mta]["entrees"]: x.pop("sources_declarees", None) def retirer_admin(f): f["flux"][autre]["administration"] = [] for nom, alterer in (("une source retiree", retirer_une_source), ("un port ajoute", ajouter_un_port), (f"sources declarees effacees sur {mta}", effacer_declarees), ("administration retiree", retirer_admin)): f = copy.deepcopy(juste) alterer(f) verifier(bool(C.verifier_flux(site, loc, f)), f"des flux alteres ({nom}) sont vus en ecart") def frontiere() -> None: """La frontiere designe chaque locataire par les memes identites que sa face reseau et la fiche du site — et chaque identite alteree se voit.""" import copy couples = C.couples_hebergeur() if not couples: print("SAUTE aucun site frere : aucune frontiere a confronter") return for site, loc in couples: e = C.verifier_frontiere(site, loc) verifier(not e, f"frontiere {loc.nom} : alias, routes et traduction sortante concordent" + (f" ({e[0]})" if e else "")) site, loc = couples[0] face, fiche = loc.face_reseau(), site.fiche_pour(loc) groupe = next(g for g in ("serveur_postfix", "serveur_dovecot", "serveur_nginx") if face["groupes"].get(g)) alterations = { "zones": lambda f, fi: f.__setitem__("zones", f["zones"][:-1]), "administration declaree": lambda f, fi: f["administration"].__setitem__("intrant", ["192.0.2.0/24"]), "tunnel": lambda f, fi: f["administration"].__setitem__("tunnel", ["192.0.2.128/25"]), f"membres du groupe {groupe}": lambda f, fi: f["groupes"].__setitem__(groupe, []), "adresse publique attribuee": lambda f, fi: fi["attribue"].__setitem__("ip_publique", "192.0.2.60"), } for nom, alterer in alterations.items(): f, fi = copy.deepcopy(face), copy.deepcopy(fiche) alterer(f, fi) verifier(bool(C.verifier_frontiere(site, loc, f, fi)), f"une identite alteree ({nom}) est vue a la frontiere") def entrees_publiques() -> None: """Ce qui entre chez chaque locataire depuis l'Internet : la frontiere dit ce que la face ouvre a tous — et chaque alteration se voit.""" import copy couples = C.couples_hebergeur() if not couples: print("SAUTE aucun site frere : aucune entree publique a confronter") return for site, loc in couples: e = C.verifier_entrees_publiques(site, loc) verifier(not e, f"entrees publiques {loc.nom} : regles WAN, redirections et tunnel concordent" + (f" ({e[0]})" if e else "")) site, loc = couples[0] face, fiche = loc.face_reseau(), site.fiche_pour(loc) hote, entree = next((h, e) for h, f in face["flux"].items() for e in f["entrees"] if "0.0.0.0/0" in (e.get("sources") or []) and e.get("proto") == "tcp") autre = next(h for h in face["flux"] if h != hote) def fermer(f, fi): for e in f["flux"][hote]["entrees"]: if e.get("port") == entree["port"] and e.get("proto") == "tcp": e["sources"] = ["192.0.2.1"] e.pop("sources_declarees", None) alterations = { f"port {entree['port']} de {hote} ferme": fermer, "port public ajoute": lambda f, fi: f["flux"][autre]["entrees"].append( {"proto": "tcp", "port": "8443", "role": "x", "sources": ["0.0.0.0/0"]}), "adresse publique attribuee": lambda f, fi: fi["attribue"].__setitem__("ip_publique", "192.0.2.60"), "port du tunnel": lambda f, fi: f["administration"].__setitem__("port_tunnel", 51999), } for nom, alterer in alterations.items(): f, fi = copy.deepcopy(face), copy.deepcopy(fiche) alterer(f, fi) verifier(bool(C.verifier_entrees_publiques(site, loc, f, fi)), f"une entree publique alteree ({nom}) est vue a la frontiere") def main() -> int: with tempfile.TemporaryDirectory() as tmp: d = Path(tmp) moteur = fabriquer(d) classes(d) contexte_actif(d, moteur) vrais_depots() monter_restaure() fiches() faces() flux() frontiere() entrees_publiques() if ECHECS: print(f"\n{len(ECHECS)} echec(s).") return 1 print("\nLe contexte se nomme, se reconnait, et refuse ce qui ne designe rien.") return 0 if __name__ == "__main__": sys.exit(main())