sonde fabric : le pare-feu, le SDN, la frontiere et les acces disent-ils le plan ?

Un minuteur horaire sur le runner du site joue les plans de lecture et
consigne l ecart ; la sonde lit le constat, et signale un constat perime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-09-28 16:56:17 -04:00
parent a401481ae9
commit 79fcff3274
7 changed files with 173 additions and 0 deletions

View file

@ -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

View file

@ -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.

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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