frontiere : un boitier injoignable etait lu comme un boitier vide
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
L'exploitant lance `make frontiere-appliquer` dans son terminal : « a creer 0, inchange
53 + 15 routes — la frontiere dit deja ce que le devis dit ». Elle etait DEJA conforme.
J'avais annonce quelques heures plus tot « 89 a creer, 0 inchange, donc les regles
heritees sont invisibles a l'API ». FAUX. L'intrant `opnsense_api_url` pointait sur
10.0.0.1, l'adresse d'avant la migration du boitier : chaque lecture rendait
{"_erreur": ...}, le plan lisait `.get("rows")`, n'y trouvait rien, et concluait au vide.
Avec CONFIRMER, on aurait pousse une politique entiere EN DOUBLE.
TROISIEME FOIS EN UNE SOIREE, meme confusion — « pas de reponse » pris pour « rien » :
devis_placement iterait un dict d'erreur comme une liste
devis_underlay declarait morts les reseaux qu'il ne joignait pas
appliquer_opnsense lisait un boitier injoignable comme un boitier vide
Toutes les lectures de la frontiere passent desormais par une garde qui refuse en nommant
l'hote, la cause et le piege evite. Eprouvee contre l'ancienne adresse : elle refuse.
CE QUE CA DIT DE LA METHODE : ce n'est ni le harnais ni moi qui avons trouve, c'est
l'exploitant, en lancant la commande la ou il VOIT la sortie. Mes commandes s'executent
dans ma session ; il n'en voit rien. Une commande lente ressemble a un blocage, et un
blocage a une commande lente — j'ai conclu deux fois a tort avant qu'il ne regarde.
make verifier 41/41 ; le plan reel reste « rien a faire ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d1db332ed3
commit
4af1fcd0e2
2 changed files with 89 additions and 8 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -1,5 +1,56 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-23 — Un boîtier injoignable n'est pas un boîtier vide
|
||||
|
||||
L'exploitant lance `make frontiere-appliquer` dans son propre terminal. Résultat :
|
||||
|
||||
```
|
||||
Frontiere https://10.17.0.1 — 50 regles au devis
|
||||
a creer : 0 | a retirer : 0 | inchange : 53 + 15 routes
|
||||
La frontiere dit deja ce que le devis dit. Rien a faire.
|
||||
```
|
||||
|
||||
**La frontière était déjà conforme.** Or j'avais annoncé, quelques heures plus tôt,
|
||||
« 89 objets à créer, 0 inchangé — donc les règles héritées sont invisibles à l'API ».
|
||||
C'était faux, et la cause était ailleurs.
|
||||
|
||||
### Ce qui s'était réellement passé
|
||||
|
||||
L'intrant `opnsense_api_url` pointait encore sur `10.0.0.1`, l'adresse d'avant la
|
||||
migration du boîtier. Chaque lecture échouait donc et rendait `{"_erreur": …}`. Le plan
|
||||
lisait `.get("rows")`, n'y trouvait rien — et concluait que **la frontière était vide**.
|
||||
|
||||
Avec `CONFIRMER=true`, on aurait poussé une politique entière **en double** sur un boîtier
|
||||
qui la portait déjà.
|
||||
|
||||
Et l'adresse avait pu rester périmée parce qu'elle était rangée au mauvais endroit : dans
|
||||
les `group_vars` d'un tenant, alors qu'elle décrit un boîtier que ce tenant ne possède
|
||||
pas. `opnsense.yml` vit désormais dans le dépôt de site, avec `underlay.yml`.
|
||||
|
||||
### La troisième fois en une soirée
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `devis_placement` | itérait un dict d'erreur comme une liste → trace Python illisible |
|
||||
| `devis_underlay` | déclarait morts les réseaux qu'il ne joignait pas depuis le poste |
|
||||
| `appliquer_opnsense` | lisait un boîtier injoignable comme un boîtier vide |
|
||||
|
||||
Trois formes d'une même confusion : **« pas de réponse » pris pour « rien »**. Toutes les
|
||||
lectures de la frontière passent maintenant par une garde qui refuse en nommant l'hôte, la
|
||||
cause, et le piège qu'elle évite. Éprouvée contre l'ancienne adresse : elle refuse.
|
||||
|
||||
### Ce que ça dit de la méthode
|
||||
|
||||
Ce n'est pas le harnais qui a trouvé le défaut, ni moi. C'est l'exploitant, en lançant la
|
||||
commande **dans son terminal**, où il voit la sortie en direct. Mes commandes s'exécutent
|
||||
dans ma session : il n'en voit rien. Une commande lente ressemble alors à un blocage, et
|
||||
un blocage à une commande lente — j'ai conclu deux fois à tort avant qu'il ne regarde
|
||||
lui-même.
|
||||
|
||||
> **Pour toute écriture longue sur du matériel, c'est à l'exploitant de lancer la
|
||||
> commande.** Non par prudence formelle : parce qu'il est le seul à voir ce qui se passe.
|
||||
|
||||
|
||||
## 2026-08-22 — Le moteur supposait l'écosystème complet
|
||||
|
||||
Troisième manifestation de la même hypothèse en cinq jours, et cette fois elle **bloquait
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ def _passerelle_pour(api: Frontiere, saut: str) -> str | None:
|
|||
prochain saut. On resout ici plutot que d'exiger un intrant de plus : le nom est
|
||||
deja sur le boitier, et le demander deux fois serait une occasion de divergence.
|
||||
"""
|
||||
for x in (api("/api/routes/gateway/status").get("items") or []):
|
||||
for x in (_lire(api, "/api/routes/gateway/status").get("items") or []):
|
||||
if str(x.get("address") or "").strip() == str(saut).strip():
|
||||
return str(x.get("name"))
|
||||
return None
|
||||
|
|
@ -198,19 +198,49 @@ def _contenu(x) -> set[str]:
|
|||
return {v.strip() for v in str(x or "").replace(",", "\n").split("\n") if v.strip()}
|
||||
|
||||
|
||||
def _lire(api: Frontiere, chemin: str, corps: dict | None = None) -> dict:
|
||||
"""Une lecture du boitier — ou un REFUS. Jamais un silence pris pour du vide.
|
||||
|
||||
POURQUOI (mesure du 2026-08-22, trouvee par l'exploitant). L'intrant `opnsense_api_url`
|
||||
pointait encore sur `10.0.0.1`, l'adresse d'avant la migration du boitier. Chaque
|
||||
lecture echouait donc et rendait `{"_erreur": ...}` ; le plan lisait `.get("rows")`,
|
||||
n'y trouvait rien, et concluait que la frontiere etait VIDE : « 89 objets a creer ».
|
||||
|
||||
J'en avais tire une conclusion fausse — « les regles heritees sont invisibles a
|
||||
l'API » — et, avec CONFIRMER, on aurait pousse une politique entiere en double sur un
|
||||
boitier qui la portait deja. La verite, une fois la bonne adresse posee : 53 objets et
|
||||
15 routes, tous conformes, rien a faire.
|
||||
|
||||
UN BOITIER INJOIGNABLE N'EST PAS UN BOITIER VIDE. C'est la troisieme fois en une
|
||||
soiree que cette confusion se paie — apres le devis de placement qui iterait une
|
||||
erreur comme une liste, et la reconnaissance de l'underlay qui declarait morts les
|
||||
reseaux qu'elle ne joignait pas. On refuse donc, en nommant l'hote et la cause.
|
||||
"""
|
||||
rep = api(chemin, corps)
|
||||
if isinstance(rep, dict) and rep.get("_erreur"):
|
||||
raise SystemExit(
|
||||
f"La frontiere ne repond pas sur `{chemin}` : {rep['_erreur']}\n"
|
||||
f" boitier interroge : {api.base if hasattr(api, 'base') else '?'}\n"
|
||||
f" a verifier : `opnsense_api_url` designe-t-il l'adresse ACTUELLE du "
|
||||
f"boitier, le VPN est-il monte, la cle d'API est-elle encore valide ?\n"
|
||||
f" (un boitier injoignable serait lu comme un boitier VIDE, et le plan "
|
||||
f"proposerait de tout recreer)")
|
||||
return rep if isinstance(rep, dict) else {}
|
||||
|
||||
|
||||
def plan(api: Frontiere, devis: dict) -> dict:
|
||||
"""Ce qu'il faudrait faire pour que le boitier dise ce que le devis dit."""
|
||||
voulues = {cle_regle(r): r for r in devis["regles"]}
|
||||
posees = {}
|
||||
for x in (api("/api/firewall/filter/search_rule/",
|
||||
{"current": 1, "rowCount": 1000}).get("rows") or []):
|
||||
for x in (_lire(api, "/api/firewall/filter/search_rule/",
|
||||
{"current": 1, "rowCount": 1000}).get("rows") or []):
|
||||
d = str(x.get("description") or "")
|
||||
if d.startswith("setops:"):
|
||||
posees[d.split(" — ")[0]] = x
|
||||
|
||||
al_voulus = devis["alias"]
|
||||
al_poses = {x["name"]: x for x in (api("/api/firewall/alias/searchItem/",
|
||||
{"rowCount": 1000}).get("rows") or [])
|
||||
al_poses = {x["name"]: x for x in (_lire(api, "/api/firewall/alias/searchItem/",
|
||||
{"rowCount": 1000}).get("rows") or [])
|
||||
if str(x.get("name", "")).startswith("SETOPS_")}
|
||||
|
||||
# Un alias encore reference par une regle qui SURVIT ne doit pas partir : la
|
||||
|
|
@ -224,7 +254,7 @@ def plan(api: Frontiere, devis: dict) -> dict:
|
|||
|
||||
nat_voulus = {cle_nat(n): n for n in devis.get("nat") or []}
|
||||
nat_poses = {}
|
||||
for x in (api("/api/firewall/source_nat/search_rule/",
|
||||
for x in (_lire(api, "/api/firewall/source_nat/search_rule/",
|
||||
{"current": 1, "rowCount": 1000}).get("rows") or []):
|
||||
d = str(x.get("description") or "")
|
||||
if d.startswith("setopsnat:"):
|
||||
|
|
@ -232,7 +262,7 @@ def plan(api: Frontiere, devis: dict) -> dict:
|
|||
|
||||
rt_voulues = {cle_route(r): r for r in devis.get("routes") or []}
|
||||
rt_posees = {}
|
||||
for x in (api("/api/routes/routes/searchroute/",
|
||||
for x in (_lire(api, "/api/routes/routes/searchroute/",
|
||||
{"current": 1, "rowCount": 1000}).get("rows") or []):
|
||||
d = str(x.get("descr") or "")
|
||||
if d.startswith("setopsroute:"):
|
||||
|
|
@ -400,7 +430,7 @@ def main() -> int:
|
|||
print(f"Frontiere {url} — {len(devis['regles'])} regles au devis\n")
|
||||
p = plan(api, devis)
|
||||
p["_uuid_alias"] = {x["name"]: x["uuid"] for x in
|
||||
(api("/api/firewall/alias/searchItem/", {"rowCount": 1000}).get("rows") or [])}
|
||||
(_lire(api, "/api/firewall/alias/searchItem/", {"rowCount": 1000}).get("rows") or [])}
|
||||
if not afficher(p):
|
||||
print("\n La frontiere dit deja ce que le devis dit. Rien a faire.")
|
||||
return 0
|
||||
|
|
|
|||
Loading…
Reference in a new issue