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:
parent
8f6de3ebe2
commit
25d0c73275
11 changed files with 337 additions and 28 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
8
Makefile
8
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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
111
scripts/bases_donnees.py
Normal 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())
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Reference in a new issue