Basculer : hosts.yml genere depuis le plan (diff vide)

make instancier-appliquer regenere inventories/production/hosts.yml
depuis le plan (serveurs.yml + applications.yml + nomenclature.yml).
Garde-fou : refuse si le diff n'est pas vide (FORCE=1 pour un changement
intentionnel) ; git est le filet.

Bascule effectuee avec diff vide verifie. hosts.yml est desormais un
artefact genere, semantiquement identique a l'ancien (make
inventaire-verifier complet OK, GUI charge 13 hotes).

NOTE : a ce stade le GUI edite encore hosts.yml directement. Tant que le
rework GUI n'est pas fait, editer le PLAN puis make instancier-appliquer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-06-23 22:00:35 -04:00
parent 69dd730b70
commit 854e324348
4 changed files with 159 additions and 141 deletions

View file

@ -3,6 +3,7 @@
## 2026-06-23
### Modifié
- **Bascule méta-classe (branche `bascule-inventaire-genere`).** `make instancier-appliquer` régénère `inventories/production/hosts.yml` **depuis le plan** (refuse si le diff n'est pas vide, sauf `FORCE=1` pour un changement intentionnel ; git sert de filet). Effectuée avec diff vide vérifié : `hosts.yml` est désormais un artefact généré, sémantiquement identique à l'ancien (vérifié par `make inventaire-verifier` complet + chargement GUI). À ce stade le GUI édite encore `hosts.yml` directement (rework à venir) : éditer le **plan** (`serveurs.yml`/`applications.yml`) puis `make instancier-appliquer`.
- **Phase 2 (début) — vue Chaîne holistique.** Dans `make inventaire-ui`, la vue Chaîne montre désormais, pour chaque application d'un hôte, l'ensemble de ses liens : `expose` (FQDN publics), `requiert` (applications dont elle dépend) et ses bases (DSN), plus son port. On voit d'un coup d'œil comment VM, applications, bases et domaines s'articulent.
- **Phase 1b — clarté de nommage.** La clé `domaines:` de `docs/nomenclature.yml` (familles fonctionnelles : `infra-pki`, `web-frontal`…) est renommée **`fonctions:`** pour lever la collision avec les domaines DNS (`docs/domaines.yml`). L'auto-proposition du GUI suit (`fonctionDe` / `infoFonction`, `nomenclature.fonctions`, libellés « Fonction inconnue… », placeholder `fonction-NN`). Aucun rôle ni inventaire impacté : la nomenclature n'est consommée qu'au moment de la planification.
- **Refonte « application-hub », Phase 1 — les liens deviennent explicites (inventaire inchangé, zéro régression).** L'application porte désormais tous ses liens : `groupe` (capacité/rôle), `hote` (VM), `port`, **`requiert`** (autres applications dont elle dépend) et **`expose`** (FQDN publics qui la publient). L'**exposition DNS quitte `docs/domaines.yml`** (le bloc `exposition` est retiré) et vit sur l'application via `expose` ; `docs/domaines.yml` ne décrit plus que les zones (autorité, edge, secondaires, dnssec, mail). `serveurs_nginx` dérive ses vhosts des **applications** (`expose` + edge du domaine), résolvant `application → hôte → IP` (rôle + `expositions.conf.j2` réécrits). Nouvelle règle partagée `expositions_des_applications` (+ `domaine_parent`) dans `inventory_rules.py`, exposée à Ansible par le filter plugin ; `valider_applications` valide `requiert` (applis connues), `expose` (domaine parent déclaré) et `port`. CLI (`applications.py --port/--requiert/--expose`, `domaines.py` affiche les expositions dérivées) et **GUI** (champs Port/Requiert/Expose sur la fiche Application, persistés) suivent ; l'API expose aussi le registre des domaines. Le groupe n'est plus une cible de liaison, seulement une capacité. *(Renommage `nomenclature.domaines:``fonctions:` reporté en Phase 1b ; génération de l'inventaire depuis le plan en Phase 3.)*

View file

@ -386,11 +386,14 @@ serveurs-verifier:
serveurs-bootstrap:
python3 scripts/serveurs.py bootstrap
.PHONY: instancier
.PHONY: instancier instancier-appliquer
instancier:
python3 scripts/instancier.py generer
python3 scripts/instancier.py comparer
instancier-appliquer:
python3 scripts/instancier.py appliquer
bases:
python3 scripts/bases_donnees.py lister

View file

@ -1,7 +1,15 @@
# Inventaire GENERE depuis le plan (make instancier / instancier-appliquer).
# NE PAS editer a la main : edite docs/serveurs.yml + docs/applications.yml.
# Source : docs/serveurs.yml + docs/applications.yml + docs/nomenclature.yml.
all:
children:
modeles_vm:
hosts: {}
clients_dns:
hosts:
collab-01: null
web-frontal-01: null
clients_supervision:
hosts:
web-frontal-01: null
hotes_actifs:
hosts: {}
hotes_planifies:
@ -10,244 +18,227 @@ all:
ansible_host: 10.1.15.21
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.15.1
proxmox_vlan: 15
proxmox_vmid: 95201
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.15.1
proxmox_stockage: TrueNAS
proxmox_vlan: 15
proxmox_vmid: 95201
data-01:
ansible_host: 10.1.13.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.13.1
proxmox_vlan: 13
proxmox_vmid: 93101
proxmox_noeud: asgard
proxmox_stockage: TrueNAS
proxmox_coeurs: 4
proxmox_disque_taille: 160G
proxmox_memoire: 2048
proxmox_coeurs: 4
proxmox_noeud: asgard
proxmox_passerelle: 10.1.13.1
proxmox_stockage: TrueNAS
proxmox_vlan: 13
proxmox_vmid: 93101
forge-01:
ansible_host: 10.1.15.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.15.1
proxmox_vlan: 15
proxmox_vmid: 95101
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.15.1
proxmox_stockage: TrueNAS
proxmox_vlan: 15
proxmox_vmid: 95101
idm-01:
ansible_host: 10.1.12.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.12.1
proxmox_vlan: 12
proxmox_vmid: 92101
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.12.1
proxmox_stockage: TrueNAS
proxmox_vlan: 12
proxmox_vmid: 92101
infra-dns-01:
ansible_host: 10.1.11.41
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.11.1
proxmox_vlan: 11
proxmox_vmid: 91401
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.11.1
proxmox_stockage: TrueNAS
proxmox_vlan: 11
proxmox_vmid: 91401
infra-edge-01:
ansible_host: 10.1.11.21
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.11.1
proxmox_vlan: 11
proxmox_vmid: 91201
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.11.1
proxmox_stockage: TrueNAS
proxmox_vlan: 11
proxmox_vmid: 91201
infra-mail-01:
ansible_host: 10.1.11.31
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.11.1
proxmox_vlan: 11
proxmox_vmid: 91301
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.11.1
proxmox_stockage: TrueNAS
proxmox_vlan: 11
proxmox_vmid: 91301
infra-pki-01:
ansible_host: 10.1.11.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.11.1
proxmox_vlan: 11
proxmox_vmid: 91101
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.11.1
proxmox_stockage: TrueNAS
proxmox_vlan: 11
proxmox_vmid: 91101
mon-01:
ansible_host: 10.1.14.41
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.14.1
proxmox_vlan: 14
proxmox_vmid: 94401
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.14.1
proxmox_stockage: TrueNAS
proxmox_vlan: 14
proxmox_vmid: 94401
obs-01:
ansible_host: 10.1.14.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.14.1
proxmox_vlan: 14
proxmox_vmid: 94101
proxmox_stockage: TrueNAS
proxmox_coeurs: 4
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 4
proxmox_passerelle: 10.1.14.1
proxmox_stockage: TrueNAS
proxmox_vlan: 14
proxmox_vmid: 94101
web-dorsal-01:
ansible_host: 10.1.15.41
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.15.1
proxmox_vlan: 15
proxmox_vmid: 95401
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.15.1
proxmox_stockage: TrueNAS
proxmox_vlan: 15
proxmox_vmid: 95401
web-frontal-01:
ansible_host: 10.1.15.31
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.15.1
proxmox_vlan: 15
proxmox_vmid: 95301
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
proxmox_passerelle: 10.1.15.1
proxmox_stockage: TrueNAS
proxmox_vlan: 15
proxmox_vmid: 95301
web-frontal-02:
ansible_host: 10.1.15.32
ansible_user: ansible
proxmox_cidr: 24
proxmox_passerelle: 10.1.15.1
proxmox_vlan: 15
proxmox_vmid: 95302
proxmox_stockage: TrueNAS
proxmox_coeurs: 2
proxmox_disque_taille: 16G
proxmox_memoire: 2048
proxmox_coeurs: 2
clients_dns:
hosts:
collab-01: {}
web-frontal-01: {}
clients_journaux:
proxmox_passerelle: 10.1.15.1
proxmox_stockage: TrueNAS
proxmox_vlan: 15
proxmox_vmid: 95302
modeles_vm:
hosts: {}
clients_ldap:
hosts: {}
clients_metriques:
hosts: {}
clients_pki:
hosts: {}
clients_smtp:
hosts: {}
clients_supervision:
hosts:
web-frontal-01: {}
serveurs_collabora:
hosts:
collab-01: {}
collab-01: null
serveurs_debian:
hosts:
collab-01: {}
data-01: {}
forge-01: {}
idm-01: {}
infra-dns-01: {}
infra-edge-01: {}
infra-mail-01: {}
infra-pki-01: {}
mon-01: {}
obs-01: {}
web-dorsal-01: {}
web-frontal-01: {}
web-frontal-02: {}
collab-01: null
data-01: null
forge-01: null
idm-01: null
infra-dns-01: null
infra-edge-01: null
infra-mail-01: null
infra-pki-01: null
mon-01: null
obs-01: null
web-dorsal-01: null
web-frontal-01: null
web-frontal-02: null
serveurs_durcis:
hosts:
collab-01: {}
data-01: {}
forge-01: {}
idm-01: {}
infra-dns-01: {}
infra-edge-01: {}
infra-mail-01: {}
infra-pki-01: {}
mon-01: {}
obs-01: {}
web-dorsal-01: {}
web-frontal-01: {}
web-frontal-02: {}
collab-01: null
data-01: null
forge-01: null
idm-01: null
infra-dns-01: null
infra-edge-01: null
infra-mail-01: null
infra-pki-01: null
mon-01: null
obs-01: null
web-dorsal-01: null
web-frontal-01: null
web-frontal-02: null
serveurs_forgejo:
hosts:
forge-01: {}
forge-01: null
serveurs_grafana:
hosts:
obs-01: {}
obs-01: null
serveurs_icinga:
hosts:
mon-01: {}
mon-01: null
serveurs_keycloak:
hosts:
idm-01: {}
idm-01: null
serveurs_loki:
hosts:
obs-01: {}
obs-01: null
serveurs_nextcloud:
hosts:
collab-01: {}
collab-01: null
serveurs_nginx:
hosts:
infra-edge-01: {}
infra-edge-01: null
serveurs_openldap:
hosts:
idm-01: {}
idm-01: null
serveurs_postgresql:
hosts:
data-01: {}
data-01: null
serveurs_powerdns:
hosts:
infra-dns-01: {}
infra-dns-01: null
serveurs_prometheus:
hosts:
obs-01: {}
obs-01: null
serveurs_redis:
hosts:
data-01: {}
data-01: null
serveurs_sendmail:
hosts:
infra-mail-01: {}
infra-mail-01: null
serveurs_step_ca:
hosts:
infra-pki-01: {}
infra-pki-01: null
serveurs_web_dorsaux:
hosts:
web-dorsal-01: {}
web-dorsal-01: null
serveurs_web_frontaux:
hosts:
web-frontal-01: {}
web-frontal-02: {}
hotes_proxmox:
hosts: {}
web-frontal-01: null
web-frontal-02: null

View file

@ -80,10 +80,11 @@ def generer() -> dict:
return {"all": {"children": children}}
def ecrire() -> None:
entete = ("# Inventaire GENERE depuis le plan (make instancier). NE PAS editer a la main.\n"
def ecrire(path: Path = GENERE) -> None:
entete = ("# Inventaire GENERE depuis le plan (make instancier / instancier-appliquer).\n"
"# NE PAS editer a la main : edite docs/serveurs.yml + docs/applications.yml.\n"
"# Source : docs/serveurs.yml + docs/applications.yml + docs/nomenclature.yml.\n")
with GENERE.open("w", encoding="utf-8") as fichier:
with path.open("w", encoding="utf-8") as fichier:
fichier.write(entete)
yaml.safe_dump(generer(), fichier, default_flow_style=False, sort_keys=True, allow_unicode=True)
@ -102,11 +103,9 @@ def _resolu(fichier: Path) -> tuple[dict, dict]:
return hostvars, groupes
def comparer() -> int:
if not GENERE.exists():
ecrire()
hv_act, grp_act = _resolu(INVENTAIRE)
hv_gen, grp_gen = _resolu(GENERE)
def compter_ecarts(reference: Path, genere: Path) -> int:
hv_act, grp_act = _resolu(reference)
hv_gen, grp_gen = _resolu(genere)
hotes = sorted(set(hv_act) | set(hv_gen))
ecarts = 0
for hote in hotes:
@ -128,10 +127,30 @@ def comparer() -> int:
if vars_diff:
details = ", ".join(f"{k} (inv={hv_act[hote].get(k)!r} gen={hv_gen[hote].get(k)!r})" for k in sorted(vars_diff))
print(f" vars differentes : {details}")
return ecarts
def comparer() -> int:
ecrire(GENERE)
ecarts = compter_ecarts(INVENTAIRE, GENERE)
if ecarts == 0:
print("DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible.")
else:
print(f"\n{ecarts} hote(s) avec ecart. Bascule INTERDITE tant que ce n'est pas vide.")
print(f"\n{ecarts} hote(s) avec ecart.")
return 0
def appliquer(force: bool = False) -> int:
"""Ecrit hosts.yml depuis le plan. Refuse si le diff n'est pas vide (sauf --force)."""
ecrire(GENERE)
ecarts = compter_ecarts(INVENTAIRE, GENERE)
if ecarts and not force:
print(f"\nDiff NON vide ({ecarts} hote(s)). Revois 'make instancier'. "
"Utilise FORCE=1 pour appliquer un changement intentionnel du plan.")
return 1
ecrire(INVENTAIRE)
print(f"\n{INVENTAIRE.relative_to(RACINE)} (re)genere depuis le plan. "
"git est le filet : git diff / git checkout pour revenir.")
return 0
@ -139,8 +158,10 @@ def main() -> int:
import argparse
parser = argparse.ArgumentParser(description="Generateur d'inventaire Set-OPS (Phase 3).")
sub = parser.add_subparsers(dest="commande", required=True)
sub.add_parser("generer", help="Ecrit hosts.yml.genere depuis le plan.")
sub.add_parser("generer", help="Ecrit hosts.genere.yml depuis le plan.")
sub.add_parser("comparer", help="Compare le genere a l'inventaire actuel (semantique).")
pa = sub.add_parser("appliquer", help="Ecrit hosts.yml depuis le plan (refuse si diff non vide).")
pa.add_argument("--force", action="store_true", help="Applique meme si le diff n'est pas vide.")
args = parser.parse_args()
try:
if args.commande == "generer":
@ -148,6 +169,8 @@ def main() -> int:
print(f"{GENERE.relative_to(RACINE)} genere depuis le plan.")
elif args.commande == "comparer":
return comparer()
elif args.commande == "appliquer":
return appliquer(force=args.force)
except Exception as exc:
print(f"erreur: {exc}", file=sys.stderr)
return 2