resolution d'instance : une seule, partagee — au lieu de neuf copies
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? » Neuf modules portaient chacun leur reponse. - 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ; - 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et _frontiere_absente lisaient le symlink au lieu de la variable ; - 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ; - 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme. Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier. LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(), dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve — c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut. Vingt-huit modules y sont branches. CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure : 17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose. P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees : instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de la resolution. make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bd1b897815
commit
c18debc25e
28 changed files with 237 additions and 97 deletions
58
CHANGELOG.md
58
CHANGELOG.md
|
|
@ -1,5 +1,63 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-22 — Neuf copies d'une même question, et la pièce qui manquait
|
||||
|
||||
Cinq jours, cinq défauts, tous de la même famille : *quelle instance, quel inventaire ?*
|
||||
Neuf modules portaient chacun leur réponse.
|
||||
|
||||
| Découvert | Ce que la copie faisait |
|
||||
|---|---|
|
||||
| 18 août | **P03** comparait chaque instance à l'inventaire d'une **autre** |
|
||||
| 19 août | `verifier_ports` codait `principal/` en dur ; `verifier_intrants` et `_frontiere_absente` lisaient le symlink au lieu de la variable |
|
||||
| 20 août | `devis_placement` rendait un verdict **juste sur le mauvais tenant** |
|
||||
| 22 août | **P35**, puis **P36** — la dixième, trouvée par la preuve elle-même |
|
||||
|
||||
Aucune n'était une faute d'inattention. Chacune avait été écrite de bonne foi, à un
|
||||
moment où le besoin semblait local. **C'est le mode de panne de la duplication : pas
|
||||
l'erreur, mais la dérive** — invisible depuis l'intérieur d'un fichier, parce que chaque
|
||||
copie a l'air correcte chez elle.
|
||||
|
||||
### La résolution unique
|
||||
|
||||
`inventory_rules` porte désormais `instance_courante()`, `inventaire_de()`,
|
||||
`dossier_inventaire()` et `plan_de()`. Trois niveaux de repli, dont **le troisième
|
||||
manquait à la moitié des copies** : un `hosts.yml` existant, puis un **répertoire**
|
||||
existant — le cas d'une instance neuve, celui qui faisait échouer `make instancier` sur le
|
||||
modèle public — puis le défaut.
|
||||
|
||||
**Vingt-huit modules** y sont branchés.
|
||||
|
||||
### Ce qui rend ce refactor sûr
|
||||
|
||||
Avant de toucher quoi que ce soit, chaque module a été interrogé sur ce qu'il résolvait,
|
||||
pour **les deux écosystèmes**. Après refactor, la même mesure :
|
||||
|
||||
```
|
||||
17 modules × 2 instances → diff vide
|
||||
```
|
||||
|
||||
Aucune résolution n'a changé. Le refactor est prouvé neutre, pas supposé tel.
|
||||
|
||||
### P41, et ses trois exemptions
|
||||
|
||||
La preuve échoue dès qu'un module réintroduit une copie. **Éprouvée en négatif** : une
|
||||
copie replacée dans `genome.py` est signalée avec son numéro de ligne.
|
||||
|
||||
Trois exemptions, nommées pour rester des choix : `instances.py` et `inventory_gui.py`
|
||||
manipulent le **symlink lui-même** — c'est la bascule d'instance —, et `devis_opnsense`
|
||||
lit délibérément quelle instance est **active** pour se situer dans la fédération. Ces
|
||||
trois-là parlent du lien, pas de la résolution.
|
||||
|
||||
Elle a d'ailleurs trouvé une dixième copie à sa première exécution : **P36**, dans le
|
||||
fichier même qui l'héberge.
|
||||
|
||||
`make verifier` : **41 OK, 0 échec, 0 sauté**. Lint vert.
|
||||
|
||||
> **Une preuve qui trouve un défaut le jour où on l'écrit a payé son coût immédiatement.**
|
||||
> Celle-ci en a trouvé un dixième, dans `prouver.py` — et dans ma propre docstring, qui
|
||||
> contenait le motif qu'elle interdit.
|
||||
|
||||
|
||||
## 2026-08-22 — Une forge n'a pas besoin d'un serveur de bases pour trois personnes
|
||||
|
||||
Doute de l'exploitant en relisant patient 0 : *« je doute de la pertinence de pgsql. »*
|
||||
|
|
|
|||
|
|
@ -19,28 +19,18 @@ from inventory_rules import (
|
|||
charger_bases_donnees,
|
||||
charger_domaines,
|
||||
charger_serveurs,
|
||||
inventaire_force,
|
||||
instance_courante,
|
||||
inventaire_de,
|
||||
valider_applications,
|
||||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
FICHIER = INSTANCE / "plan/applications.yml"
|
||||
FICHIER_BASES = INSTANCE / "plan/bases-donnees.yml"
|
||||
FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml"
|
||||
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
||||
def _inventaire(instance: Path, *noms: str) -> Path:
|
||||
forced = inventaire_force(instance) # refuse une cible hors de `instance`
|
||||
if forced:
|
||||
return forced
|
||||
for nom in noms:
|
||||
p = instance / "inventories" / nom / "hosts.yml"
|
||||
if p.exists():
|
||||
return p
|
||||
return instance / "inventories" / noms[0] / "hosts.yml"
|
||||
|
||||
|
||||
INVENTAIRE = _inventaire(INSTANCE, "principal", "production")
|
||||
INVENTAIRE = inventaire_de(INSTANCE)
|
||||
|
||||
# Groupes 'serveurs_*' qui sont des capacites de SOCLE (sur toutes les VM),
|
||||
# pas des applications a part entiere.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from pathlib import Path
|
|||
import yaml
|
||||
|
||||
from inventory_rules import (
|
||||
instance_courante,
|
||||
chaine_connexion,
|
||||
charger_applications,
|
||||
charger_bases_donnees,
|
||||
|
|
@ -19,7 +20,7 @@ from inventory_rules import (
|
|||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
FICHIER = INSTANCE / "plan/bases-donnees.yml"
|
||||
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import sys
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import inventaire_force
|
||||
from inventory_rules import instance_courante, inventaire_force
|
||||
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
|
||||
|
||||
def _inventaire_dir(instance: Path, *noms: str) -> Path:
|
||||
|
|
|
|||
|
|
@ -35,8 +35,10 @@ import yaml
|
|||
from inventory_gui import CHAMPS_ECRITS_PAR_GUI
|
||||
from modeles import decouvrir as decouvrir_modeles
|
||||
|
||||
from inventory_rules import instance_courante, instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
SOURCE_GUI = RACINE / "scripts" / "inventory_gui.py"
|
||||
|
||||
# (fichier du plan, cle racine, nom du registre) — les registres tables-de-tables.
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ import sys
|
|||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
MOTIF = str(RACINE / "instance" / "devis-certificats.json.*")
|
||||
MOTIF = str(instance_courante() / "devis-certificats.json.*")
|
||||
|
||||
# Un certificat servi qui expire dans moins que ça n'a plus de marge : le service doit
|
||||
# etre recharge avant. Genereux par rapport au renouvellement (~14 min), serre par
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ import re
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
MOTIF = str(RACINE / "instance" / "devis-courriel.json.*")
|
||||
MOTIF = str(instance_courante() / "devis-courriel.json.*")
|
||||
|
||||
|
||||
def _valeur(brut: str, cle: str) -> str:
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ import json
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
RELEVE = RACINE / "instance" / "devis-expositions.json"
|
||||
RELEVE = instance_courante() / "devis-expositions.json"
|
||||
|
||||
# Un service VIVANT peut legitimement repondre autre chose que 200 : une redirection vers
|
||||
# l'IdP, ou un 401/403 quand l'authentification est exigee. Ce qui compte est qu'il ait
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import subprocess
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Adresse de controle : documentee comme non routable et attribuee a personne. Si elle
|
||||
|
|
@ -179,7 +181,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
json.dump(cibles(json.load(sys.stdin)), sys.stdout)
|
||||
return 0
|
||||
|
||||
chemin = Path(a.releve) if a.releve else RACINE / "instance" / "devis-frontiere.json"
|
||||
chemin = Path(a.releve) if a.releve else instance_courante() / "devis-frontiere.json"
|
||||
if not chemin.is_file():
|
||||
raise SystemExit(f"Aucun releve : {chemin}\nLancer d'abord `make frontiere-mesurer`.")
|
||||
ecarts, lignes = analyser(json.loads(chemin.read_text()))
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ import json
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
RELEVE = RACINE / "instance" / "devis-identite.json"
|
||||
RELEVE = instance_courante() / "devis-identite.json"
|
||||
|
||||
|
||||
class Ecart:
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import subprocess
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
|
|
@ -73,7 +75,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--releve", default=None)
|
||||
a = ap.parse_args(argv)
|
||||
chemin = Path(a.releve) if a.releve else RACINE / "instance" / "devis-mtu.json"
|
||||
chemin = Path(a.releve) if a.releve else instance_courante() / "devis-mtu.json"
|
||||
if not chemin.is_file():
|
||||
raise SystemExit(f"Aucun releve : {chemin}\nLancer d'abord `make mtu-mesurer`.")
|
||||
ecarts, lignes = analyser(json.loads(chemin.read_text()), zones_attendues())
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ RACINE = Path(__file__).resolve().parents[1]
|
|||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import underlay as underlay_mod # noqa: E402
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
from proxmox_api import Cluster # noqa: E402
|
||||
|
||||
INVENTAIRES = ("principal", "production", "lab")
|
||||
|
|
@ -51,7 +52,7 @@ def placement_du_tenant() -> tuple[dict, Path | None]:
|
|||
pour le mauvais tenant (mesure du 2026-08-20 — les deux avaient les memes quatre
|
||||
valeurs, ce qui est exactement la circonstance ou l'erreur ne se voit pas).
|
||||
"""
|
||||
base = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
base = instance_courante()
|
||||
for nom in INVENTAIRES:
|
||||
f = base / "inventories" / nom / "group_vars" / "proxmox.yml"
|
||||
if f.is_file():
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ import json
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
MOTIF = str(RACINE / "instance" / "devis-postgresql.json.*")
|
||||
MOTIF = str(instance_courante() / "devis-postgresql.json.*")
|
||||
LOCAL = (ipaddress.ip_network("127.0.0.0/8"), ipaddress.ip_network("::1/128"))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ import yaml
|
|||
RACINE = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
from inventory_rules import ( # noqa: E402
|
||||
from inventory_rules import (
|
||||
instance_courante, # noqa: E402
|
||||
charger_serveurs,
|
||||
deriver_nomenclature,
|
||||
fonction_seq,
|
||||
|
|
@ -65,7 +66,7 @@ def pool_actif() -> str:
|
|||
peuvent pas nommer le pool differemment. Chaine vide si aucune instance n'est
|
||||
liee — l'appelant omet alors le parametre plutot que d'inventer un nom.
|
||||
"""
|
||||
instance = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
instance = instance_courante()
|
||||
nomenclature = instance / "plan" / "nomenclature.yml"
|
||||
if not nomenclature.is_file():
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import sys
|
|||
from pathlib import Path
|
||||
|
||||
from inventory_rules import (
|
||||
instance_courante,
|
||||
charger_applications,
|
||||
charger_domaines,
|
||||
expositions_des_applications,
|
||||
|
|
@ -17,7 +18,7 @@ from inventory_rules import (
|
|||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
FICHIER = INSTANCE / "plan/domaines.yml"
|
||||
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ RACINE = Path(__file__).resolve().parents[1]
|
|||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import underlay as underlay_mod # noqa: E402
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
FICHIER_PARENTE = "parente.yml"
|
||||
|
||||
|
|
@ -50,7 +51,7 @@ def _git(depot: Path, *args: str) -> tuple[int, str]:
|
|||
|
||||
|
||||
def _instance() -> Path:
|
||||
return Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance")).resolve()
|
||||
return instance_courante().resolve()
|
||||
|
||||
|
||||
def _hebergeur() -> Path | None:
|
||||
|
|
|
|||
|
|
@ -40,34 +40,18 @@ from inventory_rules import (
|
|||
fonction_seq,
|
||||
integrations_de,
|
||||
integrations_universelles,
|
||||
inventaire_force,
|
||||
instance_courante,
|
||||
inventaire_de,
|
||||
liens_acceptes,
|
||||
supernet_de,
|
||||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
ROLES = RACINE / "roles"
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
|
||||
|
||||
def _inventaire(instance: Path, *noms: str) -> Path:
|
||||
for nom in noms:
|
||||
p = instance / "inventories" / nom / "hosts.yml"
|
||||
if p.exists():
|
||||
return p
|
||||
# Aucun hosts.yml encore genere : viser le repertoire d'inventaire deja present
|
||||
# (ses group_vars y sont), sinon le premier nom par defaut. Evite d'ecrire dans
|
||||
# 'principal/' alors que l'instance (ex. modele socle) est en 'production/'.
|
||||
for nom in noms:
|
||||
if (instance / "inventories" / nom).is_dir():
|
||||
return instance / "inventories" / nom / "hosts.yml"
|
||||
return instance / "inventories" / noms[0] / "hosts.yml"
|
||||
|
||||
|
||||
# Inventaire unique de l'instance (rétro-compat : principal > production).
|
||||
# SETOPS_INVENTAIRE force la cible — mais seulement A L'INTERIEUR de l'instance visée :
|
||||
# `inventaire_force` refuse une cible qui désigne une AUTRE instance (2026-08-18, P03).
|
||||
INVENTAIRE = inventaire_force(INSTANCE) or _inventaire(INSTANCE, "principal", "production")
|
||||
INVENTAIRE = inventaire_de(INSTANCE)
|
||||
GENERE = INVENTAIRE.with_name("hosts.genere.yml")
|
||||
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
||||
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ from urllib.parse import urlparse
|
|||
import yaml
|
||||
|
||||
import devis_reseau
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
from inventory_rules import (
|
||||
GROUPE_HOTES_ACTIFS,
|
||||
GROUPE_HOTES_PLANIFIES,
|
||||
|
|
@ -44,7 +46,7 @@ from inventory_rules import (
|
|||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
|
||||
|
||||
def _inventaire(instance: Path, *noms: str) -> Path:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,58 @@ GROUPES_ETAT_HOTE = {GROUPE_HOTES_ACTIFS, GROUPE_HOTES_PLANIFIES}
|
|||
RACINE_DEPOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
ORDRE_INVENTAIRE = ("principal", "production")
|
||||
ORDRE_INVENTAIRE_MODELE = ("lab", "principal", "production")
|
||||
|
||||
|
||||
def instance_courante() -> Path:
|
||||
"""L'instance VISEE : `SETOPS_INSTANCE`, sinon le symlink `instance/`.
|
||||
|
||||
SOURCE UNIQUE. Neuf modules en portaient chacun leur copie, et cinq defauts en sont
|
||||
sortis en cinq jours (2026-08-18 au 22) : P03 comparait chaque instance a l'inventaire
|
||||
d'une autre ; `verifier_ports` codait `principal/` en dur ; `verifier_intrants` et
|
||||
`_frontiere_absente` lisaient le symlink au lieu de la variable ; `devis_placement`
|
||||
rendait un verdict juste sur le mauvais tenant ; P35 aussi.
|
||||
|
||||
Aucun n'etait une faute d'inattention : chacun etait une copie ecrite de bonne foi, a
|
||||
un moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
|
||||
l'erreur, mais la DERIVE, qui ne se voit jamais depuis l'interieur d'un fichier.
|
||||
"""
|
||||
return Path(os.environ.get("SETOPS_INSTANCE") or (RACINE_DEPOT / "instance"))
|
||||
|
||||
|
||||
def dossier_inventaire(instance: Path | None = None,
|
||||
ordre: tuple[str, ...] = ORDRE_INVENTAIRE) -> Path:
|
||||
"""Le REPERTOIRE d'inventaire de l'instance, dans l'ordre de preference donne.
|
||||
|
||||
Trois niveaux, et le troisieme manquait a la moitie des copies : un `hosts.yml`
|
||||
existant, puis un REPERTOIRE existant (le cas d'une instance neuve, dont l'inventaire
|
||||
n'est pas encore genere — c'est ainsi que `make instancier` echouait sur le modele
|
||||
public), puis le premier nom par defaut.
|
||||
"""
|
||||
base = Path(instance) if instance is not None else instance_courante()
|
||||
for nom in ordre:
|
||||
if (base / "inventories" / nom / "hosts.yml").is_file():
|
||||
return base / "inventories" / nom
|
||||
for nom in ordre:
|
||||
if (base / "inventories" / nom).is_dir():
|
||||
return base / "inventories" / nom
|
||||
return base / "inventories" / ordre[0]
|
||||
|
||||
|
||||
def inventaire_de(instance: Path | None = None,
|
||||
ordre: tuple[str, ...] = ORDRE_INVENTAIRE) -> Path:
|
||||
"""Le fichier `hosts.yml` de l'instance — `SETOPS_INVENTAIRE` le force (et est garde)."""
|
||||
base = Path(instance) if instance is not None else instance_courante()
|
||||
return inventaire_force(base) or (dossier_inventaire(base, ordre) / "hosts.yml")
|
||||
|
||||
|
||||
def plan_de(instance: Path | None = None) -> Path:
|
||||
"""Le repertoire `plan/` de l'instance visee."""
|
||||
base = Path(instance) if instance is not None else instance_courante()
|
||||
return base / "plan"
|
||||
|
||||
|
||||
def inventaire_force(instance: Path) -> Path | None:
|
||||
"""Inventaire impose par SETOPS_INVENTAIRE, ou None. REFUSE s'il vise AILLEURS.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@ from pathlib import Path
|
|||
import yaml
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE", "instance"))
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
from inventory_rules import instance_courante, plan_de # noqa: E402
|
||||
|
||||
INSTANCE = instance_courante()
|
||||
DOSSIER_AUDIT = RACINE / "docs" / "audit"
|
||||
GROUPES = "playbooks/groupes"
|
||||
DEPENDANCES = "docs/dependances-groupes.yml"
|
||||
|
|
@ -277,7 +281,7 @@ def preuve_propriete_des_intrants() -> tuple[bool, str]:
|
|||
if not underlay.exists():
|
||||
return True, "Aucun hebergeur monte (pas d'underlay.yml) : separation sans objet."
|
||||
hebergeur = underlay.resolve().parent
|
||||
instance = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
instance = instance_courante()
|
||||
if instance.resolve() == hebergeur.resolve():
|
||||
cles = {} # l'hebergeur est aussi tenant : son propre depot porte les deux
|
||||
else:
|
||||
|
|
@ -312,7 +316,7 @@ def preuve_integrations_universelles() -> tuple[bool, str]:
|
|||
universelles = integrations_universelles(RACINE)
|
||||
if not universelles:
|
||||
return False, "Aucune integration universelle declaree : la politique a disparu."
|
||||
instance = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
instance = instance_courante()
|
||||
serveurs = (charger_serveurs(instance / "plan/serveurs.yml").get("serveurs") or {})
|
||||
apps = (charger_applications(instance / "plan/applications.yml").get("applications") or {})
|
||||
services: dict[str, set[str]] = {}
|
||||
|
|
@ -659,7 +663,7 @@ def preuve_etat_sauvegarde() -> tuple[bool, str]:
|
|||
ce soit. Une unite verte sur un depot vide resterait invisible ici — c'est au reel de
|
||||
le dire, pas au depot.
|
||||
"""
|
||||
plan = RACINE / "instance" / "plan"
|
||||
plan = plan_de()
|
||||
defauts = RACINE / "roles" / "client_backup" / "defaults" / "main.yml"
|
||||
if not (plan / "serveurs.yml").is_file() or not defauts.is_file():
|
||||
return True, "Aucun plan ou aucun role client_backup : rien a verifier."
|
||||
|
|
@ -778,6 +782,52 @@ JARGON_A_ENSEIGNER = [
|
|||
]
|
||||
|
||||
|
||||
def preuve_resolution_unique() -> tuple[bool, str]:
|
||||
"""Aucun module ne reecrit sa propre resolution d'instance.
|
||||
|
||||
POURQUOI CETTE PREUVE EXISTE (2026-08-22). Neuf modules portaient chacun leur copie de
|
||||
« quelle instance, quel inventaire ». Aucune n'etait une faute d'inattention : chacune
|
||||
avait ete ecrite de bonne foi, a un moment ou le besoin semblait local. En cinq jours,
|
||||
cinq defauts en sont sortis — P03 comparait chaque instance a l'inventaire d'une autre,
|
||||
`verifier_ports` codait `principal/` en dur, `verifier_intrants` et
|
||||
`_frontiere_absente` lisaient le symlink au lieu de la variable, `devis_placement` et
|
||||
P35 rendaient un verdict juste sur le mauvais ecosysteme.
|
||||
|
||||
C'est le mode de panne de la duplication : pas l'erreur, mais la DERIVE — invisible
|
||||
depuis l'interieur d'un fichier, parce que chaque copie a l'air correcte chez elle.
|
||||
|
||||
La resolution vit donc dans `inventory_rules` (`instance_courante`, `inventaire_de`,
|
||||
`dossier_inventaire`, `plan_de`). Cette preuve garde l'unicite : elle echoue des qu'un
|
||||
module relit la variable d'environnement ou recompose le chemin du symlink
|
||||
lui-meme au lieu d'appeler la resolution partagee.
|
||||
|
||||
TROIS EXEMPTIONS, nommees pour rester des choix et non des trous : `instances.py` et
|
||||
`inventory_gui.py` manipulent le SYMLINK lui-meme (c'est la bascule d'instance), et
|
||||
`devis_opnsense` lit deliberement quelle instance est ACTIVE pour se situer dans la
|
||||
federation. Ces trois-la parlent du lien, pas de la resolution.
|
||||
"""
|
||||
exemptes = {"inventory_rules.py", "instances.py", "inventory_gui.py", "devis_opnsense.py"}
|
||||
motifs = (re.compile(r'os\.environ\.get\(\s*["\']SETOPS_INSTANCE'),
|
||||
re.compile(r'RACINE(?:_DEPOT)?\s*/\s*["\']instance["\']'))
|
||||
copies: list[str] = []
|
||||
for f in sorted((RACINE / "scripts").glob("*.py")):
|
||||
if f.name in exemptes:
|
||||
continue
|
||||
texte = f.read_text(encoding="utf-8", errors="ignore")
|
||||
for i, ligne in enumerate(texte.splitlines(), 1):
|
||||
if ligne.lstrip().startswith("#"):
|
||||
continue
|
||||
if any(m.search(ligne) for m in motifs):
|
||||
copies.append(f"{f.name}:{i}")
|
||||
if copies:
|
||||
return False, (f"{len(copies)} copie(s) de la resolution d'instance : "
|
||||
+ ", ".join(copies[:6]) + ("…" if len(copies) > 6 else "")
|
||||
+ " — passer par inventory_rules.instance_courante().")
|
||||
n = len(list((RACINE / "scripts").glob("*.py"))) - len(exemptes)
|
||||
return True, (f"Resolution unique : {n} script(s) passent par "
|
||||
f"`inventory_rules`, {len(exemptes) - 1} exemption(s) nommee(s).")
|
||||
|
||||
|
||||
def preuve_parente_inscrite() -> tuple[bool, str]:
|
||||
"""L'ecosysteme sait de QUOI il descend, et cette filiation tient encore.
|
||||
|
||||
|
|
@ -1142,6 +1192,8 @@ PREUVES: list[dict] = [
|
|||
"func": preuve_glossaire_enseigne},
|
||||
{"id": "P40", "titre": "Parente : l'ecosysteme sait de quoi il descend", "refs": [],
|
||||
"func": preuve_parente_inscrite},
|
||||
{"id": "P41", "titre": "Resolution d'instance : une seule, partagee", "refs": [],
|
||||
"func": preuve_resolution_unique},
|
||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -36,12 +36,14 @@ from pathlib import Path
|
|||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from proxmox_api import Cluster # noqa: E402
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def instance_active() -> tuple[Path, str]:
|
||||
"""Dossier de l'instance montee, et son nom court (celui qu'il faudra taper)."""
|
||||
base = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
base = instance_courante()
|
||||
if not base.exists():
|
||||
raise SystemExit("Aucune instance montee : rien a raser.")
|
||||
reel = base.resolve()
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import est_groupe_operationnel
|
||||
from inventory_rules import instance_courante, est_groupe_operationnel
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
ROLES = RACINE / "roles"
|
||||
REGISTRE = RACINE / "docs" / "registre-flux.md"
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
|
||||
SENS = {"ingress", "egress"}
|
||||
# `icmp` n'a pas de port : le champ `port` porte alors le TYPE (ex. `frag-needed`).
|
||||
|
|
|
|||
|
|
@ -22,27 +22,17 @@ from inventory_rules import (
|
|||
charger_serveurs,
|
||||
fonction_seq,
|
||||
integrations_universelles,
|
||||
inventaire_force,
|
||||
instance_courante,
|
||||
inventaire_de,
|
||||
reconcilier_serveur,
|
||||
valider_serveurs,
|
||||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
FICHIER = INSTANCE / "plan/serveurs.yml"
|
||||
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
|
||||
def _inventaire(instance: Path, *noms: str) -> Path:
|
||||
forced = inventaire_force(instance) # refuse une cible hors de `instance`
|
||||
if forced:
|
||||
return forced
|
||||
for nom in noms:
|
||||
p = instance / "inventories" / nom / "hosts.yml"
|
||||
if p.exists():
|
||||
return p
|
||||
return instance / "inventories" / noms[0] / "hosts.yml"
|
||||
|
||||
|
||||
INVENTAIRE = _inventaire(INSTANCE, "principal", "production")
|
||||
INVENTAIRE = inventaire_de(INSTANCE)
|
||||
|
||||
# Champs de placement / dimensionnement NON derivables (proviennent du plan).
|
||||
CHAMPS_PLAN = [("noeud", "noeud"), ("stockage", "stockage"),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
||||
# L'ordre compte et n'est pas alphabetique : le DNS a besoin d'un certificat, l'autorite
|
||||
|
|
@ -28,7 +30,7 @@ APPLICATIONS = ["step_ca", "powerdns"]
|
|||
|
||||
|
||||
def main() -> int:
|
||||
base = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
base = instance_courante()
|
||||
plan = base / "plan" / "applications.yml"
|
||||
if not plan.exists():
|
||||
print(f"Plan introuvable : {plan}", file=sys.stderr)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ import yaml
|
|||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
from inventory_rules import supernet_de, vlan_de # noqa: E402
|
||||
from inventory_rules import instance_courante, supernet_de, vlan_de # noqa: E402
|
||||
|
||||
# Les VLAN tenant commencent a 1000+index*10+zone (>= 1011). L'underlay reste en dessous.
|
||||
SEUIL_VLAN_TENANT = 1000
|
||||
|
|
@ -315,7 +315,7 @@ def octet_passerelle(plan_nomenclature: Path | None = None) -> int | None:
|
|||
sous-reseaux ou il participe — on retient une adresse, pas treize. Declare une
|
||||
seule fois (`reservations.passerelle`), jamais code en dur ici.
|
||||
"""
|
||||
p = plan_nomenclature or (RACINE / "instance" / "plan" / "nomenclature.yml")
|
||||
p = plan_nomenclature or (instance_courante() / "plan" / "nomenclature.yml")
|
||||
if not p.is_file():
|
||||
return None
|
||||
n = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import instance_courante
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Les deux idiomes d'exigence du depot. `| bool` n'en est pas un : une valeur fausse est
|
||||
|
|
@ -202,8 +204,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
|
||||
# SETOPS_INSTANCE d'abord : le symlink `instance/` n'est que le cas courant. Le coder
|
||||
# en dur rendait cette preuve inapplicable des qu'on visait une autre instance.
|
||||
base = Path(a.instance) if a.instance else \
|
||||
Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
base = Path(a.instance) if a.instance else instance_courante()
|
||||
if not base.exists():
|
||||
print("Aucune instance montee : rien a verifier.", file=sys.stderr)
|
||||
return 2
|
||||
|
|
|
|||
|
|
@ -34,26 +34,11 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import inventaire_de
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _inventaire_par_defaut() -> Path:
|
||||
"""L'inventaire de l'instance montee — meme precedence que le reste du moteur.
|
||||
|
||||
CINQ endroits resolvent l'inventaire dans ce depot, et deux defauts en sont sortis le
|
||||
meme jour (2026-08-19) : celui-ci codait `principal/` en dur, ce qui rendait la preuve
|
||||
inapplicable a toute instance en `production/` — dont le modele public. A consolider :
|
||||
une seule resolution partagee, plutot que cinq copies qui derivent chacune de son cote.
|
||||
"""
|
||||
force = os.environ.get("SETOPS_INVENTAIRE")
|
||||
if force:
|
||||
return Path(force)
|
||||
instance = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
for nom in ("principal", "production"):
|
||||
p = instance / "inventories" / nom / "hosts.yml"
|
||||
if p.is_file():
|
||||
return p
|
||||
return instance / "inventories" / "principal" / "hosts.yml"
|
||||
|
||||
|
||||
def _groupes(inventaire: Path) -> dict[str, set[str]]:
|
||||
|
|
@ -98,7 +83,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
# (principal > production). Le chemin `principal/` etait code en dur ici, et rendait
|
||||
# cette preuve inapplicable a toute instance en `production/` — dont le modele public,
|
||||
# donc dont un depot fraichement clone (mesure du 2026-08-19).
|
||||
inventaire = Path(a.inventaire) if a.inventaire else _inventaire_par_defaut()
|
||||
inventaire = Path(a.inventaire) if a.inventaire else inventaire_de()
|
||||
if not inventaire.is_file():
|
||||
print(f"Inventaire introuvable : {inventaire}", file=sys.stderr)
|
||||
return 2
|
||||
|
|
|
|||
|
|
@ -30,11 +30,12 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import (charger_applications, charger_bases_donnees, charger_serveurs,
|
||||
from inventory_rules import (
|
||||
instance_courante,charger_applications, charger_bases_donnees, charger_serveurs,
|
||||
integrations_universelles)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INSTANCE = instance_courante()
|
||||
ROLES = RACINE / "roles"
|
||||
MOTIF_VAULT = re.compile(r"\b(vault_[a-z0-9_]+)")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue