sonder : rapporter ce qui distingue, au lieu d un echec
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
Trois faux diagnostics en une journee, tous dus a l instrument et aucun au composant. curl et bash /dev/tcp ecrasent quatre causes incompatibles dans le meme mot : ouvert, une POLITIQUE qui refuse, une machine ABSENTE, et la frontiere muette. LE MEME CODE DIT DEUX CHOSES SELON LE DELAI, et c est la distinction qui a coute le plus cher : EHOSTUNREACH immediat = pas de route ; le meme apres trois secondes = il n y a pas de machine, c est l ARP qui renonce. Les confondre a fait appliquer un pare-feu pour reparer un vide. est pure, donc gardee par un test qui exige que les deux ne se lisent jamais pareil. LA GARDE ECHOUAIT DU COTE SILENCIEUX. L outil dit par quelle SOURCE le paquet part et refuse de conclure sur une passerelle anycast — le piege qui m a fait declarer muet un REJECT qui emettait bien ses RST. Ma premiere version rendait « pas anycast » quand inventory_rules manquait, c est-a-dire sur un hyperviseur ou l on a copie le seul fichier : precisement la ou le piege se produit. Une garde qui echoue doit crier, pas se taire. ET « NON CONCLUANT » EST UN RESULTAT : sur une source anycast et un silence, l outil ne tranche pas, il dit quoi faire — compter les paquets du cote qui refuse, ou sonder depuis une VM. TCP seulement, et c est dit : UDP n a pas de poignee. Valide contre le reel depuis ops-01 : trois cas sur quatre, le quatrieme attendant deux machines vivantes dans un meme tenant. make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
This commit is contained in:
parent
c918a50257
commit
0595bf03c1
5 changed files with 368 additions and 2 deletions
48
CHANGELOG.md
48
CHANGELOG.md
|
|
@ -1,5 +1,53 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-28 — `make sonder` : rapporter ce qui distingue, au lieu d'« échec »
|
||||
|
||||
**53 preuves.** Trois faux diagnostics en une journée, **tous dus à l'instrument, aucun au
|
||||
composant**. `curl`, `bash /dev/tcp` et un `ping` nu écrasent quatre causes incompatibles
|
||||
dans le même mot. L'information était là chaque fois ; l'outil la jetait.
|
||||
|
||||
```
|
||||
ouvert, ~0 s le flux est déclaré et le service écoute
|
||||
refusé (RST) une POLITIQUE interne refuse — immédiat
|
||||
EHOSTUNREACH, ~3 s LA MACHINE N'EXISTE PAS (l'ARP a abandonné)
|
||||
silence jusqu'au délai la FRONTIÈRE, muette par conception
|
||||
```
|
||||
|
||||
Validé contre le réel depuis `ops-01` — trois des quatre cas, le quatrième attendant deux
|
||||
machines vivantes dans un même tenant.
|
||||
|
||||
### Le même code d'erreur dit deux choses selon le délai
|
||||
|
||||
C'est la distinction qui a coûté le plus cher : `EHOSTUNREACH` **immédiat** veut dire « pas
|
||||
de route » ; le **même code après trois secondes** veut dire « il n'y a pas de machine » —
|
||||
c'est l'ARP qui renonce. Les confondre a fait appliquer un pare-feu pour réparer un vide.
|
||||
`interpreter` est une fonction pure, donc gardée par un test qui exige que les deux ne se
|
||||
lisent **jamais** pareil.
|
||||
|
||||
### La garde qui échouait du côté silencieux
|
||||
|
||||
L'outil dit par quelle **source** le paquet part, et refuse de conclure quand c'est une
|
||||
passerelle anycast — le piège qui m'a fait déclarer muet un `REJECT` qui émettait bien ses
|
||||
RST.
|
||||
|
||||
Ma première version rendait « pas anycast » quand `inventory_rules` était introuvable.
|
||||
C'est-à-dire **exactement sur un hyperviseur où l'on a copié le seul fichier pour
|
||||
diagnostiquer** — là où le piège se produit. Constaté à l'essai : l'avertissement s'est tu
|
||||
sur `10.17.21.1`, qui *est* une passerelle anycast. *Une garde qui échoue doit crier, pas
|
||||
se taire* : sans le moteur, elle retombe désormais sur une heuristique franchement
|
||||
étiquetée `PASSERELLE PROBABLE (module absent)`.
|
||||
|
||||
### Et « non concluant » est un résultat
|
||||
|
||||
Quand la source est anycast et que la mesure est un silence, l'outil ne tranche pas — il
|
||||
dit quoi faire à la place : *compter les paquets du côté qui refuse, ou sonder depuis une
|
||||
VM*. C'est ce compteur qui a prouvé, lui, que le refus était instantané.
|
||||
|
||||
*TCP seulement, et c'est dit : UDP n'a pas de poignée, un silence y confond « ouvert » et
|
||||
« filtré », et prétendre trancher serait le défaut qu'on corrige.*
|
||||
|
||||
make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute.
|
||||
|
||||
## 2026-08-28 — P53 : l'interne refuse à voix haute, la bordure se tait
|
||||
|
||||
**53 preuves.** Décision de l'exploitant : `block` vers l'Internet, `reject` à l'intérieur.
|
||||
|
|
|
|||
13
Makefile
13
Makefile
|
|
@ -261,6 +261,19 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_adressage_derive.py
|
||||
python3 scripts/tests/test_gui_intrants.py
|
||||
python3 scripts/tests/test_devis_placement.py
|
||||
python3 scripts/tests/test_sonder.py
|
||||
|
||||
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
|
||||
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
|
||||
# n'existe pas », « une politique refuse » et « la frontiere se tait » dans le meme mot.
|
||||
.PHONY: sonder
|
||||
sonder: ## Sonde une cible et DIT ce qui distingue — CIBLE=<hote|ip> [PORTS="22 443"]
|
||||
@if [[ -z "$(CIBLE)" ]]; then \
|
||||
printf '%s\n' 'Refus: relancer avec CIBLE=<hote du plan, ou adresse>.'; \
|
||||
printf '%s\n' 'Ex. make sonder CIBLE=infra-dns-01 PORTS="53 22"'; \
|
||||
exit 2; \
|
||||
fi
|
||||
@python3 scripts/sonder.py $(CIBLE) $(or $(PORTS),22) $(if $(DELAI),--delai $(DELAI))
|
||||
|
||||
.PHONY: verifier
|
||||
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 30 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 19, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 52 scripts expliques et atteignables, 104 cibles make documentees, 62 roles avec README. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 53 scripts expliques et atteignables, 105 cibles make documentees, 62 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 41 exigence(s) de role, toutes satisfaites (130 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (38 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (26 genere(s) exempte(s)). |
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 37 role(s) serveur/client tous nommes, 38 groupe(s) cite(s) en table existent tous. |
|
||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. |
|
||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 48 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 49 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 60 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
|
|
|
|||
205
scripts/sonder.py
Executable file
205
scripts/sonder.py
Executable file
|
|
@ -0,0 +1,205 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sonder une cible, et RAPPORTER CE QUI DISTINGUE — pas un « echec ».
|
||||
|
||||
POURQUOI CET OUTIL EXISTE (2026-08-28). Trois faux diagnostics en une journee, tous dus
|
||||
a l'instrument et aucun au composant :
|
||||
|
||||
1. « BLOQUE » vers le resolveur, le cache et la PKI d'un tenant, lu comme un pare-feu.
|
||||
C'etait une ABSENCE — une seule VM existait. Un pare-feu a ete applique pour reparer
|
||||
un vide.
|
||||
2. « 3,05 s » presente comme le signal d'un refus. C'etait l'ARP qui abandonne, pas un
|
||||
REJECT — lequel repond en microsecondes.
|
||||
3. Un REJECT declare muet. Il emettait bien quatre RST (compteur 17 -> 21) ; la sonde
|
||||
partait d'une passerelle ANYCAST, donc la reponse revenait ailleurs.
|
||||
|
||||
`curl`, `bash /dev/tcp` et un `ping` nu ecrasent quatre causes incompatibles dans le meme
|
||||
mot. L'information etait la a chaque fois — l'outil la jetait.
|
||||
|
||||
CE QU'IL MESURE, ET CE QU'IL REFUSE DE CONCLURE :
|
||||
|
||||
ouvert, ~0 s le flux est declare et le service ecoute
|
||||
refuse (RST/ECONNREFUSED) une POLITIQUE interne refuse — reject, immediat
|
||||
EHOSTUNREACH, ~3 s LA MACHINE N'EXISTE PAS (l'ARP a abandonne)
|
||||
silence jusqu'au delai la FRONTIERE, muette par conception (block)
|
||||
|
||||
Et deux gardes sur l'instrument lui-meme :
|
||||
|
||||
- il dit par quelle SOURCE le paquet part (`ip route get`), parce qu'une passerelle
|
||||
anycast rend la mesure NON CONCLUANTE : la reponse revient au noeud qui porte
|
||||
l'adresse, pas a l'emetteur ;
|
||||
- il ne conclut pas quand il ne peut pas. « Non concluant » est un resultat.
|
||||
|
||||
TCP SEULEMENT. UDP n'a pas de poignee : un silence y confond « ouvert » et « filtre »,
|
||||
et pretendre trancher serait exactement le defaut qu'on corrige.
|
||||
|
||||
Usage :
|
||||
python3 scripts/sonder.py 10.17.19.41 22 443
|
||||
python3 scripts/sonder.py infra-dns-01 53 # nom resolu par l'inventaire
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import errno as _errno
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
# Seuil au-dela duquel un echec « injoignable » porte la signature de l'ARP qui abandonne
|
||||
# (trois retransmissions). En-dessous, c'est un refus immediat de la couche 3.
|
||||
SEUIL_ARP = 1.5
|
||||
|
||||
VERDICTS = {
|
||||
"ouvert": "le flux est declare et le service ecoute",
|
||||
"refuse": "une POLITIQUE interne refuse (reject) — immediat",
|
||||
"absent": "LA MACHINE N'EXISTE PAS : l'ARP a abandonne",
|
||||
"injoignable": "pas de route vers cette destination",
|
||||
"silence": "refus MUET — la frontiere (block), ou un drop en chemin",
|
||||
}
|
||||
|
||||
|
||||
def interpreter(issue: str, err: int | None, duree: float) -> tuple[str, str]:
|
||||
"""(cle, phrase) — la lecture d'une sonde. Fonction PURE, donc testable.
|
||||
|
||||
ELLE NE DEVINE PAS. `EHOSTUNREACH` rapide n'est pas la meme chose que le meme code
|
||||
apres trois secondes : le premier est un refus de la couche 3, le second la signature
|
||||
de l'ARP qui renonce. Confondre les deux, c'est refaire l'erreur du 2026-08-28.
|
||||
"""
|
||||
if issue == "ouvert":
|
||||
return "ouvert", VERDICTS["ouvert"]
|
||||
if err == _errno.ECONNREFUSED:
|
||||
return "refuse", VERDICTS["refuse"]
|
||||
if err == _errno.EHOSTUNREACH:
|
||||
cle = "absent" if duree >= SEUIL_ARP else "injoignable"
|
||||
return cle, VERDICTS[cle]
|
||||
if err in (_errno.ENETUNREACH,):
|
||||
return "injoignable", VERDICTS["injoignable"]
|
||||
return "silence", VERDICTS["silence"]
|
||||
|
||||
|
||||
def sonder(cible: str, port: int, delai: float) -> dict:
|
||||
debut = time.time()
|
||||
s = socket.socket()
|
||||
s.settimeout(delai)
|
||||
try:
|
||||
s.connect((cible, port))
|
||||
issue, err = "ouvert", None
|
||||
except OSError as e:
|
||||
issue, err = "echec", getattr(e, "errno", None)
|
||||
finally:
|
||||
s.close()
|
||||
duree = time.time() - debut
|
||||
cle, phrase = interpreter(issue, err, duree)
|
||||
return {"port": port, "cle": cle, "phrase": phrase, "duree": duree,
|
||||
"errno": err, "nom_errno": _errno.errorcode.get(err, "") if err else ""}
|
||||
|
||||
|
||||
def source_vers(cible: str) -> tuple[str, str]:
|
||||
"""(adresse source, note) telle que le noyau la choisira. ('', '') si indisponible.
|
||||
|
||||
La note vaut '', 'anycast' (derive du plan) ou 'anycast?' (heuristique, module absent).
|
||||
"""
|
||||
try:
|
||||
sortie = subprocess.run(["ip", "route", "get", cible], capture_output=True,
|
||||
text=True, timeout=5).stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return "", ""
|
||||
jetons = sortie.split()
|
||||
if "src" not in jetons:
|
||||
return "", ""
|
||||
src = jetons[jetons.index("src") + 1]
|
||||
return src, _est_passerelle(src)
|
||||
|
||||
|
||||
def _est_passerelle(adresse: str) -> str:
|
||||
"""'anycast', 'anycast?' ou '' — cette adresse est-elle une passerelle de zone ?
|
||||
|
||||
DERIVE, jamais compare a une liste : les passerelles sont le premier hote de chaque
|
||||
/24 de zone (`passerelle_de`). Une liste ecrite ici vieillirait au premier tenant
|
||||
ajoute, et la garde se tairait exactement quand elle devrait parler.
|
||||
|
||||
ET ELLE NE TOMBE PAS DU COTE SILENCIEUX. Ma premiere version rendait « pas anycast »
|
||||
quand `inventory_rules` etait introuvable — ce qui arrive des qu'on copie ce seul
|
||||
fichier sur un hyperviseur pour diagnostiquer, c'est-a-dire exactement la ou le piege
|
||||
se produit. Constate le 2026-08-28 : l'avertissement s'est taise sur `10.17.21.1`,
|
||||
qui EST une passerelle anycast. Une garde qui echoue doit crier, pas se taire ; sans
|
||||
le module, on retombe donc sur une heuristique franchement etiquetee.
|
||||
"""
|
||||
morceaux = adresse.split(".")
|
||||
if len(morceaux) != 4 or morceaux[3] != "1" or morceaux[0] != "10":
|
||||
return ""
|
||||
try:
|
||||
index, troisieme = int(morceaux[1]), int(morceaux[2])
|
||||
except ValueError:
|
||||
return ""
|
||||
try:
|
||||
from inventory_rules import passerelle_de
|
||||
except Exception:
|
||||
# Sans le moteur, on ne PROUVE pas — mais un `10.x.y.1` est un premier hote,
|
||||
# donc une passerelle dans toute nomenclature de ce depot. On le dit au
|
||||
# conditionnel plutot que de laisser croire que la mesure est concluante.
|
||||
return "anycast?"
|
||||
return "anycast" if any(passerelle_de(index, cat) == adresse
|
||||
for cat in range(0, 12)) else ""
|
||||
|
||||
|
||||
def _adresse_de(nom: str) -> str:
|
||||
"""Le nom tel que l'inventaire le connait, sinon le nom tel quel."""
|
||||
try:
|
||||
import yaml
|
||||
from inventory_rules import inventaire_de
|
||||
inv = inventaire_de()
|
||||
if inv and Path(inv).is_file():
|
||||
data = yaml.safe_load(Path(inv).read_text(encoding="utf-8")) or {}
|
||||
pile = [data.get("all", {})]
|
||||
while pile:
|
||||
n = pile.pop()
|
||||
for h, v in (n.get("hosts") or {}).items():
|
||||
if h == nom and isinstance(v, dict) and v.get("ansible_host"):
|
||||
return str(v["ansible_host"])
|
||||
pile.extend((n.get("children") or {}).values())
|
||||
except Exception:
|
||||
pass
|
||||
return nom
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("cible", help="adresse, ou nom d'hote connu de l'inventaire")
|
||||
ap.add_argument("ports", nargs="*", type=int, default=[22], help="ports TCP (defaut 22)")
|
||||
ap.add_argument("--delai", type=float, default=6.0, help="patience, en secondes")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
adresse = _adresse_de(args.cible)
|
||||
etiquette = args.cible if adresse == args.cible else f"{args.cible} ({adresse})"
|
||||
print(f"Sonde de {etiquette} — TCP, patience {args.delai:g} s")
|
||||
|
||||
src, note = source_vers(adresse)
|
||||
if src:
|
||||
avertissement = ""
|
||||
if note:
|
||||
certain = note == "anycast"
|
||||
avertissement = (
|
||||
f" /!\\ PASSERELLE {'ANYCAST' if certain else 'PROBABLE (module absent)'}"
|
||||
" — mesure NON CONCLUANTE en cas de silence :\n"
|
||||
" la reponse revient au noeud qui porte cette adresse, pas ici.")
|
||||
print(f" source choisie par le noyau : {src}{avertissement}")
|
||||
print()
|
||||
|
||||
resultats = [sonder(adresse, p, args.delai) for p in args.ports]
|
||||
for r in resultats:
|
||||
code = f" [{r['nom_errno']}]" if r["nom_errno"] else ""
|
||||
print(f" {r['port']:<7} {r['cle']:<12} {r['duree']:6.3f} s{code}")
|
||||
print(f" {r['phrase']}")
|
||||
if note and any(r["cle"] == "silence" for r in resultats):
|
||||
print("\n Un silence mesure depuis une passerelle anycast ne prouve RIEN :"
|
||||
"\n compter les paquets du cote qui refuse, ou sonder depuis une VM.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
100
scripts/tests/test_sonder.py
Normal file
100
scripts/tests/test_sonder.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python3
|
||||
"""La sonde distingue-t-elle ce qu'un « echec » confond ?
|
||||
|
||||
CE QUE CES TESTS GARDENT, ET POURQUOI. Le 2026-08-28, trois faux diagnostics ont ete
|
||||
poses en une journee — tous par l'instrument, aucun par le composant. Le pire des trois
|
||||
n'etait pas une confusion entre « ouvert » et « ferme », mais entre DEUX FORMES DU MEME
|
||||
CODE : `EHOSTUNREACH` immediat (pas de route) et `EHOSTUNREACH` apres trois secondes
|
||||
(l'ARP qui renonce, donc AUCUNE MACHINE). Le second a fait appliquer un pare-feu pour
|
||||
reparer un vide.
|
||||
|
||||
`interpreter` est une fonction PURE : elle se joue sans reseau, donc elle se garde ici.
|
||||
"""
|
||||
import errno
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent.parent
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import sonder # noqa: E402
|
||||
|
||||
|
||||
def test_ouvert_est_ouvert() -> None:
|
||||
assert sonder.interpreter("ouvert", None, 0.001)[0] == "ouvert"
|
||||
|
||||
|
||||
def test_refus_immediat_est_une_politique() -> None:
|
||||
"""Un RST est le signal d'un reject : une POLITIQUE, pas une absence."""
|
||||
cle, phrase = sonder.interpreter("echec", errno.ECONNREFUSED, 0.002)
|
||||
assert cle == "refuse"
|
||||
assert "POLITIQUE" in phrase
|
||||
|
||||
|
||||
def test_le_meme_code_dit_deux_choses_selon_le_delai() -> None:
|
||||
"""LE TEST QUI COMPTE. `EHOSTUNREACH` lent = machine absente ; rapide = pas de route.
|
||||
|
||||
Les confondre est exactement l'erreur du 2026-08-28.
|
||||
"""
|
||||
lent, _ = sonder.interpreter("echec", errno.EHOSTUNREACH, 3.05)
|
||||
vif, _ = sonder.interpreter("echec", errno.EHOSTUNREACH, 0.01)
|
||||
assert lent == "absent", "un EHOSTUNREACH apres l'ARP dit qu'il n'y a PAS de machine"
|
||||
assert vif == "injoignable", "le meme code, immediat, dit qu'il n'y a pas de route"
|
||||
assert lent != vif, "les deux ne doivent JAMAIS se lire pareil"
|
||||
|
||||
|
||||
def test_le_silence_reste_le_silence() -> None:
|
||||
"""Sans code, on ne conclut pas au-dela de « quelque chose se tait »."""
|
||||
cle, phrase = sonder.interpreter("echec", None, 6.0)
|
||||
assert cle == "silence"
|
||||
assert "frontiere" in phrase
|
||||
|
||||
|
||||
def test_une_passerelle_de_zone_est_signalee_anycast() -> None:
|
||||
"""La garde qui m'a manque : `10.17.21.1` EST une passerelle de zone."""
|
||||
assert sonder._est_passerelle("10.17.21.1") == "anycast"
|
||||
assert sonder._est_passerelle("10.17.19.41") == ""
|
||||
|
||||
|
||||
def test_sans_le_moteur_la_garde_crie_au_lieu_de_se_taire() -> None:
|
||||
"""Une garde qui echoue doit crier. Ma premiere version rendait « pas anycast »
|
||||
quand `inventory_rules` manquait — c'est-a-dire sur un hyperviseur ou l'on a copie
|
||||
le seul fichier, donc precisement la ou le piege se produit."""
|
||||
vrai_import = __builtins__["__import__"] if isinstance(__builtins__, dict) else __builtins__.__import__
|
||||
|
||||
def sans_moteur(nom, *a, **k):
|
||||
if nom == "inventory_rules":
|
||||
raise ImportError("simule")
|
||||
return vrai_import(nom, *a, **k)
|
||||
|
||||
cible = __builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__
|
||||
cible["__import__"] = sans_moteur
|
||||
try:
|
||||
assert sonder._est_passerelle("10.17.21.1") == "anycast?", \
|
||||
"sans le moteur, la garde doit avertir au conditionnel, pas se taire"
|
||||
assert sonder._est_passerelle("10.17.19.41") == ""
|
||||
finally:
|
||||
cible["__import__"] = vrai_import
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tests = [
|
||||
test_ouvert_est_ouvert,
|
||||
test_refus_immediat_est_une_politique,
|
||||
test_le_meme_code_dit_deux_choses_selon_le_delai,
|
||||
test_le_silence_reste_le_silence,
|
||||
test_une_passerelle_de_zone_est_signalee_anycast,
|
||||
test_sans_le_moteur_la_garde_crie_au_lieu_de_se_taire,
|
||||
]
|
||||
definis = {n for n, v in globals().items() if n.startswith("test_") and callable(v)}
|
||||
oublies = definis - {t.__name__ for t in tests}
|
||||
assert not oublies, f"test(s) definis mais jamais joues : {sorted(oublies)}"
|
||||
for t in tests:
|
||||
t()
|
||||
print(f"ok {t.__name__}")
|
||||
print(f"{len(tests)} tests passes.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in a new issue