bindings Phase 1 : résolveur de liens dans instancier (relations déclaratives)
Une application déclare ses liens ([{vers, role}]) dans applications.yml ;
chaque rôle décrit les liens acceptés dans meta/liens.yml. instancier
résout la cible (FQDN interne dérivé de la nomenclature) et injecte les
variables en host_vars du consommateur.
Migration prouvée : les liens mail Postfix→Dovecot (mailstore) et
Postfix→rspamd (milter) passent de group_vars codés en dur à des liens
déclaratifs — make instancier donne DIFF VIDE (mêmes vars générées).
Cf. docs/bindings-conception.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4454e1e18b
commit
d95b26c243
3 changed files with 120 additions and 0 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,5 +1,20 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-07-03
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **Bindings — Phase 1 : résolveur de liens dans `instancier.py` (relations service→service
|
||||||
|
déclaratives).** Une application déclare ses `liens: [{vers, role}]` dans
|
||||||
|
`plan/applications.yml` ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml`
|
||||||
|
(`setops_liens.accepte`, comme `meta/empreinte.yml`). `instancier` résout la cible (FQDN interne
|
||||||
|
**dérivé de la nomenclature** + `domaine_interne`), substitue les gabarits (`{cible.fqdn}`,
|
||||||
|
`{cible.hote}`, `{cible.ip}`) et **injecte les variables en host_vars du consommateur**.
|
||||||
|
Validation : rôle accepteur, cible existante, genre attendu. **Migration prouvée** : les liens
|
||||||
|
mail Postfix→Dovecot (`mailstore`) et Postfix→rspamd (`milter`) passent de group_vars codés en
|
||||||
|
dur à des liens déclaratifs — `make instancier` donne **DIFF VIDE** (mêmes variables générées),
|
||||||
|
puis les group_vars sont retirés. La topologie mail devient déclarative et portable.
|
||||||
|
Cf. `docs/bindings-conception.md`. Suite : bases (Phase 2), exposition/domaines (Phase 3), GUI (Phase 4).
|
||||||
|
|
||||||
## 2026-07-02
|
## 2026-07-02
|
||||||
|
|
||||||
### Décidé
|
### Décidé
|
||||||
|
|
|
||||||
17
roles/serveur_postfix/meta/liens.yml
Normal file
17
roles/serveur_postfix/meta/liens.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
# Liens (bindings) acceptes par serveur_postfix — resolus par scripts/instancier.py.
|
||||||
|
# Le consommateur declare le lien dans plan/applications.yml (liens: [{vers, role}]) ;
|
||||||
|
# la cible est resolue (FQDN interne derive) et injectee en host_vars du consommateur.
|
||||||
|
# Placeholders disponibles : {cible.hote} {cible.fqdn} {cible.ip}. Cf. docs/bindings-conception.md.
|
||||||
|
setops_liens:
|
||||||
|
accepte:
|
||||||
|
mailstore: # ou remettre le courrier (Dovecot LMTP)
|
||||||
|
cible: application
|
||||||
|
injecte_sur: consommateur
|
||||||
|
variables:
|
||||||
|
serveur_postfix_mailstore_hote: "{cible.fqdn}"
|
||||||
|
milter: # filtre antispam + DKIM (rspamd), co-localise
|
||||||
|
cible: application
|
||||||
|
injecte_sur: consommateur
|
||||||
|
variables:
|
||||||
|
serveur_postfix_rspamd_milter: true
|
||||||
|
|
@ -62,10 +62,95 @@ PLACEMENT = [("noeud", "proxmox_noeud"), ("stockage", "proxmox_stockage"),
|
||||||
("coeurs", "proxmox_coeurs")]
|
("coeurs", "proxmox_coeurs")]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Bindings (liens) : relations declaratives entre applications -----------------
|
||||||
|
# Une application declare ses 'liens' ([{vers, role}]) dans plan/applications.yml ;
|
||||||
|
# chaque role decrit les liens qu'il accepte dans roles/<groupe>/meta/liens.yml
|
||||||
|
# (setops_liens.accepte). instancier resout la cible (FQDN interne derive) et injecte
|
||||||
|
# les variables en host_vars du consommateur. Cf. docs/bindings-conception.md.
|
||||||
|
|
||||||
|
def _domaine_interne() -> str:
|
||||||
|
"""Lit domaine_interne dans group_vars/all de l'instance (keystone, non chiffre)."""
|
||||||
|
base = INVENTAIRE.parent / "group_vars" / "all"
|
||||||
|
if base.is_dir():
|
||||||
|
for fichier in sorted(base.glob("*.yml")):
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
||||||
|
except yaml.YAMLError:
|
||||||
|
continue
|
||||||
|
if isinstance(data, dict) and str(data.get("domaine_interne", "")).strip():
|
||||||
|
return str(data["domaine_interne"]).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _liens_acceptes(groupe: str) -> dict:
|
||||||
|
"""meta/liens.yml d'un role -> setops_liens.accepte (liens que le role accepte)."""
|
||||||
|
fichier = ROLES / groupe / "meta" / "liens.yml"
|
||||||
|
if not fichier.is_file():
|
||||||
|
return {}
|
||||||
|
data = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
||||||
|
return ((data.get("setops_liens") or {}).get("accepte")) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _attributs_cible(vers: str, apps: dict, serveurs: dict, nomenclature: dict, domaine: str) -> dict | None:
|
||||||
|
"""Resout une cible de lien en attributs substituables. Phase 1 : applications."""
|
||||||
|
if vers in apps:
|
||||||
|
hote = apps[vers].get("hote")
|
||||||
|
srv = serveurs.get(hote, {})
|
||||||
|
_, seq = fonction_seq(str(hote))
|
||||||
|
d = deriver_nomenclature(str(srv.get("fonction", "")), seq, nomenclature) or {}
|
||||||
|
return {
|
||||||
|
"genre": "application",
|
||||||
|
"hote": hote,
|
||||||
|
"fqdn": f"{hote}.{domaine}" if domaine else str(hote),
|
||||||
|
"ip": d.get("adresse_ip"),
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _substituer(valeur, attrs: dict):
|
||||||
|
"""Remplace {cible.X} dans une chaine ; laisse bool/int tels quels."""
|
||||||
|
if not isinstance(valeur, str):
|
||||||
|
return valeur
|
||||||
|
for cle, val in attrs.items():
|
||||||
|
valeur = valeur.replace("{cible." + cle + "}", str(val))
|
||||||
|
return valeur
|
||||||
|
|
||||||
|
|
||||||
|
def resoudre_liens(apps: dict, serveurs: dict, nomenclature: dict) -> dict:
|
||||||
|
"""Derive {hote: {var: valeur}} depuis les 'liens' des applications (bindings)."""
|
||||||
|
domaine = _domaine_interne()
|
||||||
|
injections: dict = {}
|
||||||
|
for nom_app, app in apps.items():
|
||||||
|
accepte = None
|
||||||
|
for lien in (app.get("liens") or []):
|
||||||
|
vers, role = lien.get("vers"), lien.get("role")
|
||||||
|
if accepte is None:
|
||||||
|
accepte = _liens_acceptes(str(app.get("groupe", "")))
|
||||||
|
spec = accepte.get(role)
|
||||||
|
if not spec:
|
||||||
|
raise ValueError(
|
||||||
|
f"application '{nom_app}': lien role '{role}' non accepte par "
|
||||||
|
f"'{app.get('groupe')}' (roles/{app.get('groupe')}/meta/liens.yml).")
|
||||||
|
attrs = _attributs_cible(str(vers), apps, serveurs, nomenclature, domaine)
|
||||||
|
if attrs is None:
|
||||||
|
raise ValueError(f"application '{nom_app}': cible de lien '{vers}' introuvable.")
|
||||||
|
attendu = spec.get("cible")
|
||||||
|
if attendu and attendu != attrs["genre"]:
|
||||||
|
raise ValueError(
|
||||||
|
f"application '{nom_app}': lien '{role}' attend une cible '{attendu}', "
|
||||||
|
f"'{vers}' est '{attrs['genre']}'.")
|
||||||
|
sur_conso = spec.get("injecte_sur", "consommateur") == "consommateur"
|
||||||
|
cible_hote = app.get("hote") if sur_conso else attrs["hote"]
|
||||||
|
for var, gabarit in (spec.get("variables") or {}).items():
|
||||||
|
injections.setdefault(cible_hote, {})[var] = _substituer(gabarit, attrs)
|
||||||
|
return injections
|
||||||
|
|
||||||
|
|
||||||
def generer() -> dict:
|
def generer() -> dict:
|
||||||
serveurs = charger_serveurs(FICHIER_SERVEURS).get("serveurs", {})
|
serveurs = charger_serveurs(FICHIER_SERVEURS).get("serveurs", {})
|
||||||
apps = charger_applications(FICHIER_APPLICATIONS).get("applications", {})
|
apps = charger_applications(FICHIER_APPLICATIONS).get("applications", {})
|
||||||
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
||||||
|
injections = resoudre_liens(apps, serveurs, nomenclature)
|
||||||
|
|
||||||
services_par_hote: dict = {}
|
services_par_hote: dict = {}
|
||||||
for app in apps.values():
|
for app in apps.values():
|
||||||
|
|
@ -96,6 +181,9 @@ def generer() -> dict:
|
||||||
hostvars.setdefault("proxmox_coeurs", ressources["coeurs"])
|
hostvars.setdefault("proxmox_coeurs", ressources["coeurs"])
|
||||||
hostvars.setdefault("proxmox_memoire", ressources["memoire"])
|
hostvars.setdefault("proxmox_memoire", ressources["memoire"])
|
||||||
hostvars.setdefault("proxmox_disque_taille", ressources["disque"])
|
hostvars.setdefault("proxmox_disque_taille", ressources["disque"])
|
||||||
|
# Variables derivees des liens (bindings) declares par les applications.
|
||||||
|
for var, val in injections.get(nom, {}).items():
|
||||||
|
hostvars[var] = val
|
||||||
etat = "hotes_actifs" if srv.get("etat") == "actif" else "hotes_planifies"
|
etat = "hotes_actifs" if srv.get("etat") == "actif" else "hotes_planifies"
|
||||||
children[etat]["hosts"][nom] = hostvars
|
children[etat]["hosts"][nom] = hostvars
|
||||||
groupes = set(GROUPES_SOCLE) | services_par_hote.get(nom, set()) | set(srv.get("integrations") or [])
|
groupes = set(GROUPES_SOCLE) | services_par_hote.get(nom, set()) | set(srv.get("integrations") or [])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue