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
|
# 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
|
## 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. »*
|
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_bases_donnees,
|
||||||
charger_domaines,
|
charger_domaines,
|
||||||
charger_serveurs,
|
charger_serveurs,
|
||||||
inventaire_force,
|
instance_courante,
|
||||||
|
inventaire_de,
|
||||||
valider_applications,
|
valider_applications,
|
||||||
)
|
)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
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 = INSTANCE / "plan/applications.yml"
|
||||||
FICHIER_BASES = INSTANCE / "plan/bases-donnees.yml"
|
FICHIER_BASES = INSTANCE / "plan/bases-donnees.yml"
|
||||||
FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml"
|
FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml"
|
||||||
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
||||||
def _inventaire(instance: Path, *noms: str) -> Path:
|
INVENTAIRE = inventaire_de(INSTANCE)
|
||||||
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")
|
|
||||||
|
|
||||||
# Groupes 'serveurs_*' qui sont des capacites de SOCLE (sur toutes les VM),
|
# Groupes 'serveurs_*' qui sont des capacites de SOCLE (sur toutes les VM),
|
||||||
# pas des applications a part entiere.
|
# pas des applications a part entiere.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from pathlib import Path
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from inventory_rules import (
|
from inventory_rules import (
|
||||||
|
instance_courante,
|
||||||
chaine_connexion,
|
chaine_connexion,
|
||||||
charger_applications,
|
charger_applications,
|
||||||
charger_bases_donnees,
|
charger_bases_donnees,
|
||||||
|
|
@ -19,7 +20,7 @@ from inventory_rules import (
|
||||||
)
|
)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
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 = INSTANCE / "plan/bases-donnees.yml"
|
||||||
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ import sys
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from inventory_rules import inventaire_force
|
from inventory_rules import instance_courante, inventaire_force
|
||||||
|
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
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:
|
def _inventaire_dir(instance: Path, *noms: str) -> Path:
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,10 @@ import yaml
|
||||||
from inventory_gui import CHAMPS_ECRITS_PAR_GUI
|
from inventory_gui import CHAMPS_ECRITS_PAR_GUI
|
||||||
from modeles import decouvrir as decouvrir_modeles
|
from modeles import decouvrir as decouvrir_modeles
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante, instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
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"
|
SOURCE_GUI = RACINE / "scripts" / "inventory_gui.py"
|
||||||
|
|
||||||
# (fichier du plan, cle racine, nom du registre) — les registres tables-de-tables.
|
# (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 datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
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
|
# 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
|
# etre recharge avant. Genereux par rapport au renouvellement (~14 min), serre par
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
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:
|
def _valeur(brut: str, cle: str) -> str:
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,10 @@ import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
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
|
# 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
|
# 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
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# Adresse de controle : documentee comme non routable et attribuee a personne. Si elle
|
# 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)
|
json.dump(cibles(json.load(sys.stdin)), sys.stdout)
|
||||||
return 0
|
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():
|
if not chemin.is_file():
|
||||||
raise SystemExit(f"Aucun releve : {chemin}\nLancer d'abord `make frontiere-mesurer`.")
|
raise SystemExit(f"Aucun releve : {chemin}\nLancer d'abord `make frontiere-mesurer`.")
|
||||||
ecarts, lignes = analyser(json.loads(chemin.read_text()))
|
ecarts, lignes = analyser(json.loads(chemin.read_text()))
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
RELEVE = RACINE / "instance" / "devis-identite.json"
|
RELEVE = instance_courante() / "devis-identite.json"
|
||||||
|
|
||||||
|
|
||||||
class Ecart:
|
class Ecart:
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -73,7 +75,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
ap.add_argument("--releve", default=None)
|
ap.add_argument("--releve", default=None)
|
||||||
a = ap.parse_args(argv)
|
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():
|
if not chemin.is_file():
|
||||||
raise SystemExit(f"Aucun releve : {chemin}\nLancer d'abord `make mtu-mesurer`.")
|
raise SystemExit(f"Aucun releve : {chemin}\nLancer d'abord `make mtu-mesurer`.")
|
||||||
ecarts, lignes = analyser(json.loads(chemin.read_text()), zones_attendues())
|
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"))
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
|
||||||
import underlay as underlay_mod # noqa: E402
|
import underlay as underlay_mod # noqa: E402
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
from proxmox_api import Cluster # noqa: E402
|
from proxmox_api import Cluster # noqa: E402
|
||||||
|
|
||||||
INVENTAIRES = ("principal", "production", "lab")
|
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
|
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).
|
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:
|
for nom in INVENTAIRES:
|
||||||
f = base / "inventories" / nom / "group_vars" / "proxmox.yml"
|
f = base / "inventories" / nom / "group_vars" / "proxmox.yml"
|
||||||
if f.is_file():
|
if f.is_file():
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
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"))
|
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]
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(RACINE / "scripts"))
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
|
||||||
from inventory_rules import ( # noqa: E402
|
from inventory_rules import (
|
||||||
|
instance_courante, # noqa: E402
|
||||||
charger_serveurs,
|
charger_serveurs,
|
||||||
deriver_nomenclature,
|
deriver_nomenclature,
|
||||||
fonction_seq,
|
fonction_seq,
|
||||||
|
|
@ -65,7 +66,7 @@ def pool_actif() -> str:
|
||||||
peuvent pas nommer le pool differemment. Chaine vide si aucune instance n'est
|
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.
|
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"
|
nomenclature = instance / "plan" / "nomenclature.yml"
|
||||||
if not nomenclature.is_file():
|
if not nomenclature.is_file():
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from inventory_rules import (
|
from inventory_rules import (
|
||||||
|
instance_courante,
|
||||||
charger_applications,
|
charger_applications,
|
||||||
charger_domaines,
|
charger_domaines,
|
||||||
expositions_des_applications,
|
expositions_des_applications,
|
||||||
|
|
@ -17,7 +18,7 @@ from inventory_rules import (
|
||||||
)
|
)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
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 = INSTANCE / "plan/domaines.yml"
|
||||||
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ RACINE = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(RACINE / "scripts"))
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
|
||||||
import underlay as underlay_mod # noqa: E402
|
import underlay as underlay_mod # noqa: E402
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
FICHIER_PARENTE = "parente.yml"
|
FICHIER_PARENTE = "parente.yml"
|
||||||
|
|
||||||
|
|
@ -50,7 +51,7 @@ def _git(depot: Path, *args: str) -> tuple[int, str]:
|
||||||
|
|
||||||
|
|
||||||
def _instance() -> Path:
|
def _instance() -> Path:
|
||||||
return Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance")).resolve()
|
return instance_courante().resolve()
|
||||||
|
|
||||||
|
|
||||||
def _hebergeur() -> Path | None:
|
def _hebergeur() -> Path | None:
|
||||||
|
|
|
||||||
|
|
@ -40,34 +40,18 @@ from inventory_rules import (
|
||||||
fonction_seq,
|
fonction_seq,
|
||||||
integrations_de,
|
integrations_de,
|
||||||
integrations_universelles,
|
integrations_universelles,
|
||||||
inventaire_force,
|
instance_courante,
|
||||||
|
inventaire_de,
|
||||||
liens_acceptes,
|
liens_acceptes,
|
||||||
supernet_de,
|
supernet_de,
|
||||||
)
|
)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
ROLES = RACINE / "roles"
|
ROLES = RACINE / "roles"
|
||||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
INSTANCE = instance_courante()
|
||||||
|
|
||||||
|
|
||||||
def _inventaire(instance: Path, *noms: str) -> Path:
|
INVENTAIRE = inventaire_de(INSTANCE)
|
||||||
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")
|
|
||||||
GENERE = INVENTAIRE.with_name("hosts.genere.yml")
|
GENERE = INVENTAIRE.with_name("hosts.genere.yml")
|
||||||
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
||||||
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ from urllib.parse import urlparse
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import devis_reseau
|
import devis_reseau
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
from inventory_rules import (
|
from inventory_rules import (
|
||||||
GROUPE_HOTES_ACTIFS,
|
GROUPE_HOTES_ACTIFS,
|
||||||
GROUPE_HOTES_PLANIFIES,
|
GROUPE_HOTES_PLANIFIES,
|
||||||
|
|
@ -44,7 +46,7 @@ from inventory_rules import (
|
||||||
)
|
)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
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:
|
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]
|
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:
|
def inventaire_force(instance: Path) -> Path | None:
|
||||||
"""Inventaire impose par SETOPS_INVENTAIRE, ou None. REFUSE s'il vise AILLEURS.
|
"""Inventaire impose par SETOPS_INVENTAIRE, ou None. REFUSE s'il vise AILLEURS.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,11 @@ from pathlib import Path
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
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"
|
DOSSIER_AUDIT = RACINE / "docs" / "audit"
|
||||||
GROUPES = "playbooks/groupes"
|
GROUPES = "playbooks/groupes"
|
||||||
DEPENDANCES = "docs/dependances-groupes.yml"
|
DEPENDANCES = "docs/dependances-groupes.yml"
|
||||||
|
|
@ -277,7 +281,7 @@ def preuve_propriete_des_intrants() -> tuple[bool, str]:
|
||||||
if not underlay.exists():
|
if not underlay.exists():
|
||||||
return True, "Aucun hebergeur monte (pas d'underlay.yml) : separation sans objet."
|
return True, "Aucun hebergeur monte (pas d'underlay.yml) : separation sans objet."
|
||||||
hebergeur = underlay.resolve().parent
|
hebergeur = underlay.resolve().parent
|
||||||
instance = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
instance = instance_courante()
|
||||||
if instance.resolve() == hebergeur.resolve():
|
if instance.resolve() == hebergeur.resolve():
|
||||||
cles = {} # l'hebergeur est aussi tenant : son propre depot porte les deux
|
cles = {} # l'hebergeur est aussi tenant : son propre depot porte les deux
|
||||||
else:
|
else:
|
||||||
|
|
@ -312,7 +316,7 @@ def preuve_integrations_universelles() -> tuple[bool, str]:
|
||||||
universelles = integrations_universelles(RACINE)
|
universelles = integrations_universelles(RACINE)
|
||||||
if not universelles:
|
if not universelles:
|
||||||
return False, "Aucune integration universelle declaree : la politique a disparu."
|
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 {})
|
serveurs = (charger_serveurs(instance / "plan/serveurs.yml").get("serveurs") or {})
|
||||||
apps = (charger_applications(instance / "plan/applications.yml").get("applications") or {})
|
apps = (charger_applications(instance / "plan/applications.yml").get("applications") or {})
|
||||||
services: dict[str, set[str]] = {}
|
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
|
ce soit. Une unite verte sur un depot vide resterait invisible ici — c'est au reel de
|
||||||
le dire, pas au depot.
|
le dire, pas au depot.
|
||||||
"""
|
"""
|
||||||
plan = RACINE / "instance" / "plan"
|
plan = plan_de()
|
||||||
defauts = RACINE / "roles" / "client_backup" / "defaults" / "main.yml"
|
defauts = RACINE / "roles" / "client_backup" / "defaults" / "main.yml"
|
||||||
if not (plan / "serveurs.yml").is_file() or not defauts.is_file():
|
if not (plan / "serveurs.yml").is_file() or not defauts.is_file():
|
||||||
return True, "Aucun plan ou aucun role client_backup : rien a verifier."
|
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]:
|
def preuve_parente_inscrite() -> tuple[bool, str]:
|
||||||
"""L'ecosysteme sait de QUOI il descend, et cette filiation tient encore.
|
"""L'ecosysteme sait de QUOI il descend, et cette filiation tient encore.
|
||||||
|
|
||||||
|
|
@ -1142,6 +1192,8 @@ PREUVES: list[dict] = [
|
||||||
"func": preuve_glossaire_enseigne},
|
"func": preuve_glossaire_enseigne},
|
||||||
{"id": "P40", "titre": "Parente : l'ecosysteme sait de quoi il descend", "refs": [],
|
{"id": "P40", "titre": "Parente : l'ecosysteme sait de quoi il descend", "refs": [],
|
||||||
"func": preuve_parente_inscrite},
|
"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": [],
|
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||||
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
|
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,14 @@ from pathlib import Path
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
from proxmox_api import Cluster # noqa: E402
|
from proxmox_api import Cluster # noqa: E402
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
def instance_active() -> tuple[Path, str]:
|
def instance_active() -> tuple[Path, str]:
|
||||||
"""Dossier de l'instance montee, et son nom court (celui qu'il faudra taper)."""
|
"""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():
|
if not base.exists():
|
||||||
raise SystemExit("Aucune instance montee : rien a raser.")
|
raise SystemExit("Aucune instance montee : rien a raser.")
|
||||||
reel = base.resolve()
|
reel = base.resolve()
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from inventory_rules import est_groupe_operationnel
|
from inventory_rules import instance_courante, est_groupe_operationnel
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
ROLES = RACINE / "roles"
|
ROLES = RACINE / "roles"
|
||||||
REGISTRE = RACINE / "docs" / "registre-flux.md"
|
REGISTRE = RACINE / "docs" / "registre-flux.md"
|
||||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
INSTANCE = instance_courante()
|
||||||
|
|
||||||
SENS = {"ingress", "egress"}
|
SENS = {"ingress", "egress"}
|
||||||
# `icmp` n'a pas de port : le champ `port` porte alors le TYPE (ex. `frag-needed`).
|
# `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,
|
charger_serveurs,
|
||||||
fonction_seq,
|
fonction_seq,
|
||||||
integrations_universelles,
|
integrations_universelles,
|
||||||
inventaire_force,
|
instance_courante,
|
||||||
|
inventaire_de,
|
||||||
reconcilier_serveur,
|
reconcilier_serveur,
|
||||||
valider_serveurs,
|
valider_serveurs,
|
||||||
)
|
)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
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 = INSTANCE / "plan/serveurs.yml"
|
||||||
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
|
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
|
||||||
def _inventaire(instance: Path, *noms: str) -> Path:
|
INVENTAIRE = inventaire_de(INSTANCE)
|
||||||
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")
|
|
||||||
|
|
||||||
# Champs de placement / dimensionnement NON derivables (proviennent du plan).
|
# Champs de placement / dimensionnement NON derivables (proviennent du plan).
|
||||||
CHAMPS_PLAN = [("noeud", "noeud"), ("stockage", "stockage"),
|
CHAMPS_PLAN = [("noeud", "noeud"), ("stockage", "stockage"),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante # noqa: E402
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# L'ordre compte et n'est pas alphabetique : le DNS a besoin d'un certificat, l'autorite
|
# 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:
|
def main() -> int:
|
||||||
base = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
base = instance_courante()
|
||||||
plan = base / "plan" / "applications.yml"
|
plan = base / "plan" / "applications.yml"
|
||||||
if not plan.exists():
|
if not plan.exists():
|
||||||
print(f"Plan introuvable : {plan}", file=sys.stderr)
|
print(f"Plan introuvable : {plan}", file=sys.stderr)
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ import yaml
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
sys.path.insert(0, str(RACINE / "scripts"))
|
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.
|
# Les VLAN tenant commencent a 1000+index*10+zone (>= 1011). L'underlay reste en dessous.
|
||||||
SEUIL_VLAN_TENANT = 1000
|
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
|
sous-reseaux ou il participe — on retient une adresse, pas treize. Declare une
|
||||||
seule fois (`reservations.passerelle`), jamais code en dur ici.
|
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():
|
if not p.is_file():
|
||||||
return None
|
return None
|
||||||
n = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
n = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from inventory_rules import instance_courante
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# Les deux idiomes d'exigence du depot. `| bool` n'en est pas un : une valeur fausse est
|
# 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
|
# 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.
|
# en dur rendait cette preuve inapplicable des qu'on visait une autre instance.
|
||||||
base = Path(a.instance) if a.instance else \
|
base = Path(a.instance) if a.instance else instance_courante()
|
||||||
Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
|
||||||
if not base.exists():
|
if not base.exists():
|
||||||
print("Aucune instance montee : rien a verifier.", file=sys.stderr)
|
print("Aucune instance montee : rien a verifier.", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
|
||||||
|
|
@ -34,26 +34,11 @@ from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from inventory_rules import inventaire_de
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parent.parent
|
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]]:
|
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
|
# (principal > production). Le chemin `principal/` etait code en dur ici, et rendait
|
||||||
# cette preuve inapplicable a toute instance en `production/` — dont le modele public,
|
# cette preuve inapplicable a toute instance en `production/` — dont le modele public,
|
||||||
# donc dont un depot fraichement clone (mesure du 2026-08-19).
|
# 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():
|
if not inventaire.is_file():
|
||||||
print(f"Inventaire introuvable : {inventaire}", file=sys.stderr)
|
print(f"Inventaire introuvable : {inventaire}", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,12 @@ from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
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)
|
integrations_universelles)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
INSTANCE = instance_courante()
|
||||||
ROLES = RACINE / "roles"
|
ROLES = RACINE / "roles"
|
||||||
MOTIF_VAULT = re.compile(r"\b(vault_[a-z0-9_]+)")
|
MOTIF_VAULT = re.compile(r"\b(vault_[a-z0-9_]+)")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue