validation avant release : test casse par (74), trois defauts d outillage

test_restauration fournit client_backup_attente_verrou et exige
--retry-lock ; test_frontiere_refus lit le WAN sous if_wan ; prouver
--verifier nomme les preuves en echec ; le Makefile n exporte plus une
liste de cles de voute vide (valeur identique sur le poste).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-04 15:46:43 -04:00
parent 5d7d2a8918
commit f42d30b38a
5 changed files with 56 additions and 3 deletions

View file

@ -1,5 +1,36 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-10-04 (76) — Validation avant release : un test cassé par (74), trois défauts d'outillage
**Contexte** : avant d'étiqueter, `make verifier` et `make ci`. La validation a trouvé une
régression **introduite par (74)**, que sa propre validation n'avait pas vue :
`--syntax-check` et `ansible-lint` seulement, pas `make test`.
**Corrigé** :
- `scripts/tests/test_restauration.py` : le test rendait `restaurer.sh.j2` sans
`client_backup_attente_verrou` (ajoutée en (74)) et échouait au rendu. Il la fournit
désormais, et son faux `restic` **exige** `--retry-lock` avant la sous-commande : l'attente
du verrou est couverte, plus seulement tolérée.
- `scripts/tests/test_frontiere_refus.py` (depuis le 2026-10-01) : il lisait le WAN sous
`interfaces.wan`, une clé que le devis ne produit pas (c'est `if_wan`). Il retombait sur
« wan », juste au site par chance.
- `scripts/prouver.py` : en mode `--verifier`, le harnais comptait les échecs (« 6 echec »)
sans les nommer. Il nomme maintenant chaque preuve en échec, avec son détail.
- `Makefile` : sur un clone sans aucune clé de voûte (la CI de la forge), l'export posait
`ANSIBLE_VAULT_IDENTITY_LIST=` vide ; Ansible le lisait comme un fichier de mot de passe
et refusait (« can not be a directory »). `make ci` mourait à sa première commande. Vide,
la variable n'est plus exportée. Sur le poste, la valeur exportée est **identique**
(mêmes 5 clés, comparée avant et après).
**Pas corrigé ici, et nommé** : `make ci` reste non conforme (77 OK, 6 échecs : P02, P20,
P74, P79, P81, P82). Le modèle public `exemples/modeles/socle` date d'avant la séparation
site / locataire et ne satisfait pas les gardes ajoutées depuis août. Décision de
l'exploitant : le modèle aura son propre dépôt, `OPS-Modele`, comme les locataires. Aucune
preuve n'a été assouplie pour le vieux modèle.
**Validation** : `make verifier` conforme (83/83), `ansible-lint` (0 échec). La release
attend une reconstruction qui prouve (73), (74) et ces corrections.
## 2026-10-04 (75) — Exporter les voûtes une seconde fois ne bute plus sur la première ## 2026-10-04 (75) — Exporter les voûtes une seconde fois ne bute plus sur la première
**Signalé par l'exploitant** : `make voutes-exporter VERS=<clé>` a refusé (« existe deja. **Signalé par l'exploitant** : `make voutes-exporter VERS=<clé>` a refusé (« existe deja.

View file

@ -21,7 +21,16 @@ export SETOPS_INSTANCE ?= instance
# #
# `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent # `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent
# nommer leurs propres cles sans que ce fichier ait a les connaitre. # nommer leurs propres cles sans que ce fichier ait a les connaitre.
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(shell python3 scripts/voutes.py identites 2>/dev/null) #
# VIDE, ON N'EXPORTE RIEN (2026-10-04). Sur un clone sans aucune cle (la CI de la forge),
# `voutes.py` ne rend rien, et l'export posait quand meme `ANSIBLE_VAULT_IDENTITY_LIST=`.
# Ansible lit cette chaine vide comme un FICHIER de mot de passe, la resout en repertoire
# courant et refuse (« can not be a directory ») : `make ci` mourait a sa premiere commande
# sur un clone nu. Le poste a toujours des cles, il ne pouvait pas le voir.
SETOPS_VOUTES_IDENTITES := $(shell python3 scripts/voutes.py identites 2>/dev/null)
ifneq ($(strip $(SETOPS_VOUTES_IDENTITES)),)
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(SETOPS_VOUTES_IDENTITES)
endif
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par # Inventaire de l'instance : un seul par instance dans le modèle « séparation par
# instance ». Détection rétro-compatible : principal > production > lab. # instance ». Détection rétro-compatible : principal > production > lab.
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml # Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml

View file

@ -4972,6 +4972,12 @@ def main(argv: list[str] | None = None) -> int:
print(f"{'CONFORME' if conforme else 'NON CONFORME'} : " print(f"{'CONFORME' if conforme else 'NON CONFORME'} : "
f"{n_ok} OK, {n_echec} echec, {n_saute} saute.") f"{n_ok} OK, {n_echec} echec, {n_saute} saute.")
if not ecrire_rapport: if not ecrire_rapport:
# NOMMER CE QUI ECHOUE (2026-10-04) : le verdict seul (« 6 echec ») obligeait a
# reconstituer le contexte et relire le code pour savoir lesquelles.
for p, s, d in resultats:
if s == "ECHEC":
print(f" ECHEC {p['id']} — {p.get('titre') or p.get('nom') or ''} : "
f"{' '.join(str(d).split())[:300]}")
return 0 if conforme else 1 return 0 if conforme else 1
icone = {"OK": "✅", "ECHEC": "❌", "SAUTE": "⚪"} icone = {"OK": "✅", "ECHEC": "❌", "SAUTE": "⚪"}

View file

@ -28,8 +28,11 @@ def main() -> int:
if r.returncode != 0 or not r.stdout.strip().startswith("{"): if r.returncode != 0 or not r.stdout.strip().startswith("{"):
print("SAUTE le devis de la frontiere ne se genere pas ici (site invisible)") print("SAUTE le devis de la frontiere ne se genere pas ici (site invisible)")
return 0 return 0
regles = json.loads(r.stdout).get("regles") or [] devis = json.loads(r.stdout)
wan = str((json.loads(r.stdout).get("interfaces") or {}).get("wan") or "wan") regles = devis.get("regles") or []
# `if_wan`, la cle que le devis produit. Le test lisait `interfaces.wan`, qui n'existe
# pas : il retombait sur « wan », juste au site par chance, faux sur le modele public.
wan = str(devis.get("if_wan") or "wan")
echecs = [] echecs = []
refus = [x for x in regles if x.get("action") == "reject"] refus = [x for x in regles if x.get("action") == "reject"]
if any(x.get("interface") == wan for x in refus): if any(x.get("interface") == wan for x in refus):

View file

@ -130,6 +130,9 @@ def outil() -> None:
# SETOPS_TEST_DEPOT_ABSENT=1 imite restic 0.17+ face a un depot inexistant (code 10). # SETOPS_TEST_DEPOT_ABSENT=1 imite restic 0.17+ face a un depot inexistant (code 10).
executable(bin_ / "restic", f"""#!/bin/bash executable(bin_ / "restic", f"""#!/bin/bash
if [[ -n "${{SETOPS_TEST_DEPOT_ABSENT:-}}" ]]; then exit 10; fi if [[ -n "${{SETOPS_TEST_DEPOT_ABSENT:-}}" ]]; then exit 10; fi
# L'outil passe `--retry-lock <duree>` avant la sous-commande (74) : on l'exige, puis on le saute.
[[ "$1" == --retry-lock && -n "$2" ]] || {{ echo "restic appele sans --retry-lock : $*" >&2; exit 99; }}
shift 2
case "$1" in case "$1" in
snapshots) cat "{d}/snapshots.json" ;; snapshots) cat "{d}/snapshots.json" ;;
restore) restore)
@ -164,6 +167,7 @@ if (( ! avec_c )); then cat > "{log}/section.sql"; fi
client_backup_restauration_mise_de_cote=str(d / "mis-de-cote"), client_backup_restauration_mise_de_cote=str(d / "mis-de-cote"),
client_backup_staging=str(staging), client_backup_staging=str(staging),
client_backup_jobs=[{"nom": "courriel"}, {"nom": "postgresql"}], client_backup_jobs=[{"nom": "courriel"}, {"nom": "postgresql"}],
client_backup_attente_verrou="30m",
).replace("/etc/ssh/ssh_host_ed25519_key.pub", str(cle)) \ ).replace("/etc/ssh/ssh_host_ed25519_key.pub", str(cle)) \
.replace("/etc/setops/restic.pass", str(d / "restic.pass")) .replace("/etc/setops/restic.pass", str(d / "restic.pass"))
script = d / "setops-restaurer" script = d / "setops-restaurer"