diff --git a/CHANGELOG.md b/CHANGELOG.md index 087ebb8..d43cd4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2026-06-22 ### Ajouté +- Gestion des bases de données dans la GUI et la CLI : vue « Bases » de `make inventaire-ui` présentant les serveurs de BD, les bases applicatives et leurs **chaînes de connexion** (mot de passe masqué), avec gestion complète (ajout / édition / retrait) et endpoint `POST /api/bases` (validé, jeton). CLI `scripts/bases_donnees.py` (`lister` / `verifier` / `ajouter-serveur` / `ajouter-base` / `retirer-*`) + cibles `make bases` et `make bases-verifier`. Le registre est désormais validé dans `make inventaire-verifier`. GUI et CLI partagent les mêmes règles (`inventory_rules`). - Rôle `serveurs_forgejo` (forge Git, Phase 5) : binaire officiel Forgejo (version épinglée + lien symbolique), utilisateur `git`, **base PostgreSQL via le registre** (4e consommateur, entrée `forgejo`), publication derrière `serveurs_nginx` (`HTTP_ADDR=127.0.0.1`, `INSTALL_LOCK`), secrets (BD + `SECRET_KEY` + `INTERNAL_TOKEN` + admin) via Vault, mailer vers `serveurs_sendmail`, compte administrateur initial créé une fois. Détails d'installation tirés de la doc officielle Forgejo. Activation de l'entrée `forgejo` dans `docs/bases-donnees.yml`. Playbook de groupe et `group_vars` production associés. - Rôle `serveurs_icinga` (supervision active — cœur, Phase 4) : dépôt apt officiel Icinga, `icinga2` + `icingadb` + `icingadb-redis`, `icinga2 api setup` + activation de la fonctionnalité `icingadb`, **base PostgreSQL via le registre** (entrée `icingadb`, import du schéma, mot de passe partagé via Vault), config Icinga DB (BD + Redis). **Icinga Web 2 différé** à une phase dédiée. Détails de configuration Icinga DB paramétrés et signalés comme non vérifiables verbatim (pages doc partiellement indisponibles). Ajout de l'entrée `icingadb` à `docs/bases-donnees.yml`. Playbook de groupe et `group_vars` production associés. - Rôle `serveurs_redis` (cache / files, paquet Debian) : surcharge **non destructive** de `redis.conf` via `include`, `requirepass` (Vault, car exposé sur le réseau interne), `maxmemory` + politique `allkeys-lru`, écoute localhost + IP interne. Complète le nœud données (data-01) aux côtés de PostgreSQL. Playbook de groupe et `group_vars` production associés. @@ -46,6 +47,7 @@ - variables de production DNS et runbook `docs/dns-interne.md`. ### Modifié +- Registre `docs/bases-donnees.yml` passé à un **modèle multi-serveurs** : section `serveurs_bd` (serveurs de BD nommés — `type`/`hôte`/`port`/`groupe`) que chaque base référence. La **chaîne de connexion dérivée** devient le lien appli↔BD ; l'hôte n'est plus codé en dur. Rôles `serveurs_postgresql` (filtre par serveur nommé relié à son groupe) et `serveurs_keycloak`/`serveurs_forgejo`/`serveurs_icinga` (hôte/port lus depuis le registre) migrés. - Robustesse du serveur `make inventaire-ui` : `repondre()` avale proprement les connexions fermées par le client (`BrokenPipeError`/`ConnectionResetError`) au lieu de déverser une trace dans la console ; ajout d'un favicon (204) pour éviter le 404 répété du navigateur. - `make deployer` et `make verifier-deploiement` détectent un `group_vars` de production chiffré (Ansible Vault) et demandent le mot de passe **une seule fois** (réutilisé pour tous les playbooks et sous-vérifications via fichier temporaire `0600`, nettoyé en fin de run). Refus propre si vault chiffré détecté en exécution **non-interactive** ; la GUI ferme `stdin` du sous-processus et fournit le mot de passe via la modale, donc elle ne bloque jamais. Ferme l'asymétrie de gestion des secrets entre terminal et GUI. - Migration complète de l'adressage interne vers `10.1.0.0/16` : golden template `basiqueChezlepro` → `10.1.99.99`, et tous les exemples `192.168.x` (README, Makefile, runbooks Proxmox et template) réécrits vers le schéma `10.1.x` / `web-frontal-01`. diff --git a/Makefile b/Makefile index 714b761..68ed6c2 100644 --- a/Makefile +++ b/Makefile @@ -370,6 +370,14 @@ inventaire-verifier: ansible-runtime ansible-inventory -i $(INVENTAIRE_PRODUCTION) --list > /dev/null python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) verifier-playbooks --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES) python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES) + python3 scripts/bases_donnees.py verifier + +.PHONY: bases bases-verifier +bases: + python3 scripts/bases_donnees.py lister + +bases-verifier: + python3 scripts/bases_donnees.py verifier inventaire-lister: ansible-runtime ansible-inventory -i $(FICHIER_INVENTAIRE) --list diff --git a/docs/bases-donnees.yml b/docs/bases-donnees.yml index b4b5ec9..f771e89 100644 --- a/docs/bases-donnees.yml +++ b/docs/bases-donnees.yml @@ -1,46 +1,45 @@ --- # Registre des bases de donnees applicatives Set-OPS. # -# Patron : 1 appli -> 1 base nommee -> 1 compte proprietaire (db_owner) -> 1 chaine de connexion. +# Patron : 1 appli -> 1 base nommee -> 1 compte proprietaire -> 1 chaine de connexion. # -# Deux consommateurs du MEME registre : -# - serveurs_postgresql : cree chaque base + son compte proprietaire -# (mot de passe = la variable Vault nommee par le champ "secret"). -# - le role applicatif : construit sa chaine de connexion vers cette base. +# Deux niveaux : +# serveurs_bd : les serveurs de base de donnees (un ou plusieurs), nommes. +# Le champ "groupe" relie un serveur nomme au groupe Ansible qui +# l'heberge (le role serveur cree les bases dont le serveur lui correspond). +# bases_donnees: une entree par appli, qui REFERENCE un serveur_bd nomme. # -# Le mot de passe est PARTAGE : il vit une seule fois dans le Vault (variable nommee -# par "secret"), lu par le serveur (pour creer le compte) et par l'appli (pour se -# connecter). Source unique, jamais de desynchronisation. +# La chaine de connexion se DERIVE : +# ://:@:/ +# Avec plusieurs serveurs de BD, c'est cette chaine (et le serveur nomme) qui fait +# le lien appli <-> base ; l'hote n'est plus code en dur dans les roles applicatifs. # -# Convention de chaine de connexion cote appli : -# host= port=5432 dbname= user= -# password={{ lookup('vars', ) }} sslmode=require -# L'IP du serveur BD se derive de la nomenclature (ex. data-01 -> 10.1.13.11). +# Le champ "secret" nomme la variable Ansible Vault contenant le mot de passe +# (jamais le mot de passe en clair). La GUI/CLI ne voient jamais le secret reel. # -# Prerequis : la dependance "appli requiert serveurs_postgresql actif" est declaree -# dans docs/dependances-groupes.yml (la base existe avant que l'appli s'y connecte). -# -# Ajouter une appli = une entree ici + son secret dans le Vault. Aucun nouveau role. +# Deux consommateurs : serveurs_postgresql (cree base + compte) et le role applicatif +# (construit sa connexion). Secret partage, source unique. + +serveurs_bd: + pg-principal: + type: postgres + hote: "10.1.13.11" + port: 5432 + groupe: serveurs_postgresql bases_donnees: keycloak: + serveur: pg-principal base: keycloak proprietaire: keycloak - serveur: serveurs_postgresql secret: vault_bd_keycloak icingadb: + serveur: pg-principal base: icingadb proprietaire: icingadb - serveur: serveurs_postgresql secret: vault_bd_icingadb -# icingaweb : a ajouter avec la phase Icinga Web 2 (2e base de l'ecosysteme Icinga). forgejo: + serveur: pg-principal base: forgejo proprietaire: forgejo - serveur: serveurs_postgresql secret: vault_bd_forgejo -# nextcloud: -# base: nextcloud -# proprietaire: nextcloud -# serveur: serveurs_postgresql -# secret: vault_bd_nextcloud diff --git a/roles/serveurs_forgejo/tasks/main.yml b/roles/serveurs_forgejo/tasks/main.yml index ad4dc8d..555fd38 100644 --- a/roles/serveurs_forgejo/tasks/main.yml +++ b/roles/serveurs_forgejo/tasks/main.yml @@ -27,6 +27,14 @@ serveurs_forgejo_db_password: "{{ lookup('vars', serveurs_forgejo_entree.secret) }}" no_log: true +- name: Resoudre l'hote et le port du serveur de base de donnees (registre) + ansible.builtin.set_fact: + serveurs_forgejo_db_host: >- + {{ (serveurs_bd | default({}))[serveurs_forgejo_entree.serveur].hote + | default(serveurs_forgejo_db_host) }} + serveurs_forgejo_db_port: >- + {{ (serveurs_bd | default({}))[serveurs_forgejo_entree.serveur].port | default(5432) }} + - name: Creer l'utilisateur systeme git ansible.builtin.user: name: "{{ serveurs_forgejo_utilisateur }}" diff --git a/roles/serveurs_forgejo/templates/app.ini.j2 b/roles/serveurs_forgejo/templates/app.ini.j2 index 0866029..ff267bf 100644 --- a/roles/serveurs_forgejo/templates/app.ini.j2 +++ b/roles/serveurs_forgejo/templates/app.ini.j2 @@ -13,7 +13,7 @@ SSH_DOMAIN = {{ serveurs_forgejo_hostname }} [database] DB_TYPE = postgres -HOST = {{ serveurs_forgejo_db_host }}:5432 +HOST = {{ serveurs_forgejo_db_host }}:{{ serveurs_forgejo_db_port | default(5432) }} NAME = {{ serveurs_forgejo_entree.base }} USER = {{ serveurs_forgejo_entree.proprietaire }} PASSWD = {{ serveurs_forgejo_db_password }} diff --git a/roles/serveurs_icinga/tasks/main.yml b/roles/serveurs_icinga/tasks/main.yml index 86e1956..567afd7 100644 --- a/roles/serveurs_icinga/tasks/main.yml +++ b/roles/serveurs_icinga/tasks/main.yml @@ -42,6 +42,12 @@ serveurs_icinga_db_password: "{{ lookup('vars', serveurs_icinga_entree.secret) }}" no_log: true +- name: Resoudre l'hote du serveur de base de donnees (registre) + ansible.builtin.set_fact: + serveurs_icinga_db_host: >- + {{ (serveurs_bd | default({}))[serveurs_icinga_entree.serveur].hote + | default(serveurs_icinga_db_host) }} + - name: Configurer l'API Icinga 2 ansible.builtin.command: cmd: icinga2 api setup diff --git a/roles/serveurs_keycloak/tasks/main.yml b/roles/serveurs_keycloak/tasks/main.yml index d823799..948f883 100644 --- a/roles/serveurs_keycloak/tasks/main.yml +++ b/roles/serveurs_keycloak/tasks/main.yml @@ -26,6 +26,12 @@ serveurs_keycloak_db_password: "{{ lookup('vars', serveurs_keycloak_entree.secret) }}" no_log: true +- name: Resoudre l'hote du serveur de base de donnees (registre) + ansible.builtin.set_fact: + serveurs_keycloak_db_host: >- + {{ (serveurs_bd | default({}))[serveurs_keycloak_entree.serveur].hote + | default(serveurs_keycloak_db_host) }} + - name: Installer le JRE ansible.builtin.apt: name: "{{ serveurs_keycloak_java_paquet }}" diff --git a/roles/serveurs_postgresql/tasks/main.yml b/roles/serveurs_postgresql/tasks/main.yml index f06ebcf..22d1aa0 100644 --- a/roles/serveurs_postgresql/tasks/main.yml +++ b/roles/serveurs_postgresql/tasks/main.yml @@ -98,12 +98,20 @@ file: "{{ role_path }}/../../docs/bases-donnees.yml" failed_when: false +- name: Determiner les serveurs BD nommes heberges par ce groupe + ansible.builtin.set_fact: + serveurs_postgresql_noms_bd: >- + {{ (serveurs_bd | default({})) | dict2items + | selectattr('value.groupe', 'defined') + | selectattr('value.groupe', 'equalto', serveurs_postgresql_groupe) + | map(attribute='key') | list }} + - name: Selectionner les bases hebergees par ce serveur ansible.builtin.set_fact: serveurs_postgresql_registre: >- {{ (bases_donnees | default({})) | dict2items | selectattr('value.serveur', 'defined') - | selectattr('value.serveur', 'equalto', serveurs_postgresql_groupe) + | selectattr('value.serveur', 'in', serveurs_postgresql_noms_bd) | list }} - name: Creer les comptes proprietaires (registre) diff --git a/scripts/bases_donnees.py b/scripts/bases_donnees.py new file mode 100644 index 0000000..ccf9b6c --- /dev/null +++ b/scripts/bases_donnees.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Gere le registre des bases de donnees Set-OPS (CLI, miroir de make inventaire-ui).""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import yaml + +from inventory_rules import chaine_connexion, charger_bases_donnees, valider_bases + +RACINE = Path(__file__).resolve().parents[1] +FICHIER = RACINE / "docs/bases-donnees.yml" + + +def ecrire(registre: dict) -> None: + entete = ( + "# Registre des bases de donnees Set-OPS (serveurs_bd + bases_donnees).\n" + "# Edite par make inventaire-ui ou scripts/bases_donnees.py.\n" + "# Le champ 'secret' nomme une variable Ansible Vault (jamais le mot de passe).\n" + "---\n" + ) + contenu = { + "serveurs_bd": registre.get("serveurs_bd", {}) or {}, + "bases_donnees": registre.get("bases_donnees", {}) or {}, + } + with FICHIER.open("w", encoding="utf-8") as fichier: + fichier.write(entete) + yaml.safe_dump(contenu, fichier, default_flow_style=False, sort_keys=False, allow_unicode=True) + + +def lister(registre: dict) -> None: + serveurs = registre.get("serveurs_bd", {}) + print("Serveurs de bases de donnees:") + for nom, srv in serveurs.items(): + print(f" {nom}: {srv.get('type')} {srv.get('hote')}:{srv.get('port')} (groupe {srv.get('groupe', '-')})") + print("Bases applicatives:") + for cle, base in registre.get("bases_donnees", {}).items(): + srv = serveurs.get(base.get("serveur"), {}) + print(f" {cle}: {chaine_connexion(base, srv)} [secret {base.get('secret')}]") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Registre des bases de donnees Set-OPS.") + sub = parser.add_subparsers(dest="commande", required=True) + sub.add_parser("lister", help="Affiche serveurs, bases et chaines de connexion.") + sub.add_parser("verifier", help="Valide la coherence du registre.") + + ps = sub.add_parser("ajouter-serveur", help="Ajoute ou met a jour un serveur de BD.") + ps.add_argument("--nom", required=True) + ps.add_argument("--type", default="postgres") + ps.add_argument("--hote", required=True) + ps.add_argument("--port", type=int, default=5432) + ps.add_argument("--groupe", default="") + + pb = sub.add_parser("ajouter-base", help="Ajoute ou met a jour une base applicative.") + pb.add_argument("--cle", required=True) + pb.add_argument("--serveur", required=True) + pb.add_argument("--base", required=True) + pb.add_argument("--proprietaire", required=True) + pb.add_argument("--secret", required=True) + + pr = sub.add_parser("retirer-base", help="Retire une base applicative.") + pr.add_argument("--cle", required=True) + prs = sub.add_parser("retirer-serveur", help="Retire un serveur de BD.") + prs.add_argument("--nom", required=True) + + args = parser.parse_args() + try: + registre = charger_bases_donnees(FICHIER) + if args.commande == "lister": + lister(registre) + elif args.commande == "verifier": + valider_bases(registre) + print("Registre des bases valide.") + elif args.commande == "ajouter-serveur": + registre["serveurs_bd"][args.nom] = { + "type": args.type, "hote": args.hote, "port": args.port, + **({"groupe": args.groupe} if args.groupe else {}), + } + valider_bases(registre) + ecrire(registre) + print(f"Serveur de BD '{args.nom}' enregistre.") + elif args.commande == "ajouter-base": + registre["bases_donnees"][args.cle] = { + "serveur": args.serveur, "base": args.base, + "proprietaire": args.proprietaire, "secret": args.secret, + } + valider_bases(registre) + ecrire(registre) + print(f"Base '{args.cle}' enregistree.") + elif args.commande == "retirer-base": + registre["bases_donnees"].pop(args.cle, None) + valider_bases(registre) + ecrire(registre) + print(f"Base '{args.cle}' retiree.") + elif args.commande == "retirer-serveur": + registre["serveurs_bd"].pop(args.nom, None) + valider_bases(registre) + ecrire(registre) + print(f"Serveur de BD '{args.nom}' retire.") + except Exception as exc: + print(f"erreur: {exc}", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 112a82a..82da003 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -21,10 +21,13 @@ from inventory_rules import ( GROUPE_HOTES_ACTIFS, GROUPE_HOTES_PLANIFIES, GROUPES_ETAT_HOTE, + chaine_connexion, + charger_bases_donnees, charger_dependances, charger_nomenclature, est_groupe_operationnel, groupes_operationnels_connus, + valider_bases, ) RACINE = Path(__file__).resolve().parents[1] @@ -33,6 +36,7 @@ INVENTAIRE_PRODUCTION = (RACINE / "inventories/production/hosts.yml").resolve() DOSSIER_PLAYBOOKS_GROUPES = RACINE / "playbooks/groupes" FICHIER_DEPENDANCES = RACINE / "docs/dependances-groupes.yml" FICHIER_NOMENCLATURE = RACINE / "docs/nomenclature.yml" +FICHIER_BASES = RACINE / "docs/bases-donnees.yml" # Garde-fous des executions (verifier / deployer) depuis l'interface. JETON = secrets.token_urlsafe(18) @@ -170,10 +174,27 @@ def inventaire_api(path: Path) -> dict: "dependances": dependencies, "nomenclature": charger_nomenclature(FICHIER_NOMENCLATURE), "chaine": roles_des_groupes(), + "bases": charger_bases_donnees(FICHIER_BASES), "hotes": liste_hotes(data), } +def ecrire_bases(path: Path, registre: dict) -> None: + entete = ( + "# Registre des bases de donnees Set-OPS (serveurs_bd + bases_donnees).\n" + "# Edite par make inventaire-ui ou scripts/bases_donnees.py.\n" + "# Le champ 'secret' nomme une variable Ansible Vault (jamais le mot de passe).\n" + "---\n" + ) + contenu = { + "serveurs_bd": registre.get("serveurs_bd", {}) or {}, + "bases_donnees": registre.get("bases_donnees", {}) or {}, + } + with path.open("w", encoding="utf-8") as fichier: + fichier.write(entete) + yaml.safe_dump(contenu, fichier, default_flow_style=False, sort_keys=False, allow_unicode=True) + + def donnees_hote(hote: dict) -> dict: variables: dict = {} for cle, var, typ in CHAMPS_PROVISION: @@ -473,6 +494,10 @@ HTML = r""" .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--teal); font-size: 12px; } .ch-roles { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; } .ch-stub { color: var(--ambre); font-size: 11.5px; font-style: italic; margin-top: 5px; } + .bases-liste { display: grid; gap: 10px; } + .base-ligne { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)) auto; gap: 9px 11px; align-items: end; background: var(--surface); border: 1px solid var(--ligne); border-radius: var(--rayon-sm); padding: 11px 12px; box-shadow: var(--ombre); } + .base-ligne .danger { align-self: end; } + .base-ligne .dsn { grid-column: 1 / -1; color: var(--muted); font-size: 11.5px; padding-top: 2px; overflow-wrap: anywhere; } .modale { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.55); } .modale[hidden] { display: none; } @@ -505,6 +530,7 @@ HTML = r"""
+
@@ -556,6 +582,9 @@ HTML = r""" let filtre = ''; let filtreEtat = 'tous'; let vuePrincipale = 'cartes'; + let bdServeurs = []; + let bdApplis = []; + let basesModifie = false; let selection = -1; let selectionNom = null; let ongletActif = 'reseau'; @@ -598,6 +627,7 @@ HTML = r""" dependances = data.dependances || {}; nomenclature = data.nomenclature || {}; chaine = data.chaine || {}; + chargerBases(data); hotes = data.hotes; estProduction = !!data.production; modifie = false; verifie = {}; @@ -734,12 +764,14 @@ HTML = r""" vuePrincipale = v; document.getElementById('btn-vue-cartes').classList.toggle('on', v === 'cartes'); document.getElementById('btn-vue-chaine').classList.toggle('on', v === 'chaine'); + document.getElementById('btn-vue-bases').classList.toggle('on', v === 'bases'); dessinerGrilles(); } function dessinerGrilles() { - document.getElementById('chips').style.display = vuePrincipale === 'chaine' ? 'none' : ''; + document.getElementById('chips').style.display = vuePrincipale === 'cartes' ? '' : 'none'; if (vuePrincipale === 'chaine') { dessinerArbre(); return; } + if (vuePrincipale === 'bases') { dessinerBases(); return; } const cible = document.getElementById('grilles'); if (!hotes.length) { cible.innerHTML = '
Inventaire videAjoutez un hôte pour commencer.
'; return; } const visibles = hotes.map((h, i) => [h, i]).filter(([h]) => correspond(h)); @@ -783,6 +815,83 @@ HTML = r""" }).join(''); } + function chargerBases(data) { + const b = data.bases || {serveurs_bd: {}, bases_donnees: {}}; + bdServeurs = Object.entries(b.serveurs_bd || {}).map(([nom, s]) => ({ + nom, type: s.type || 'postgres', hote: s.hote || '', port: s.port || 5432, groupe: s.groupe || ''})); + bdApplis = Object.entries(b.bases_donnees || {}).map(([cle, x]) => ({ + cle, serveur: x.serveur || '', base: x.base || '', proprietaire: x.proprietaire || '', secret: x.secret || ''})); + basesModifie = false; + } + + function dsnBase(a) { + const s = bdServeurs.find(x => x.nom === a.serveur); + if (!s) return '(serveur inconnu : ' + echapper(a.serveur || '—') + ')'; + return `${s.type}://${a.proprietaire || '?'}:****@${s.hote || '?'}:${s.port || '?'}/${a.base || '?'}`; + } + + function marquerBasesModifie() { + basesModifie = true; + const b = document.getElementById('btn-sauver-bases'); + if (b) { b.classList.toggle('attention', true); b.textContent = 'Sauvegarder les bases •'; } + } + function definirServeurBd(i, champ, v) { bdServeurs[i][champ] = v; marquerBasesModifie(); } + function definirBaseBd(i, champ, v) { bdApplis[i][champ] = v; marquerBasesModifie(); if (champ === 'serveur') dessinerBases(); } + function ajouterServeurBd() { bdServeurs.push({nom: '', type: 'postgres', hote: '', port: 5432, groupe: ''}); marquerBasesModifie(); dessinerBases(); } + function retirerServeurBd(i) { bdServeurs.splice(i, 1); marquerBasesModifie(); dessinerBases(); } + function ajouterBaseBd() { bdApplis.push({cle: '', serveur: (bdServeurs[0] || {}).nom || '', base: '', proprietaire: '', secret: ''}); marquerBasesModifie(); dessinerBases(); } + function retirerBaseBd(i) { bdApplis.splice(i, 1); marquerBasesModifie(); dessinerBases(); } + + function dessinerBases() { + const cible = document.getElementById('grilles'); + const serveurs = bdServeurs.length ? bdServeurs.map((s, i) => ` +
+ + + + + + +
`).join('') : '
Aucun serveur de BD.
'; + const applis = bdApplis.length ? bdApplis.map((a, i) => ` +
+ + + + + + +
${echapper(dsnBase(a))}
+
`).join('') : '
Aucune base applicative.
'; + cible.innerHTML = ` +
+
Serveurs de bases de données
+
${serveurs}
+
+
+
Bases applicatives
+
${applis}
+
+
`; + } + + async function sauvegarderBases() { + const sd = {}; + bdServeurs.forEach(s => { if (String(s.nom).trim()) sd[s.nom] = {type: s.type, hote: s.hote, port: parseInt(s.port, 10) || s.port, ...(s.groupe ? {groupe: s.groupe} : {})}; }); + const bd = {}; + bdApplis.forEach(a => { if (String(a.cle).trim()) bd[a.cle] = {serveur: a.serveur, base: a.base, proprietaire: a.proprietaire, secret: a.secret}; }); + const rep = await fetch('/api/bases', { + method: 'POST', headers: {'Content-Type': 'application/json', 'X-Jeton': JETON}, body: JSON.stringify({serveurs_bd: sd, bases_donnees: bd}) + }); + const data = await rep.json(); + if (!rep.ok) { message(data.erreur || 'Sauvegarde refusée.', 'erreur'); return; } + chargerBases(data); + dessinerBases(); + message('Bases de données sauvegardées.', 'ok'); + } + function dessinerDependances() { const cible = document.getElementById('dependances'); const noms = Object.keys(dependances).sort(); @@ -1202,6 +1311,10 @@ class Gestionnaire(BaseHTTPRequestHandler): ecrire_yaml(self.inventaire, nouveau) VERIF_OK.clear() self.repondre_json(200, inventaire_api(self.inventaire)) + elif chemin == "/api/bases": + valider_bases(donnees) + ecrire_bases(FICHIER_BASES, donnees) + self.repondre_json(200, inventaire_api(self.inventaire)) elif chemin == "/api/verifier": self.executer_flux(str(donnees.get("hote", "")), "verifier", donnees.get("vault")) elif chemin == "/api/deployer": diff --git a/scripts/inventory_rules.py b/scripts/inventory_rules.py index 042b691..bc49b75 100644 --- a/scripts/inventory_rules.py +++ b/scripts/inventory_rules.py @@ -24,6 +24,54 @@ def groupes_operationnels_connus(enfants: dict, dossier_playbooks: Path) -> list return sorted(groupe for groupe in groupes if est_groupe_operationnel(groupe)) +def charger_bases_donnees(path: Path | None) -> dict: + """Charge le registre des bases de donnees (serveurs_bd + bases_donnees).""" + if not path or not path.exists(): + return {"serveurs_bd": {}, "bases_donnees": {}} + with path.open("r", encoding="utf-8") as fichier: + data = yaml.safe_load(fichier) or {} + if not isinstance(data, dict): + raise ValueError(f"{path} ne contient pas une table YAML.") + data.setdefault("serveurs_bd", {}) + data.setdefault("bases_donnees", {}) + return data + + +def valider_bases(registre: dict) -> None: + """Valide la coherence du registre des bases de donnees.""" + serveurs = registre.get("serveurs_bd") or {} + bases = registre.get("bases_donnees") or {} + if not isinstance(serveurs, dict) or not isinstance(bases, dict): + raise ValueError("serveurs_bd et bases_donnees doivent etre des tables.") + for nom, srv in serveurs.items(): + if not isinstance(srv, dict): + raise ValueError(f"Serveur BD '{nom}': table attendue.") + for champ in ("type", "hote", "port"): + if not str(srv.get(champ, "")).strip(): + raise ValueError(f"Serveur BD '{nom}': champ '{champ}' requis.") + noms_bases: set[str] = set() + for cle, base in bases.items(): + if not isinstance(base, dict): + raise ValueError(f"Base '{cle}': table attendue.") + for champ in ("serveur", "base", "proprietaire", "secret"): + if not str(base.get(champ, "")).strip(): + raise ValueError(f"Base '{cle}': champ '{champ}' requis.") + if base["serveur"] not in serveurs: + raise ValueError(f"Base '{cle}': serveur '{base['serveur']}' inconnu.") + couple = f"{base['serveur']}/{base['base']}" + if couple in noms_bases: + raise ValueError(f"Base en double sur un meme serveur: {couple}") + noms_bases.add(couple) + + +def chaine_connexion(entree: dict, serveur: dict, secret: str = "***") -> str: + """Derive une chaine de connexion (mot de passe masque par defaut).""" + type_bd = (serveur or {}).get("type", "postgres") + hote = (serveur or {}).get("hote", "?") + port = (serveur or {}).get("port", "?") + return f"{type_bd}://{entree.get('proprietaire', '?')}:{secret}@{hote}:{port}/{entree.get('base', '?')}" + + def charger_nomenclature(path: Path | None) -> dict: """Charge le registre de nomenclature (domaines, categories, adressage).""" if not path or not path.exists():