diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dacec5..64e9310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.)* diff --git a/Makefile b/Makefile index f9242cd..f6727c9 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/inventories/production/hosts.yml b/inventories/production/hosts.yml index 7f0bf9c..0f31942 100644 --- a/inventories/production/hosts.yml +++ b/inventories/production/hosts.yml @@ -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 diff --git a/scripts/instancier.py b/scripts/instancier.py index 6e9962b..17c304f 100644 --- a/scripts/instancier.py +++ b/scripts/instancier.py @@ -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