Gerer les bases de donnees par chaines de connexion (GUI + CLI)

Registre multi-serveurs (docs/bases-donnees.yml):
- Section serveurs_bd: serveurs de BD nommes (type/hote/port/groupe).
- Chaque base reference un serveur nomme; la chaine de connexion derivee fait
  le lien appli <-> BD. L'hote n'est plus code en dur dans les roles applicatifs.

Regles partagees (scripts/inventory_rules.py):
- charger_bases_donnees, valider_bases, chaine_connexion (GUI et CLI s'appuient dessus).

GUI (make inventaire-ui):
- Vue "Bases": serveurs, bases et chaines de connexion (mot de passe masque),
  gestion complete (ajout/edition/retrait), endpoint POST /api/bases (valide, jeton).

CLI (scripts/bases_donnees.py):
- lister/verifier/ajouter-serveur/ajouter-base/retirer-*; cibles make bases et
  bases-verifier; registre valide dans make inventaire-verifier.

Roles migres: serveurs_postgresql (filtre par serveur nomme relie a son groupe),
serveurs_keycloak / serveurs_forgejo / serveurs_icinga (hote et port depuis le registre).

Valide: ansible-lint (roles migres), node --check (JS), round-trips GUI et CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-06-22 19:14:12 -04:00
parent 8f6de3ebe2
commit 25d0c73275
11 changed files with 337 additions and 28 deletions

View file

@ -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`.

View file

@ -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

View file

@ -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 :
# <type>://<proprietaire>:<secret>@<hote>:<port>/<base>
# 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=<IP du serveur BD> port=5432 dbname=<base> user=<proprietaire>
# password={{ lookup('vars', <secret>) }} 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

View file

@ -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 }}"

View file

@ -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 }}

View file

@ -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

View file

@ -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 }}"

View file

@ -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)

111
scripts/bases_donnees.py Normal file
View file

@ -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())

View file

@ -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"""<!doctype html>
.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"""<!doctype html>
<div class="vue-bascule">
<button type="button" id="btn-vue-cartes" class="on" onclick="setVue('cartes')">Inventaire</button>
<button type="button" id="btn-vue-chaine" onclick="setVue('chaine')">Chaîne</button>
<button type="button" id="btn-vue-bases" onclick="setVue('bases')">Bases</button>
</div>
<span class="recherche"><input id="filtre" type="search" placeholder="Filtrer…" oninput="filtrer(this.value)"></span>
<button type="button" class="fantome" onclick="ajouterHote()">+ Hôte</button>
@ -556,6 +582,9 @@ HTML = r"""<!doctype html>
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"""<!doctype html>
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"""<!doctype html>
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 = '<div class="vide"><b>Inventaire vide</b>Ajoutez un hôte pour commencer.</div>'; return; }
const visibles = hotes.map((h, i) => [h, i]).filter(([h]) => correspond(h));
@ -783,6 +815,83 @@ HTML = r"""<!doctype html>
}).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) => `
<div class="base-ligne">
<label class="champ"><span>Nom</span><input value="${echapper(s.nom)}" oninput="definirServeurBd(${i}, 'nom', this.value)"></label>
<label class="champ"><span>Type</span><input value="${echapper(s.type)}" oninput="definirServeurBd(${i}, 'type', this.value)"></label>
<label class="champ"><span>Hôte</span><input value="${echapper(s.hote)}" oninput="definirServeurBd(${i}, 'hote', this.value)"></label>
<label class="champ"><span>Port</span><input type="number" value="${echapper(s.port)}" oninput="definirServeurBd(${i}, 'port', this.value)"></label>
<label class="champ"><span>Groupe Ansible</span><input value="${echapper(s.groupe)}" placeholder="serveurs_postgresql" oninput="definirServeurBd(${i}, 'groupe', this.value)"></label>
<button type="button" class="danger" title="Retirer" onclick="retirerServeurBd(${i})"></button>
</div>`).join('') : '<div class="vide" style="padding:14px">Aucun serveur de BD.</div>';
const applis = bdApplis.length ? bdApplis.map((a, i) => `
<div class="base-ligne">
<label class="champ"><span>Application</span><input value="${echapper(a.cle)}" oninput="definirBaseBd(${i}, 'cle', this.value)"></label>
<label class="champ"><span>Serveur</span><select onchange="definirBaseBd(${i}, 'serveur', this.value)">
${bdServeurs.map(s => `<option value="${echapper(s.nom)}" ${s.nom === a.serveur ? 'selected' : ''}>${echapper(s.nom)}</option>`).join('')}
</select></label>
<label class="champ"><span>Base</span><input value="${echapper(a.base)}" oninput="definirBaseBd(${i}, 'base', this.value)"></label>
<label class="champ"><span>Propriétaire</span><input value="${echapper(a.proprietaire)}" oninput="definirBaseBd(${i}, 'proprietaire', this.value)"></label>
<label class="champ"><span>Secret (Vault)</span><input value="${echapper(a.secret)}" placeholder="vault_bd_app" oninput="definirBaseBd(${i}, 'secret', this.value)"></label>
<button type="button" class="danger" title="Retirer" onclick="retirerBaseBd(${i})"></button>
<div class="dsn mono">${echapper(dsnBase(a))}</div>
</div>`).join('') : '<div class="vide" style="padding:14px">Aucune base applicative.</div>';
cible.innerHTML = `
<div class="section-grille">
<div class="section-tete">Serveurs de bases de données <button type="button" class="fantome" style="margin-left:auto" onclick="ajouterServeurBd()">+ Serveur</button></div>
<div class="bases-liste">${serveurs}</div>
</div>
<div class="section-grille">
<div class="section-tete">Bases applicatives <button type="button" class="fantome" style="margin-left:auto" onclick="ajouterBaseBd()">+ Base</button></div>
<div class="bases-liste">${applis}</div>
</div>
<div style="margin:4px 2px 20px"><button type="button" class="primaire ${basesModifie ? 'attention' : ''}" id="btn-sauver-bases" onclick="sauvegarderBases()">Sauvegarder les bases${basesModifie ? '' : ''}</button></div>`;
}
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":

View file

@ -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():