reconstruction : monter-flotte et parefeu-*-flotte entrent dans le code
La sequence du runner (flux, socle, deployer-tout, valider) devient make monter-flotte ; reconstruire s'appuie dessus. eprouver_parefeu.py --flotte : toutes les VM une a une, runner en dernier, arret au premier refus ; les reseaux des IPSet (zone d'administration) comptent enfin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9167f67c7b
commit
cc906af918
4 changed files with 155 additions and 9 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -1,5 +1,32 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-30 (51) — Ce qui était fait à la main dans les reconstructions entre dans le code
|
||||
|
||||
**Le bilan de l'exploitant** : aucune des deux reconstructions du jour n'est allée au bout
|
||||
seule. Au-delà des défauts corrigés, trois gestes vivaient hors du dépôt :
|
||||
|
||||
1. **La séquence du runner du locataire** était un script écrit dans `/tmp` du runner.
|
||||
Elle devient `make monter-flotte CONFIRMER=true` : `flux`, AC et DNS (`_amorcer-socle`),
|
||||
`deployer-tout`, `valider` — étapes horodatées, arrêt à la première en échec.
|
||||
`reconstruire` s'appuie désormais dessus (et gagne `valider`, qu'il n'avait pas).
|
||||
2. **La réactivation du pare-feu Proxmox** était une boucle tapée à la main.
|
||||
`eprouver_parefeu.py --flotte` porte la procédure pour toutes les VM, **une à une, le
|
||||
runner en dernier, arrêt au premier refus** — la règle qui la rend sûre vit avec elle.
|
||||
Cibles `parefeu-verifier-flotte` (mesure) et `parefeu-activer-flotte` (depuis le poste :
|
||||
lui seul tient à la fois les accès du locataire et le runner du site).
|
||||
3. **La sauvegarde fraîche avant de raser** reste une ÉTAPE du runbook `flotte-refaire`,
|
||||
pas une condition de `raser` : décision de l'exploitant, « une confirmation suffit ».
|
||||
|
||||
**`--flotte` a trouvé un défaut dès sa première passe** (lecture seule, Technolibre) : il
|
||||
s'est arrêté sur `infra-edge-01`, un flux `10.37.0.17 → 443` « hors des règles ». C'était le
|
||||
navigateur de l'exploitant, depuis la zone d'administration du site — légitime, et
|
||||
effectivement admis par Proxmox. La matrice ne retenait des IPSet que les membres qui sont
|
||||
des MACHINES ; les RÉSEAUX (`t23-admin` : `10.37.0.0/24`…) étaient ignorés, et l'observateur
|
||||
ne connaissait l'administration que sur le port 22. Les réseaux de chaque règle (exclusions
|
||||
`!` comprises) comptent désormais sur tous les ports. Seconde passe : **13/13, runner en
|
||||
dernier, aucun flux perdu**. Ce faux refus aurait bloqué une reconstruction le jour où
|
||||
l'exploitant a un onglet ouvert sur ses services — c'est-à-dire presque toujours.
|
||||
|
||||
## 2026-09-30 (50) — Le web frontal n'est plus un détenteur d'état
|
||||
|
||||
Le frontal RELAIE : ses vhosts, son WAF et sa page 404 se redéploient depuis le dépôt. Le
|
||||
|
|
|
|||
45
Makefile
45
Makefile
|
|
@ -1423,8 +1423,49 @@ reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM pu
|
|||
$(MAKE) flux; \
|
||||
$(MAKE) flotte-creer CONFIRMER=true; \
|
||||
$(MAKE) _attendre-flotte; \
|
||||
$(MAKE) _amorcer-socle; \
|
||||
$(MAKE) deployer-tout CONFIRMER=true
|
||||
$(MAKE) monter-flotte CONFIRMER=true
|
||||
|
||||
# MONTER UNE FLOTTE DONT LES VM EXISTENT DEJA (2026-09-30). C'est la seconde moitie de
|
||||
# `reconstruire`, et TOUT ce que le runner d'un locataire peut en faire : il ne cree pas de VM
|
||||
# (l'API du cluster est a l'hebergeur), il les recoit, armees, et les monte. Jusqu'ici cette
|
||||
# sequence vivait dans un script ecrit a la main dans /tmp du runner — deux reconstructions
|
||||
# l'ont portee, et aucune trace ne la gardait.
|
||||
#
|
||||
# `set -e` : la premiere etape en echec arrete tout. Chaque etape est horodatee, pour qu'un
|
||||
# journal dise ou l'on en etait sans relire dix mille lignes d'Ansible. `valider` fait partie
|
||||
# de la sequence : une flotte sans recette n'est pas montee, elle est deployee.
|
||||
.PHONY: monter-flotte
|
||||
monter-flotte: _instance-requise ## Monte une flotte deja creee : flux, AC et DNS, tout le reste, recette — CONFIRMER=true
|
||||
@set -e; \
|
||||
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
||||
printf '%s\n' 'Refus: MONTER la flotte (deploiement complet puis recette). Relancer avec CONFIRMER=true.'; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
etape() { printf '\n=== ETAPE %s %s\n' "$$1" "$$(date +%T)"; }; \
|
||||
etape flux; $(MAKE) flux; \
|
||||
etape _amorcer-socle; $(MAKE) _amorcer-socle; \
|
||||
etape deployer-tout; $(MAKE) deployer-tout CONFIRMER=true; \
|
||||
etape valider; $(MAKE) valider; \
|
||||
printf '\n=== FLOTTE MONTEE %s\n' "$$(date +%T)"
|
||||
|
||||
# LE PARE-FEU PROXMOX D'UNE FLOTTE RECONSTRUITE (2026-09-30). Une VM clonee nait sans les
|
||||
# options de pare-feu : chaque reconstruction les perd. La procedure (une VM a la fois,
|
||||
# matrice avant/apres, Icinga) etait rejouee par une boucle tapee a la main ; `--flotte` la
|
||||
# porte, avec la regle qui la rend sure : arret au premier refus.
|
||||
#
|
||||
# DEPUIS LE POSTE : la matrice se joue avec les acces du LOCATAIRE, l'activation passe par
|
||||
# le runner du SITE, seul a joindre l'API du cluster. Le poste est le seul a tenir les deux.
|
||||
.PHONY: parefeu-verifier-flotte parefeu-activer-flotte
|
||||
parefeu-verifier-flotte: ## Verifie, sans rien activer, ce que le pare-feu Proxmox couperait sur chaque VM — INSTANCE=OPS-X
|
||||
@if [[ -z "$(INSTANCE)" ]]; then printf '%s\n' 'Refus: INSTANCE=<OPS-X> requis.'; exit 2; fi
|
||||
python3 scripts/eprouver_parefeu.py --instance "$(INSTANCE)" --flotte --plan
|
||||
|
||||
parefeu-activer-flotte: ## Active le pare-feu Proxmox de chaque VM, une a une, arret au premier ecart — INSTANCE=OPS-X CONFIRMER=true
|
||||
@if [[ -z "$(INSTANCE)" || "$(CONFIRMER)" != "true" ]]; then \
|
||||
printf '%s\n' 'Refus: INSTANCE=<OPS-X> et CONFIRMER=true requis (active le pare-feu de TOUTES les VM, une a une).'; \
|
||||
exit 2; \
|
||||
fi
|
||||
python3 scripts/eprouver_parefeu.py --instance "$(INSTANCE)" --flotte --activer
|
||||
|
||||
# « Go ahead, make my day. » — LE bouton rouge : alias de reconstruire (Clint Eastwood).
|
||||
# Cree toutes les VM puis deploie tout, en une commande. Garde CONFIRMER=true.
|
||||
|
|
|
|||
|
|
@ -382,6 +382,14 @@ runbooks:
|
|||
pourquoi: >-
|
||||
Les VM manquantes, clonees depuis le gabarit dore. VMID, IP et VLAN sont DERIVES
|
||||
du plan : on ne les saisit nulle part.
|
||||
- cible: monter-flotte
|
||||
nature: ecriture
|
||||
duree: "long"
|
||||
fixes: {CONFIRMER: "true"}
|
||||
pourquoi: >-
|
||||
Des VM qui viennent de naitre (ou de renaitre) a la flotte recettee : flux, AC et
|
||||
DNS d'abord, tout le reste, puis `valider`. C'est la sequence du runner apres une
|
||||
reconstruction ; chaque proprietaire d'etat y remet celui d'avant.
|
||||
- cible: deployer-tout
|
||||
nature: ecriture
|
||||
duree: "long"
|
||||
|
|
@ -498,6 +506,21 @@ runbooks:
|
|||
- cible: valider
|
||||
nature: mesure
|
||||
pourquoi: "Une reconstruction sans recette n'a rien prouve."
|
||||
- cible: parefeu-verifier-flotte
|
||||
nature: mesure
|
||||
portee: poste
|
||||
variables: [INSTANCE]
|
||||
pourquoi: >-
|
||||
Ce que le pare-feu Proxmox couperait, VM par VM, avant de l'activer : une VM
|
||||
clonee nait sans ses options de pare-feu.
|
||||
- cible: parefeu-activer-flotte
|
||||
nature: ecriture
|
||||
portee: poste
|
||||
variables: [INSTANCE]
|
||||
fixes: {CONFIRMER: "true"}
|
||||
pourquoi: >-
|
||||
L'activer une VM a la fois, matrice avant et apres, Icinga ; arret au premier
|
||||
ecart, le runner en dernier.
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- id: gabarit
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ def _ansible(instance: str, hotes: str, commande: str) -> dict[str, str]:
|
|||
return sorties
|
||||
|
||||
|
||||
def matrice(instance: str, hote: str) -> tuple[list[dict], int]:
|
||||
def matrice(instance: str, hote: str) -> tuple[list[dict], int, dict[str, list]]:
|
||||
env = dict(os.environ, SETOPS_INSTANCE=str(DOSSIER_INSTANCES / instance))
|
||||
r = subprocess.run([sys.executable, "scripts/devis_proxmox_fw.py", "--json"],
|
||||
capture_output=True, text=True, cwd=RACINE, env=env)
|
||||
|
|
@ -98,16 +98,29 @@ def matrice(instance: str, hote: str) -> tuple[list[dict], int]:
|
|||
continue
|
||||
groupes = {g["nom"]: g for g in b["groupes"]}
|
||||
tests = []
|
||||
# LES MEMBRES QUI SONT DES RESEAUX, PAS DES MACHINES (2026-09-30). On ne peut pas les
|
||||
# TESTER (aucune machine a qui demander d'ouvrir la connexion), mais Proxmox les
|
||||
# applique : `t23-admin` porte la zone d'administration du site. Les ignorer faisait
|
||||
# prendre le navigateur de l'exploitant, sur l'edge en 443, pour un flux « hors des
|
||||
# regles » — et `--flotte` s'arretait. `!reseau` = exclusion (`nomatch`).
|
||||
reseaux: dict[str, list] = {}
|
||||
for gn in a["groupes"]:
|
||||
for regle in groupes[gn]["regles"]:
|
||||
membres = b["ipsets"].get(regle["source"].lstrip("+"), {}).get("membres", [])
|
||||
port = str(regle.get("dport") or regle.get("icmp_type"))
|
||||
pos = [m for m in membres if "/" in str(m) and not str(m).startswith("!")]
|
||||
neg = [str(m)[1:] for m in membres if str(m).startswith("!")]
|
||||
if pos:
|
||||
reseaux.setdefault(port, []).append(
|
||||
([ipaddress.ip_network(x, strict=False) for x in pos],
|
||||
[ipaddress.ip_network(x, strict=False) for x in neg]))
|
||||
for m in membres:
|
||||
src = h_par_ip.get(m)
|
||||
if src and src != hote:
|
||||
tests.append({"src": src, "ip": ips[hote], "proto": regle["proto"],
|
||||
"port": str(regle.get("dport") or regle.get("icmp_type")),
|
||||
"groupe": gn})
|
||||
return tests, int(a["vmid"])
|
||||
return tests, int(a["vmid"]), reseaux
|
||||
raise SystemExit(f"REFUS : « {hote} » n'a aucune affectation au devis de {instance}.")
|
||||
|
||||
|
||||
|
|
@ -144,7 +157,8 @@ def ecoutes(instance: str, hote: str) -> set[str]:
|
|||
return ports
|
||||
|
||||
|
||||
def observer(instance: str, hote: str, tests: list[dict]) -> list[str]:
|
||||
def observer(instance: str, hote: str, tests: list[dict],
|
||||
reseaux: dict[str, list] | None = None) -> list[str]:
|
||||
"""Les connexions entrantes etablies qu'AUCUNE regle ne couvre."""
|
||||
cmd = ('L=" $(ss -Hltn | awk \'{n=split($4,a,":"); print a[n]}\' | sort -u | tr "\\n" " ") "; '
|
||||
'for i in 1 2 3; do ss -Hnt state established | awk -v L="$L" '
|
||||
|
|
@ -173,6 +187,10 @@ def observer(instance: str, hote: str, tests: list[dict]) -> list[str]:
|
|||
continue
|
||||
if port == "22" and any(ipaddress.ip_address(ip) in n for n in admin):
|
||||
continue
|
||||
adr = ipaddress.ip_address(ip)
|
||||
if any(any(adr in n for n in pos) and not any(adr in n for n in neg)
|
||||
for pos, neg in (reseaux or {}).get(port, [])):
|
||||
continue
|
||||
hors.append(f"{src} -> {hote}:{port}")
|
||||
return sorted(set(hors))
|
||||
|
||||
|
|
@ -206,10 +224,30 @@ def critiques_icinga(instance: str) -> list[str]:
|
|||
return ["(Icinga illisible)"]
|
||||
|
||||
|
||||
def ordre_flotte(instance: str) -> list[str]:
|
||||
"""Toutes les VM du locataire, son RUNNER EN DERNIER.
|
||||
|
||||
C'est depuis le poste qu'on joue la matrice, mais c'est le runner qui conduit le reste
|
||||
de la vie du locataire : s'il devait etre coupe, qu'il le soit apres que tout le reste
|
||||
a ete prouve — et qu'on le voie seul en cause.
|
||||
"""
|
||||
import yaml
|
||||
enfants = (yaml.safe_load(_inventaire(instance).read_text(encoding="utf-8"))
|
||||
.get("all", {}).get("children", {}))
|
||||
runners = set()
|
||||
for g in ("serveur_ops_tenant", "serveur_ops"):
|
||||
runners |= set(((enfants.get(g) or {}).get("hosts") or {}))
|
||||
hotes = sorted(_ip_par_hote(instance))
|
||||
return [h for h in hotes if h not in runners] + [h for h in hotes if h in runners]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--instance", required=True, help="le locataire, ex. OPS-Technolibre")
|
||||
ap.add_argument("--hote", required=True, help="la VM, ex. idm-01")
|
||||
cible = ap.add_mutually_exclusive_group(required=True)
|
||||
cible.add_argument("--hote", help="la VM, ex. idm-01")
|
||||
cible.add_argument("--flotte", action="store_true",
|
||||
help="toutes les VM, une a une, le runner en dernier ; arret au premier refus")
|
||||
mode = ap.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--plan", action="store_true", help="verifier, sans rien activer")
|
||||
mode.add_argument("--activer", action="store_true", help="verifier, activer, reverifier")
|
||||
|
|
@ -217,11 +255,28 @@ def main() -> int:
|
|||
ap.add_argument("--rebond", default="ansible@10.37.0.1", help="rebond vers le runner")
|
||||
ap.add_argument("--racine-runner", default="/opt/setops")
|
||||
args = ap.parse_args()
|
||||
I, H = args.instance, args.hote
|
||||
if args.hote:
|
||||
return eprouver(args.instance, args.hote, args)
|
||||
# UNE A UNE, ET ON S'ARRETE AU PREMIER REFUS (2026-09-30). C'etait une boucle tapee a
|
||||
# la main apres chaque reconstruction ; la regle qui la rend sure — ne pas continuer
|
||||
# apres un ecart — doit vivre avec elle, pas dans la memoire de celui qui la tape.
|
||||
hotes = ordre_flotte(args.instance)
|
||||
for n, h in enumerate(hotes, 1):
|
||||
rc = eprouver(args.instance, h, args)
|
||||
if rc != 0:
|
||||
print(f"\nARRET sur {h} (code {rc}) : {n - 1}/{len(hotes)} VM traitees avant lui. "
|
||||
"Rien d'autre n'a ete touche.")
|
||||
return rc
|
||||
print(f"\n{len(hotes)}/{len(hotes)} VM : pare-feu Proxmox "
|
||||
+ ("verifiable" if args.plan else "actif") + ", aucun flux perdu.")
|
||||
return 0
|
||||
|
||||
tests, vmid = matrice(I, H)
|
||||
|
||||
def eprouver(I: str, H: str, args: argparse.Namespace) -> int:
|
||||
"""La procedure pour UNE VM (voir l'en-tete). 0 = conforme."""
|
||||
tests, vmid, reseaux = matrice(I, H)
|
||||
print(f"=== {H} ({I}, VM {vmid}) — {len(tests)} test(s) tires du devis")
|
||||
hors = observer(I, H, tests)
|
||||
hors = observer(I, H, tests, reseaux)
|
||||
print(f" observation : {len(hors)} flux etabli(s) hors des regles")
|
||||
for h in hors:
|
||||
print(f" HORS : {h}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue