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
|
## 2026-06-22
|
||||||
|
|
||||||
### Ajouté
|
### 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_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_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.
|
- 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`.
|
- variables de production DNS et runbook `docs/dns-interne.md`.
|
||||||
|
|
||||||
### Modifié
|
### 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.
|
- 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.
|
- `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`.
|
- 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
|
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) 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/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
|
inventaire-lister: ansible-runtime
|
||||||
ansible-inventory -i $(FICHIER_INVENTAIRE) --list
|
ansible-inventory -i $(FICHIER_INVENTAIRE) --list
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,45 @@
|
||||||
---
|
---
|
||||||
# Registre des bases de donnees applicatives Set-OPS.
|
# 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 :
|
# Deux niveaux :
|
||||||
# - serveurs_postgresql : cree chaque base + son compte proprietaire
|
# serveurs_bd : les serveurs de base de donnees (un ou plusieurs), nommes.
|
||||||
# (mot de passe = la variable Vault nommee par le champ "secret").
|
# Le champ "groupe" relie un serveur nomme au groupe Ansible qui
|
||||||
# - le role applicatif : construit sa chaine de connexion vers cette base.
|
# 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
|
# La chaine de connexion se DERIVE :
|
||||||
# par "secret"), lu par le serveur (pour creer le compte) et par l'appli (pour se
|
# <type>://<proprietaire>:<secret>@<hote>:<port>/<base>
|
||||||
# connecter). Source unique, jamais de desynchronisation.
|
# 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 :
|
# Le champ "secret" nomme la variable Ansible Vault contenant le mot de passe
|
||||||
# host=<IP du serveur BD> port=5432 dbname=<base> user=<proprietaire>
|
# (jamais le mot de passe en clair). La GUI/CLI ne voient jamais le secret reel.
|
||||||
# password={{ lookup('vars', <secret>) }} sslmode=require
|
|
||||||
# L'IP du serveur BD se derive de la nomenclature (ex. data-01 -> 10.1.13.11).
|
|
||||||
#
|
#
|
||||||
# Prerequis : la dependance "appli requiert serveurs_postgresql actif" est declaree
|
# Deux consommateurs : serveurs_postgresql (cree base + compte) et le role applicatif
|
||||||
# dans docs/dependances-groupes.yml (la base existe avant que l'appli s'y connecte).
|
# (construit sa connexion). Secret partage, source unique.
|
||||||
#
|
|
||||||
# Ajouter une appli = une entree ici + son secret dans le Vault. Aucun nouveau role.
|
serveurs_bd:
|
||||||
|
pg-principal:
|
||||||
|
type: postgres
|
||||||
|
hote: "10.1.13.11"
|
||||||
|
port: 5432
|
||||||
|
groupe: serveurs_postgresql
|
||||||
|
|
||||||
bases_donnees:
|
bases_donnees:
|
||||||
keycloak:
|
keycloak:
|
||||||
|
serveur: pg-principal
|
||||||
base: keycloak
|
base: keycloak
|
||||||
proprietaire: keycloak
|
proprietaire: keycloak
|
||||||
serveur: serveurs_postgresql
|
|
||||||
secret: vault_bd_keycloak
|
secret: vault_bd_keycloak
|
||||||
icingadb:
|
icingadb:
|
||||||
|
serveur: pg-principal
|
||||||
base: icingadb
|
base: icingadb
|
||||||
proprietaire: icingadb
|
proprietaire: icingadb
|
||||||
serveur: serveurs_postgresql
|
|
||||||
secret: vault_bd_icingadb
|
secret: vault_bd_icingadb
|
||||||
# icingaweb : a ajouter avec la phase Icinga Web 2 (2e base de l'ecosysteme Icinga).
|
|
||||||
forgejo:
|
forgejo:
|
||||||
|
serveur: pg-principal
|
||||||
base: forgejo
|
base: forgejo
|
||||||
proprietaire: forgejo
|
proprietaire: forgejo
|
||||||
serveur: serveurs_postgresql
|
|
||||||
secret: vault_bd_forgejo
|
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) }}"
|
serveurs_forgejo_db_password: "{{ lookup('vars', serveurs_forgejo_entree.secret) }}"
|
||||||
no_log: true
|
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
|
- name: Creer l'utilisateur systeme git
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ serveurs_forgejo_utilisateur }}"
|
name: "{{ serveurs_forgejo_utilisateur }}"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ SSH_DOMAIN = {{ serveurs_forgejo_hostname }}
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
DB_TYPE = postgres
|
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 }}
|
NAME = {{ serveurs_forgejo_entree.base }}
|
||||||
USER = {{ serveurs_forgejo_entree.proprietaire }}
|
USER = {{ serveurs_forgejo_entree.proprietaire }}
|
||||||
PASSWD = {{ serveurs_forgejo_db_password }}
|
PASSWD = {{ serveurs_forgejo_db_password }}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@
|
||||||
serveurs_icinga_db_password: "{{ lookup('vars', serveurs_icinga_entree.secret) }}"
|
serveurs_icinga_db_password: "{{ lookup('vars', serveurs_icinga_entree.secret) }}"
|
||||||
no_log: true
|
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
|
- name: Configurer l'API Icinga 2
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: icinga2 api setup
|
cmd: icinga2 api setup
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@
|
||||||
serveurs_keycloak_db_password: "{{ lookup('vars', serveurs_keycloak_entree.secret) }}"
|
serveurs_keycloak_db_password: "{{ lookup('vars', serveurs_keycloak_entree.secret) }}"
|
||||||
no_log: true
|
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
|
- name: Installer le JRE
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
name: "{{ serveurs_keycloak_java_paquet }}"
|
name: "{{ serveurs_keycloak_java_paquet }}"
|
||||||
|
|
|
||||||
|
|
@ -98,12 +98,20 @@
|
||||||
file: "{{ role_path }}/../../docs/bases-donnees.yml"
|
file: "{{ role_path }}/../../docs/bases-donnees.yml"
|
||||||
failed_when: false
|
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
|
- name: Selectionner les bases hebergees par ce serveur
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
serveurs_postgresql_registre: >-
|
serveurs_postgresql_registre: >-
|
||||||
{{ (bases_donnees | default({})) | dict2items
|
{{ (bases_donnees | default({})) | dict2items
|
||||||
| selectattr('value.serveur', 'defined')
|
| selectattr('value.serveur', 'defined')
|
||||||
| selectattr('value.serveur', 'equalto', serveurs_postgresql_groupe)
|
| selectattr('value.serveur', 'in', serveurs_postgresql_noms_bd)
|
||||||
| list }}
|
| list }}
|
||||||
|
|
||||||
- name: Creer les comptes proprietaires (registre)
|
- 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_ACTIFS,
|
||||||
GROUPE_HOTES_PLANIFIES,
|
GROUPE_HOTES_PLANIFIES,
|
||||||
GROUPES_ETAT_HOTE,
|
GROUPES_ETAT_HOTE,
|
||||||
|
chaine_connexion,
|
||||||
|
charger_bases_donnees,
|
||||||
charger_dependances,
|
charger_dependances,
|
||||||
charger_nomenclature,
|
charger_nomenclature,
|
||||||
est_groupe_operationnel,
|
est_groupe_operationnel,
|
||||||
groupes_operationnels_connus,
|
groupes_operationnels_connus,
|
||||||
|
valider_bases,
|
||||||
)
|
)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
|
|
@ -33,6 +36,7 @@ INVENTAIRE_PRODUCTION = (RACINE / "inventories/production/hosts.yml").resolve()
|
||||||
DOSSIER_PLAYBOOKS_GROUPES = RACINE / "playbooks/groupes"
|
DOSSIER_PLAYBOOKS_GROUPES = RACINE / "playbooks/groupes"
|
||||||
FICHIER_DEPENDANCES = RACINE / "docs/dependances-groupes.yml"
|
FICHIER_DEPENDANCES = RACINE / "docs/dependances-groupes.yml"
|
||||||
FICHIER_NOMENCLATURE = RACINE / "docs/nomenclature.yml"
|
FICHIER_NOMENCLATURE = RACINE / "docs/nomenclature.yml"
|
||||||
|
FICHIER_BASES = RACINE / "docs/bases-donnees.yml"
|
||||||
|
|
||||||
# Garde-fous des executions (verifier / deployer) depuis l'interface.
|
# Garde-fous des executions (verifier / deployer) depuis l'interface.
|
||||||
JETON = secrets.token_urlsafe(18)
|
JETON = secrets.token_urlsafe(18)
|
||||||
|
|
@ -170,10 +174,27 @@ def inventaire_api(path: Path) -> dict:
|
||||||
"dependances": dependencies,
|
"dependances": dependencies,
|
||||||
"nomenclature": charger_nomenclature(FICHIER_NOMENCLATURE),
|
"nomenclature": charger_nomenclature(FICHIER_NOMENCLATURE),
|
||||||
"chaine": roles_des_groupes(),
|
"chaine": roles_des_groupes(),
|
||||||
|
"bases": charger_bases_donnees(FICHIER_BASES),
|
||||||
"hotes": liste_hotes(data),
|
"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:
|
def donnees_hote(hote: dict) -> dict:
|
||||||
variables: dict = {}
|
variables: dict = {}
|
||||||
for cle, var, typ in CHAMPS_PROVISION:
|
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; }
|
.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-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; }
|
.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 { 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; }
|
.modale[hidden] { display: none; }
|
||||||
|
|
@ -505,6 +530,7 @@ HTML = r"""<!doctype html>
|
||||||
<div class="vue-bascule">
|
<div class="vue-bascule">
|
||||||
<button type="button" id="btn-vue-cartes" class="on" onclick="setVue('cartes')">Inventaire</button>
|
<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-chaine" onclick="setVue('chaine')">Chaîne</button>
|
||||||
|
<button type="button" id="btn-vue-bases" onclick="setVue('bases')">Bases</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="recherche"><input id="filtre" type="search" placeholder="Filtrer…" oninput="filtrer(this.value)"></span>
|
<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>
|
<button type="button" class="fantome" onclick="ajouterHote()">+ Hôte</button>
|
||||||
|
|
@ -556,6 +582,9 @@ HTML = r"""<!doctype html>
|
||||||
let filtre = '';
|
let filtre = '';
|
||||||
let filtreEtat = 'tous';
|
let filtreEtat = 'tous';
|
||||||
let vuePrincipale = 'cartes';
|
let vuePrincipale = 'cartes';
|
||||||
|
let bdServeurs = [];
|
||||||
|
let bdApplis = [];
|
||||||
|
let basesModifie = false;
|
||||||
let selection = -1;
|
let selection = -1;
|
||||||
let selectionNom = null;
|
let selectionNom = null;
|
||||||
let ongletActif = 'reseau';
|
let ongletActif = 'reseau';
|
||||||
|
|
@ -598,6 +627,7 @@ HTML = r"""<!doctype html>
|
||||||
dependances = data.dependances || {};
|
dependances = data.dependances || {};
|
||||||
nomenclature = data.nomenclature || {};
|
nomenclature = data.nomenclature || {};
|
||||||
chaine = data.chaine || {};
|
chaine = data.chaine || {};
|
||||||
|
chargerBases(data);
|
||||||
hotes = data.hotes;
|
hotes = data.hotes;
|
||||||
estProduction = !!data.production;
|
estProduction = !!data.production;
|
||||||
modifie = false; verifie = {};
|
modifie = false; verifie = {};
|
||||||
|
|
@ -734,12 +764,14 @@ HTML = r"""<!doctype html>
|
||||||
vuePrincipale = v;
|
vuePrincipale = v;
|
||||||
document.getElementById('btn-vue-cartes').classList.toggle('on', v === 'cartes');
|
document.getElementById('btn-vue-cartes').classList.toggle('on', v === 'cartes');
|
||||||
document.getElementById('btn-vue-chaine').classList.toggle('on', v === 'chaine');
|
document.getElementById('btn-vue-chaine').classList.toggle('on', v === 'chaine');
|
||||||
|
document.getElementById('btn-vue-bases').classList.toggle('on', v === 'bases');
|
||||||
dessinerGrilles();
|
dessinerGrilles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function 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 === 'chaine') { dessinerArbre(); return; }
|
||||||
|
if (vuePrincipale === 'bases') { dessinerBases(); return; }
|
||||||
const cible = document.getElementById('grilles');
|
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; }
|
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));
|
const visibles = hotes.map((h, i) => [h, i]).filter(([h]) => correspond(h));
|
||||||
|
|
@ -783,6 +815,83 @@ HTML = r"""<!doctype html>
|
||||||
}).join('');
|
}).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() {
|
function dessinerDependances() {
|
||||||
const cible = document.getElementById('dependances');
|
const cible = document.getElementById('dependances');
|
||||||
const noms = Object.keys(dependances).sort();
|
const noms = Object.keys(dependances).sort();
|
||||||
|
|
@ -1202,6 +1311,10 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
||||||
ecrire_yaml(self.inventaire, nouveau)
|
ecrire_yaml(self.inventaire, nouveau)
|
||||||
VERIF_OK.clear()
|
VERIF_OK.clear()
|
||||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
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":
|
elif chemin == "/api/verifier":
|
||||||
self.executer_flux(str(donnees.get("hote", "")), "verifier", donnees.get("vault"))
|
self.executer_flux(str(donnees.get("hote", "")), "verifier", donnees.get("vault"))
|
||||||
elif chemin == "/api/deployer":
|
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))
|
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:
|
def charger_nomenclature(path: Path | None) -> dict:
|
||||||
"""Charge le registre de nomenclature (domaines, categories, adressage)."""
|
"""Charge le registre de nomenclature (domaines, categories, adressage)."""
|
||||||
if not path or not path.exists():
|
if not path or not path.exists():
|
||||||
|
|
|
||||||
Reference in a new issue