diff --git a/roles/serveur_ops_site/defaults/main.yml b/roles/serveur_ops_site/defaults/main.yml index 9f3b660..4e522c7 100644 --- a/roles/serveur_ops_site/defaults/main.yml +++ b/roles/serveur_ops_site/defaults/main.yml @@ -64,3 +64,16 @@ serveur_ops_site_cle_deposer: false serveur_ops_site_cle_source: "" serveur_ops_site_cle_destination: >- {{ serveur_ops_site_racine }}/.config/setops-vault-{{ serveur_ops_site_depot | lower }} + +# LA CONFORMITE DE LA FABRIC (2026-09-28) — voir `templates/conformite-fabric.sh.j2`. +# Des plans de LECTURE seulement : aucun n'ecrit. +serveur_ops_site_conformite_plans: + - proxmox-fw-plan + - sdn-plan + - frontiere-plan + - vpn-admin-plan +serveur_ops_site_conformite_etat: "/var/lib/setops/conformite-fabric.json" +serveur_ops_site_conformite_horaire: "hourly" +# Au-dela, le constat est PERIME et la sonde passe au rouge : un minuteur mort ne doit pas +# laisser la derniere bonne nouvelle au vert. +serveur_ops_site_conformite_age_max_h: 3 diff --git a/roles/serveur_ops_site/meta/supervision.yml b/roles/serveur_ops_site/meta/supervision.yml index d478ead..c44aa0e 100644 --- a/roles/serveur_ops_site/meta/supervision.yml +++ b/roles/serveur_ops_site/meta/supervision.yml @@ -34,3 +34,9 @@ sondes: service en disparaissant — on s''en apercoit quand un ecosysteme doit naitre. Et une voute dechiffree en transit ne fait echouer aucun deploiement : Ansible la lit tres bien, elle expose simplement tout.' + - nom: fabric + ttl: 5400 + raison: >- + Le pare-feu Proxmox, le SDN, la frontiere et les acces WireGuard disent-ils encore ce + que le plan dit ? Le 2026-09-28, aucune VM des locataires n'avait son pare-feu actif + et rien ne le signalait ; les plans existaient, personne ne les jouait sans raison. diff --git a/roles/serveur_ops_site/tasks/main.yml b/roles/serveur_ops_site/tasks/main.yml index a1842a2..72d5d42 100644 --- a/roles/serveur_ops_site/tasks/main.yml +++ b/roles/serveur_ops_site/tasks/main.yml @@ -151,3 +151,48 @@ owner: root group: root mode: "0750" + +# LA CONFORMITE DE LA FABRIC (2026-09-28). Un minuteur joue les plans de lecture et +# consigne l'ecart ; la sonde « fabric » le lit. Voir `templates/conformite-fabric.sh.j2`. +- name: Assurer le repertoire d'etat + ansible.builtin.file: + path: /var/lib/setops + state: directory + owner: root + group: root + mode: "0750" + +- name: Deposer le constat de conformite de la fabric + ansible.builtin.template: + src: conformite-fabric.sh.j2 + dest: /usr/local/sbin/setops-conformite-fabric.sh + owner: root + group: root + mode: "0750" + +- name: Deposer l'unite et le minuteur du constat + ansible.builtin.template: + src: "{{ item }}.j2" + dest: "/etc/systemd/system/{{ item }}" + owner: root + group: root + mode: "0644" + loop: + - setops-conformite-fabric.service + - setops-conformite-fabric.timer + +- name: Activer le minuteur du constat + ansible.builtin.systemd: + name: setops-conformite-fabric.timer + enabled: true + state: started + daemon_reload: true + when: not ansible_check_mode + +- name: Deposer la sonde « fabric » + ansible.builtin.template: + src: sonde-fabric.sh.j2 + dest: /usr/local/lib/setops/sondes/fabric.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_ops_site/templates/conformite-fabric.sh.j2 b/roles/serveur_ops_site/templates/conformite-fabric.sh.j2 new file mode 100644 index 0000000..b4d9740 --- /dev/null +++ b/roles/serveur_ops_site/templates/conformite-fabric.sh.j2 @@ -0,0 +1,51 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_ops_site). Ne pas editer a la main. +# +# LA FABRIC DIT-ELLE ENCORE CE QUE LE PLAN DIT ? (2026-09-28) +# +# Joue les plans de LECTURE (aucune ecriture) et consigne, pour chacun, ce qu'il resterait +# a creer, retirer, mettre a jour ou corriger. La sonde « fabric » lit ce constat. +# +# POURQUOI : le 2026-09-28, aucune des 26 VM des locataires n'avait son pare-feu Proxmox +# actif — les reconstructions clonent sans ses options —, et rien ne le signalait. Le meme +# jour, un changement du moteur a fait vouloir a la frontiere une regle qui ne +# correspondrait jamais ; seul un plan relu a la main l'a montre. Les plans existaient : +# personne ne les jouait sans raison. +# +# Pourquoi un minuteur et pas la sonde elle-meme : une sonde dispose de quelques secondes, +# et ces plans interrogent l'API du cluster et celle de la frontiere. +set -uo pipefail + +RACINE={{ serveur_ops_site_racine }} +UTIL={{ serveur_ops_site_utilisateur }} +ETAT={{ serveur_ops_site_conformite_etat }} +# Le devis du pare-feu Proxmox resout les flux d'un locataire : n'importe lequel suffit, +# il est borne par `underlay.tenants`. +TENANT=$(ls -d "${RACINE}"/OPS-* 2>/dev/null | head -1) + +TMP=$(mktemp); trap 'rm -f "${TMP}"' EXIT +resultats=() +for plan in {{ serveur_ops_site_conformite_plans | join(' ') }}; do + runuser -u "${UTIL}" -- bash -lc "cd '${RACINE}/Set-OPS-public' && export PATH='${RACINE}/venv/bin':\$PATH && SETOPS_INSTANCE='${TENANT}' timeout 240 make ${plan}" > "${TMP}" 2>&1 + rc=$? + # La sortie passe par un FICHIER : `python3 -` lit deja son code sur l'entree standard. + resultats+=("$(python3 - "${plan}" "${rc}" "${TMP}" <<'EOF' +import json, re, sys +plan, rc, texte = sys.argv[1], int(sys.argv[2]), open(sys.argv[3], errors="replace").read() +a_faire = sum(int(n) for n in re.findall( + r"(?:a creer|a retirer|a mettre a jour|noeuds a corriger)\s*:\s*(\d+)", texte)) +detail = [l.strip() for l in texte.splitlines() if re.match(r"\s*[-+~]\s", l)][:6] +print(json.dumps({"plan": plan, "rc": rc, "a_faire": a_faire, "detail": detail})) +EOF +)") +done + +python3 - "${ETAT}" "${resultats[@]}" <<'EOF' +import json, sys, time, os +etat, lignes = sys.argv[1], sys.argv[2:] +doc = {"horodatage": int(time.time()), "plans": [json.loads(l) for l in lignes]} +tmp = etat + ".tmp" +with open(tmp, "w") as f: + json.dump(doc, f) +os.replace(tmp, etat) +EOF diff --git a/roles/serveur_ops_site/templates/setops-conformite-fabric.service.j2 b/roles/serveur_ops_site/templates/setops-conformite-fabric.service.j2 new file mode 100644 index 0000000..41723d4 --- /dev/null +++ b/roles/serveur_ops_site/templates/setops-conformite-fabric.service.j2 @@ -0,0 +1,8 @@ +# GENERE par Set-OPS (role serveur_ops_site). Ne pas editer a la main. +[Unit] +Description=La fabric dit-elle encore ce que le plan dit ? (plans de lecture, aucune ecriture) +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/setops-conformite-fabric.sh diff --git a/roles/serveur_ops_site/templates/setops-conformite-fabric.timer.j2 b/roles/serveur_ops_site/templates/setops-conformite-fabric.timer.j2 new file mode 100644 index 0000000..85f665d --- /dev/null +++ b/roles/serveur_ops_site/templates/setops-conformite-fabric.timer.j2 @@ -0,0 +1,12 @@ +# GENERE par Set-OPS (role serveur_ops_site). Ne pas editer a la main. +[Unit] +Description=Constat de conformite de la fabric + +[Timer] +OnCalendar={{ serveur_ops_site_conformite_horaire }} +OnBootSec=5min +RandomizedDelaySec=300 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/roles/serveur_ops_site/templates/sonde-fabric.sh.j2 b/roles/serveur_ops_site/templates/sonde-fabric.sh.j2 new file mode 100644 index 0000000..da3def9 --- /dev/null +++ b/roles/serveur_ops_site/templates/sonde-fabric.sh.j2 @@ -0,0 +1,38 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_ops_site). Ne pas editer a la main. +# +# SONDE « fabric » — le pare-feu Proxmox, le SDN, la frontiere et les acces WireGuard +# disent-ils encore ce que le plan dit ? +# +# Contrat : docs/supervision-conception.md — une ligne, 0/1/2/3. Elle ne JOUE pas les +# plans (trop lents pour une sonde) : elle lit le constat du minuteur +# `setops-conformite-fabric`. Un constat trop VIEUX est une panne a part entiere : sans +# lui, un minuteur mort laisserait la derniere bonne nouvelle au vert pour toujours. +set -uo pipefail + +python3 - {{ serveur_ops_site_conformite_etat }} {{ serveur_ops_site_conformite_age_max_h }} <<'EOF' +import json, sys, time +etat, age_max = sys.argv[1], int(sys.argv[2]) +try: + doc = json.load(open(etat)) +except (OSError, ValueError): + print(f"Aucun constat lisible ({etat}) : le minuteur setops-conformite-fabric a-t-il tourne ?") + sys.exit(2) +age_h = (time.time() - doc["horodatage"]) / 3600 +if age_h > age_max: + print(f"Constat PERIME : {age_h:.0f} h (seuil {age_max} h). Le minuteur ne mesure plus.") + sys.exit(2) +plans = doc["plans"] +illisibles = [p["plan"] for p in plans if p["rc"] != 0] +ecarts = [p for p in plans if p["rc"] == 0 and p["a_faire"] > 0] +perf = " ".join(f"{p['plan']}={p['a_faire'] if p['rc'] == 0 else -1}" for p in plans) +if illisibles: + print(f"Plan(s) illisible(s) : {', '.join(illisibles)} — la fabric n'a pas pu etre relue. | {perf}") + sys.exit(3) +if ecarts: + quoi = "; ".join(f"{p['plan']} : {p['a_faire']} ({(p['detail'] or ['?'])[0][:60]})" for p in ecarts) + print(f"ECART avec le plan — {quoi}. Relire le plan, puis appliquer ou corriger. | {perf}") + sys.exit(1) +print(f"Fabric conforme au plan ({len(plans)} plans, constat de {age_h * 60:.0f} min). | {perf}") +sys.exit(0) +EOF