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
|
||||
|
||||
## 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
|
||||
|
||||
### 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")]
|
||||
|
||||
|
||||
# --- 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:
|
||||
serveurs = charger_serveurs(FICHIER_SERVEURS).get("serveurs", {})
|
||||
apps = charger_applications(FICHIER_APPLICATIONS).get("applications", {})
|
||||
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
||||
injections = resoudre_liens(apps, serveurs, nomenclature)
|
||||
|
||||
services_par_hote: dict = {}
|
||||
for app in apps.values():
|
||||
|
|
@ -96,6 +181,9 @@ def generer() -> dict:
|
|||
hostvars.setdefault("proxmox_coeurs", ressources["coeurs"])
|
||||
hostvars.setdefault("proxmox_memoire", ressources["memoire"])
|
||||
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"
|
||||
children[etat]["hosts"][nom] = hostvars
|
||||
groupes = set(GROUPES_SOCLE) | services_par_hote.get(nom, set()) | set(srv.get("integrations") or [])
|
||||
|
|
|
|||
Loading…
Reference in a new issue