Faire de l'application une entite de premiere classe (BD multi-portee)
Une VM peut porter plusieurs applications ; c'est l'application, pas l'hote, a laquelle une base se lie via son DSN. - docs/applications.yml : nouveau registre application -> groupe, hote. - docs/bases-donnees.yml : champ 'portee' (application|groupe|hote, defaut groupe). Une application A (groupe G, hote H) recoit les bases ou (portee=application ET consommateur=A) OU (portee=groupe ET consommateur=G) OU (portee=hote ET consommateur=H). Plusieurs DSN par application. - inventory_rules.py : charger/valider_applications, applications_de_hote, bases_de_application, PORTEES_BD ; valider_bases croise portee=application avec le registre des applications. - CLI : scripts/applications.py + bases_donnees.py --portee ; cibles make applications / applications-verifier ; integre a inventaire-verifier. - GUI : vue Applications (CRUD), portee + consommateur dynamique dans Bases, vue Chaine par application (DSN resolus par hote). - filter_plugins/registres.py : meme regle de resolution exposee a Ansible (source unique). Playbooks web_dorsaux/web_frontaux iterent sur les applications de l'hote (scaffold ; deploiement applicatif s'y insere). Services en grappe (keycloak/icingadb/forgejo) restent en portee groupe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
92762d3c38
commit
abd6395a13
12 changed files with 460 additions and 40 deletions
|
|
@ -1,5 +1,11 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-06-23
|
||||
|
||||
### Ajouté
|
||||
- **Application = entité de première classe** (une VM peut porter plusieurs applications). Nouveau registre `docs/applications.yml` (`application → groupe, hote`). Les bases se lient via `consommateur` + **`portee`** (`application` | `groupe` | `hote`, défaut `groupe`) dans `docs/bases-donnees.yml` : une application `A` (groupe `G`, hôte `H`) reçoit les bases où `(portee=application ET consommateur=A) OU (portee=groupe ET consommateur=G) OU (portee=hote ET consommateur=H)` — plusieurs DSN par application. Résolution + validation partagées dans `inventory_rules.py` (`charger_applications`, `valider_applications`, `applications_de_hote`, `bases_de_application`, `PORTEES_BD` ; `valider_bases` croise désormais `portee=application` avec le registre des applications). CLI `scripts/applications.py` (`lister`/`verifier`/`ajouter`/`retirer`) + `scripts/bases_donnees.py --portee` ; cibles `make applications` / `applications-verifier` ; validation intégrée dans `make inventaire-verifier`. GUI : vue **Applications** (CRUD), sélecteur de **portée** + consommateur dynamique (groupe/hôte/application) dans la vue Bases, et **vue Chaîne par application** (DSN résolus par hôte). Filter plugin `filter_plugins/registres.py` exposant la **même** règle de résolution à Ansible (source unique) ; les playbooks `serveurs_web_dorsaux` / `serveurs_web_frontaux` itèrent désormais sur les applications de l'hôte et résolvent leurs bases (scaffold ; le déploiement applicatif réel s'y insère). Les services en grappe (keycloak/icingadb/forgejo) restent en `portee: groupe` (inchangés).
|
||||
- Vhosts `serveurs_nginx` **dérivés du registre `docs/domaines.yml`** (bloc `exposition`) : pour chaque exposition `web` ciblant cet edge, un vhost est généré (`expositions.conf.j2`) avec amont = `amont` explicite, sinon `http://<IP interne de la cible>:<port>` résolu depuis l'inventaire ; une exposition non résolvable (cible sans hôte actif, ou sans port) est listée mais non publiée. Variables `serveurs_nginx_groupe` (edge ciblé) et `serveurs_nginx_publier_expositions` (bascule). Les sites déclarés à la main continuent de coexister. Ajout du `port: 3000` à l'exposition `forge`.
|
||||
|
||||
## 2026-06-22
|
||||
|
||||
### Ajouté
|
||||
|
|
|
|||
9
Makefile
9
Makefile
|
|
@ -370,10 +370,11 @@ 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/applications.py verifier
|
||||
python3 scripts/bases_donnees.py verifier
|
||||
python3 scripts/domaines.py verifier
|
||||
|
||||
.PHONY: bases bases-verifier domaines domaines-verifier
|
||||
.PHONY: bases bases-verifier domaines domaines-verifier applications applications-verifier
|
||||
bases:
|
||||
python3 scripts/bases_donnees.py lister
|
||||
|
||||
|
|
@ -386,6 +387,12 @@ domaines:
|
|||
domaines-verifier:
|
||||
python3 scripts/domaines.py verifier
|
||||
|
||||
applications:
|
||||
python3 scripts/applications.py lister
|
||||
|
||||
applications-verifier:
|
||||
python3 scripts/applications.py verifier
|
||||
|
||||
inventaire-lister: ansible-runtime
|
||||
ansible-inventory -i $(FICHIER_INVENTAIRE) --list
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[defaults]
|
||||
inventory = inventories/lab/hosts.yml
|
||||
roles_path = roles
|
||||
filter_plugins = filter_plugins
|
||||
interpreter_python = auto_silent
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
|
|
|
|||
23
docs/applications.yml
Normal file
23
docs/applications.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
# Registre des applications Set-OPS — l'application est l'entite PIVOT.
|
||||
#
|
||||
# Une VM (hote) peut porter PLUSIEURS applications. L'application, pas l'hote,
|
||||
# est ce a quoi une base de donnees se lie (via son DSN). C'est aussi l'entite
|
||||
# que l'exposition DNS pourra cibler a terme.
|
||||
#
|
||||
# Chaque application :
|
||||
# <id> : identifiant unique (minuscule, court) ex. boutique
|
||||
# groupe : groupe operationnel / role dont elle releve ex. serveurs_web_dorsaux
|
||||
# hote : hote de l'inventaire ou elle s'execute ex. web-dorsal-01
|
||||
#
|
||||
# Une base se lie a une application via consommateur=<id> + portee=application
|
||||
# dans docs/bases-donnees.yml. Une application recoit aussi les bases de portee
|
||||
# 'groupe' de son groupe (bases partagees).
|
||||
|
||||
applications: {}
|
||||
# boutique:
|
||||
# groupe: serveurs_web_dorsaux
|
||||
# hote: web-dorsal-01
|
||||
# blog:
|
||||
# groupe: serveurs_web_dorsaux
|
||||
# hote: web-dorsal-01 # meme VM que boutique : 2 applications sur 1 hote
|
||||
|
|
@ -1,15 +1,24 @@
|
|||
---
|
||||
# Registre des bases de donnees applicatives Set-OPS.
|
||||
#
|
||||
# Trois niveaux :
|
||||
# Niveaux :
|
||||
# serveurs_bd : les serveurs de base de donnees (un ou plusieurs), nommes.
|
||||
# "groupe" relie un serveur nomme au groupe Ansible qui l'heberge.
|
||||
# bases_donnees: une base = un identifiant unique -> serveur, base, proprietaire, secret.
|
||||
# consommateur : le groupe applicatif qui UTILISE la base (le binding appli <-> base).
|
||||
# Une appli peut avoir plusieurs bases (meme consommateur, "usage" different).
|
||||
# consommateur : QUI utilise la base. Interprete selon "portee" :
|
||||
# portee=application : consommateur = id d'une application (docs/applications.yml)
|
||||
# portee=groupe : consommateur = groupe operationnel (base PARTAGEE)
|
||||
# portee=hote : consommateur = hote de l'inventaire
|
||||
# Defaut : groupe (retrocompatible).
|
||||
# usage : etiquette libre (principale, partagee, ...). Une appli peut avoir
|
||||
# plusieurs bases (portee/usage differents).
|
||||
#
|
||||
# La chaine de connexion se derive : <type>://<proprietaire>:<secret>@<hote>:<port>/<base>
|
||||
# C'est elle, plus le couple (consommateur, base), qui fait le lien appli <-> BD.
|
||||
# Une application A (groupe G, hote H) recoit les bases ou :
|
||||
# (portee=application ET consommateur=A) OU (portee=groupe ET consommateur=G)
|
||||
# OU (portee=hote ET consommateur=H). Plusieurs DSN par application sont possibles.
|
||||
#
|
||||
# La chaine de connexion (DSN) se derive : <type>://<proprietaire>:<secret>@<hote>:<port>/<base>
|
||||
# C'est le DSN qui lie la bonne instanciation d'application a sa base.
|
||||
#
|
||||
# "secret" nomme une variable Ansible Vault (jamais le mot de passe en clair).
|
||||
|
||||
|
|
@ -27,6 +36,7 @@ bases_donnees:
|
|||
proprietaire: keycloak
|
||||
secret: vault_bd_keycloak
|
||||
consommateur: serveurs_keycloak
|
||||
portee: groupe
|
||||
usage: principale
|
||||
icingadb:
|
||||
serveur: pg-principal
|
||||
|
|
@ -34,6 +44,7 @@ bases_donnees:
|
|||
proprietaire: icingadb
|
||||
secret: vault_bd_icingadb
|
||||
consommateur: serveurs_icinga
|
||||
portee: groupe
|
||||
usage: principale
|
||||
forgejo:
|
||||
serveur: pg-principal
|
||||
|
|
@ -41,4 +52,5 @@ bases_donnees:
|
|||
proprietaire: forgejo
|
||||
secret: vault_bd_forgejo
|
||||
consommateur: serveurs_forgejo
|
||||
portee: groupe
|
||||
usage: principale
|
||||
|
|
|
|||
32
filter_plugins/registres.py
Normal file
32
filter_plugins/registres.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Filtres Ansible exposant les regles de resolution des registres Set-OPS.
|
||||
|
||||
Reutilise scripts/inventory_rules.py : la regle de liaison application <-> base
|
||||
(et la derivation du DSN) vit une SEULE fois, en Python. Le CLI, le GUI (qui en
|
||||
est le miroir) et Ansible passent tous par la meme logique.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
|
||||
from inventory_rules import ( # noqa: E402
|
||||
applications_de_hote,
|
||||
bases_de_application,
|
||||
bases_du_groupe,
|
||||
chaine_connexion,
|
||||
)
|
||||
|
||||
|
||||
class FilterModule:
|
||||
"""Filtres Set-OPS pour les playbooks par application."""
|
||||
|
||||
def filters(self) -> dict:
|
||||
return {
|
||||
"bases_de_application": bases_de_application,
|
||||
"applications_de_hote": applications_de_hote,
|
||||
"bases_du_groupe": bases_du_groupe,
|
||||
"chaine_connexion": chaine_connexion,
|
||||
}
|
||||
|
|
@ -12,6 +12,32 @@
|
|||
fail_msg: "Ce playbook est prévu pour Debian."
|
||||
|
||||
tasks:
|
||||
- name: Indiquer que le rôle dorsal web reste à définir
|
||||
# Couche application : une VM dorsale peut porter PLUSIEURS applications.
|
||||
# On itere donc sur les applications mappees a cet hote (docs/applications.yml),
|
||||
# chacune resolvant ses bases via son DSN (regle partagee, filter plugin).
|
||||
- name: Charger les registres applications et bases
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ item }}"
|
||||
loop:
|
||||
- "{{ playbook_dir }}/../../docs/applications.yml"
|
||||
- "{{ playbook_dir }}/../../docs/bases-donnees.yml"
|
||||
|
||||
- name: Determiner les applications instanciees sur cet hote
|
||||
ansible.builtin.set_fact:
|
||||
applications_hote: "{{ {'applications': applications | default({})} | applications_de_hote(inventory_hostname) }}"
|
||||
registre_bd: "{{ {'serveurs_bd': serveurs_bd | default({}), 'bases_donnees': bases_donnees | default({})} }}"
|
||||
|
||||
- name: Afficher la chaine application -> bases (scaffold ; le deploiement reel s'insere ici)
|
||||
ansible.builtin.debug:
|
||||
msg: "Aucun rôle n'est encore associé au groupe serveurs_web_dorsaux (couche application web : API, traitement)."
|
||||
msg: >-
|
||||
{{ item.id }} [{{ item.groupe }}] ->
|
||||
{{ (registre_bd | bases_de_application(item.id, item.groupe, inventory_hostname))
|
||||
| map(attribute='cle') | list }}
|
||||
loop: "{{ applications_hote }}"
|
||||
loop_control:
|
||||
label: "{{ item.id }}"
|
||||
|
||||
- name: Avertir si l'hote ne porte aucune application
|
||||
ansible.builtin.debug:
|
||||
msg: "Aucune application declaree sur cet hote (docs/applications.yml). Couche dorsale : API, traitement."
|
||||
when: applications_hote | length == 0
|
||||
|
|
|
|||
|
|
@ -12,6 +12,32 @@
|
|||
fail_msg: "Ce playbook est prévu pour Debian."
|
||||
|
||||
tasks:
|
||||
- name: Indiquer que le rôle frontal web reste à définir
|
||||
# Couche presentation : une VM frontale peut porter PLUSIEURS applications.
|
||||
# On itere sur les applications mappees a cet hote (docs/applications.yml),
|
||||
# chacune resolvant ses bases via son DSN (regle partagee, filter plugin).
|
||||
- name: Charger les registres applications et bases
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ item }}"
|
||||
loop:
|
||||
- "{{ playbook_dir }}/../../docs/applications.yml"
|
||||
- "{{ playbook_dir }}/../../docs/bases-donnees.yml"
|
||||
|
||||
- name: Determiner les applications instanciees sur cet hote
|
||||
ansible.builtin.set_fact:
|
||||
applications_hote: "{{ {'applications': applications | default({})} | applications_de_hote(inventory_hostname) }}"
|
||||
registre_bd: "{{ {'serveurs_bd': serveurs_bd | default({}), 'bases_donnees': bases_donnees | default({})} }}"
|
||||
|
||||
- name: Afficher la chaine application -> bases (scaffold ; le deploiement reel s'insere ici)
|
||||
ansible.builtin.debug:
|
||||
msg: "Aucun rôle n'est encore associé au groupe serveurs_web_frontaux (couche présentation web, derrière serveurs_nginx)."
|
||||
msg: >-
|
||||
{{ item.id }} [{{ item.groupe }}] ->
|
||||
{{ (registre_bd | bases_de_application(item.id, item.groupe, inventory_hostname))
|
||||
| map(attribute='cle') | list }}
|
||||
loop: "{{ applications_hote }}"
|
||||
loop_control:
|
||||
label: "{{ item.id }}"
|
||||
|
||||
- name: Avertir si l'hote ne porte aucune application
|
||||
ansible.builtin.debug:
|
||||
msg: "Aucune application declaree sur cet hote (docs/applications.yml). Couche presentation, derriere serveurs_nginx."
|
||||
when: applications_hote | length == 0
|
||||
|
|
|
|||
91
scripts/applications.py
Normal file
91
scripts/applications.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Lit et valide le registre des applications Set-OPS (docs/applications.yml)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import (
|
||||
bases_de_application,
|
||||
chaine_connexion,
|
||||
charger_applications,
|
||||
charger_bases_donnees,
|
||||
valider_applications,
|
||||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
FICHIER = RACINE / "docs/applications.yml"
|
||||
FICHIER_BASES = RACINE / "docs/bases-donnees.yml"
|
||||
|
||||
|
||||
def ecrire(registre: dict) -> None:
|
||||
entete = (
|
||||
"# Registre des applications Set-OPS (application = entite pivot).\n"
|
||||
"# Edite par make inventaire-ui ou scripts/applications.py.\n"
|
||||
"# Une VM peut porter plusieurs applications ; une base se lie a une application.\n"
|
||||
"---\n"
|
||||
)
|
||||
with FICHIER.open("w", encoding="utf-8") as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump({"applications": registre.get("applications", {}) or {}},
|
||||
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
||||
|
||||
def lister(registre: dict, bases: dict) -> None:
|
||||
apps = registre.get("applications", {})
|
||||
if not apps:
|
||||
print("Aucune application declaree.")
|
||||
return
|
||||
for nom, app in apps.items():
|
||||
print(f"{nom} [groupe {app.get('groupe', '?')}, hote {app.get('hote', '?')}]")
|
||||
for entree in bases_de_application(bases, application=nom,
|
||||
groupe=app.get("groupe"), hote=app.get("hote")):
|
||||
base = entree["base"]
|
||||
portee = base.get("portee", "groupe")
|
||||
print(f" DSN ({portee}/{base.get('usage', '-')}): {chaine_connexion(base, entree['serveur'])}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Registre des applications Set-OPS.")
|
||||
sub = parser.add_subparsers(dest="commande", required=True)
|
||||
sub.add_parser("lister", help="Affiche les applications et leurs DSN resolus.")
|
||||
sub.add_parser("verifier", help="Valide la coherence du registre.")
|
||||
|
||||
pa = sub.add_parser("ajouter", help="Ajoute ou met a jour une application.")
|
||||
pa.add_argument("--id", required=True)
|
||||
pa.add_argument("--groupe", required=True)
|
||||
pa.add_argument("--hote", required=True)
|
||||
|
||||
pr = sub.add_parser("retirer", help="Retire une application.")
|
||||
pr.add_argument("--id", required=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
registre = charger_applications(FICHIER)
|
||||
if args.commande == "lister":
|
||||
lister(registre, charger_bases_donnees(FICHIER_BASES))
|
||||
elif args.commande == "verifier":
|
||||
valider_applications(registre)
|
||||
print("Registre des applications valide.")
|
||||
elif args.commande == "ajouter":
|
||||
registre["applications"][args.id] = {"groupe": args.groupe, "hote": args.hote}
|
||||
valider_applications(registre)
|
||||
ecrire(registre)
|
||||
print(f"Application '{args.id}' enregistree.")
|
||||
elif args.commande == "retirer":
|
||||
registre["applications"].pop(args.id, None)
|
||||
valider_applications(registre)
|
||||
ecrire(registre)
|
||||
print(f"Application '{args.id}' retiree.")
|
||||
except Exception as exc:
|
||||
print(f"erreur: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -9,17 +9,24 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import chaine_connexion, charger_bases_donnees, valider_bases
|
||||
from inventory_rules import (
|
||||
chaine_connexion,
|
||||
charger_applications,
|
||||
charger_bases_donnees,
|
||||
valider_bases,
|
||||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
FICHIER = RACINE / "docs/bases-donnees.yml"
|
||||
FICHIER_APPLICATIONS = RACINE / "docs/applications.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"
|
||||
"# 'portee' (application|groupe|hote) interprete 'consommateur' ; defaut groupe.\n"
|
||||
"# 'secret' nomme une variable Ansible Vault (jamais le mot de passe).\n"
|
||||
"---\n"
|
||||
)
|
||||
contenu = {
|
||||
|
|
@ -40,8 +47,9 @@ def lister(registre: dict) -> None:
|
|||
for cle, base in registre.get("bases_donnees", {}).items():
|
||||
srv = serveurs.get(base.get("serveur"), {})
|
||||
conso = base.get("consommateur", "?")
|
||||
portee = base.get("portee", "groupe")
|
||||
usage = base.get("usage", "-")
|
||||
print(f" {cle} <- {conso} ({usage}): {chaine_connexion(base, srv)} [secret {base.get('secret')}]")
|
||||
print(f" {cle} <- {conso} [{portee}/{usage}]: {chaine_connexion(base, srv)} [secret {base.get('secret')}]")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
|
@ -63,7 +71,9 @@ def main() -> int:
|
|||
pb.add_argument("--base", required=True)
|
||||
pb.add_argument("--proprietaire", required=True)
|
||||
pb.add_argument("--secret", required=True)
|
||||
pb.add_argument("--consommateur", required=True, help="Groupe applicatif qui utilise la base.")
|
||||
pb.add_argument("--consommateur", required=True,
|
||||
help="Qui utilise la base : groupe, hote ou id d'application (selon --portee).")
|
||||
pb.add_argument("--portee", default="groupe", choices=["groupe", "hote", "application"])
|
||||
pb.add_argument("--usage", default="principale")
|
||||
|
||||
pr = sub.add_parser("retirer-base", help="Retire une base applicative.")
|
||||
|
|
@ -74,36 +84,37 @@ def main() -> int:
|
|||
args = parser.parse_args()
|
||||
try:
|
||||
registre = charger_bases_donnees(FICHIER)
|
||||
applications = charger_applications(FICHIER_APPLICATIONS)
|
||||
if args.commande == "lister":
|
||||
lister(registre)
|
||||
elif args.commande == "verifier":
|
||||
valider_bases(registre)
|
||||
valider_bases(registre, applications)
|
||||
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)
|
||||
valider_bases(registre, applications)
|
||||
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,
|
||||
"consommateur": args.consommateur, "usage": args.usage,
|
||||
"consommateur": args.consommateur, "portee": args.portee, "usage": args.usage,
|
||||
}
|
||||
valider_bases(registre)
|
||||
valider_bases(registre, applications)
|
||||
ecrire(registre)
|
||||
print(f"Base '{args.cle}' enregistree.")
|
||||
elif args.commande == "retirer-base":
|
||||
registre["bases_donnees"].pop(args.cle, None)
|
||||
valider_bases(registre)
|
||||
valider_bases(registre, applications)
|
||||
ecrire(registre)
|
||||
print(f"Base '{args.cle}' retiree.")
|
||||
elif args.commande == "retirer-serveur":
|
||||
registre["serveurs_bd"].pop(args.nom, None)
|
||||
valider_bases(registre)
|
||||
valider_bases(registre, applications)
|
||||
ecrire(registre)
|
||||
print(f"Serveur de BD '{args.nom}' retire.")
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@ from inventory_rules import (
|
|||
GROUPE_HOTES_PLANIFIES,
|
||||
GROUPES_ETAT_HOTE,
|
||||
chaine_connexion,
|
||||
charger_applications,
|
||||
charger_bases_donnees,
|
||||
charger_dependances,
|
||||
charger_nomenclature,
|
||||
est_groupe_operationnel,
|
||||
groupes_operationnels_connus,
|
||||
valider_applications,
|
||||
valider_bases,
|
||||
)
|
||||
|
||||
|
|
@ -37,6 +39,7 @@ 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"
|
||||
FICHIER_APPLICATIONS = RACINE / "docs/applications.yml"
|
||||
|
||||
# Garde-fous des executions (verifier / deployer) depuis l'interface.
|
||||
JETON = secrets.token_urlsafe(18)
|
||||
|
|
@ -175,6 +178,7 @@ def inventaire_api(path: Path) -> dict:
|
|||
"nomenclature": charger_nomenclature(FICHIER_NOMENCLATURE),
|
||||
"chaine": roles_des_groupes(),
|
||||
"bases": charger_bases_donnees(FICHIER_BASES),
|
||||
"applications": charger_applications(FICHIER_APPLICATIONS),
|
||||
"hotes": liste_hotes(data),
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +187,8 @@ 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"
|
||||
"# 'portee' (application|groupe|hote) interprete 'consommateur' ; defaut groupe.\n"
|
||||
"# 'secret' nomme une variable Ansible Vault (jamais le mot de passe).\n"
|
||||
"---\n"
|
||||
)
|
||||
contenu = {
|
||||
|
|
@ -195,6 +200,19 @@ def ecrire_bases(path: Path, registre: dict) -> None:
|
|||
yaml.safe_dump(contenu, fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
||||
|
||||
def ecrire_applications(path: Path, registre: dict) -> None:
|
||||
entete = (
|
||||
"# Registre des applications Set-OPS (application = entite pivot).\n"
|
||||
"# Edite par make inventaire-ui ou scripts/applications.py.\n"
|
||||
"# Une VM peut porter plusieurs applications ; une base se lie a une application.\n"
|
||||
"---\n"
|
||||
)
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump({"applications": registre.get("applications", {}) or {}},
|
||||
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:
|
||||
|
|
@ -533,6 +551,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-applications" onclick="setVue('applications')">Applications</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>
|
||||
|
|
@ -588,6 +607,8 @@ HTML = r"""<!doctype html>
|
|||
let bdServeurs = [];
|
||||
let bdApplis = [];
|
||||
let basesModifie = false;
|
||||
let applications = [];
|
||||
let applicationsModifie = false;
|
||||
let selection = -1;
|
||||
let selectionNom = null;
|
||||
let ongletActif = 'reseau';
|
||||
|
|
@ -631,6 +652,7 @@ HTML = r"""<!doctype html>
|
|||
nomenclature = data.nomenclature || {};
|
||||
chaine = data.chaine || {};
|
||||
chargerBases(data);
|
||||
chargerApplications(data);
|
||||
hotes = data.hotes;
|
||||
estProduction = !!data.production;
|
||||
modifie = false; verifie = {};
|
||||
|
|
@ -767,6 +789,7 @@ 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-applications').classList.toggle('on', v === 'applications');
|
||||
document.getElementById('btn-vue-bases').classList.toggle('on', v === 'bases');
|
||||
dessinerGrilles();
|
||||
}
|
||||
|
|
@ -774,6 +797,7 @@ HTML = r"""<!doctype html>
|
|||
function dessinerGrilles() {
|
||||
document.getElementById('chips').style.display = vuePrincipale === 'cartes' ? '' : 'none';
|
||||
if (vuePrincipale === 'chaine') { dessinerArbre(); return; }
|
||||
if (vuePrincipale === 'applications') { dessinerApplications(); 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; }
|
||||
|
|
@ -787,23 +811,47 @@ HTML = r"""<!doctype html>
|
|||
</div>`).join('');
|
||||
}
|
||||
|
||||
function basesDeApp(app, hoteNom) {
|
||||
return bdApplis.filter(a => {
|
||||
const p = a.portee || 'groupe';
|
||||
return (p === 'application' && a.consommateur === app.id)
|
||||
|| (p === 'groupe' && a.consommateur === app.groupe)
|
||||
|| (p === 'hote' && a.consommateur === hoteNom);
|
||||
});
|
||||
}
|
||||
|
||||
function lignesDsn(liste, etiquette) {
|
||||
return liste.length ? `<div class="ch-bases">${liste.map(a => `
|
||||
<div class="ch-base"><span class="badge-bd">${echapper(etiquette(a))}</span> <span class="mono">${echapper(dsnBase(a))}</span></div>`).join('')}</div>` : '';
|
||||
}
|
||||
|
||||
function renduChaine(hote) {
|
||||
const grps = hote.groupes || [];
|
||||
if (!grps.length) return '<div class="vide" style="padding:18px">Aucun groupe opérationnel.</div>';
|
||||
return grps.map(g => {
|
||||
const apps = applications.filter(a => a.hote === hote.nom);
|
||||
if (!grps.length && !apps.length) return '<div class="vide" style="padding:18px">Aucun groupe opérationnel ni application.</div>';
|
||||
const grpHtml = grps.map(g => {
|
||||
const c = chaine[g] || {roles: [], stub: true};
|
||||
const corps = (c.roles && c.roles.length)
|
||||
? `<div class="ch-roles">${c.roles.map(r => `<span class="tag">${echapper(r)}</span>`).join('')}</div>`
|
||||
: '<div class="ch-stub">stub — aucun rôle rattaché</div>';
|
||||
const bg = bdApplis.filter(a => a.consommateur === g);
|
||||
const basesHtml = bg.length ? `<div class="ch-bases">${bg.map(a => `
|
||||
<div class="ch-base"><span class="badge-bd">BD ${echapper(a.usage || '')}</span> <span class="mono">${echapper(dsnBase(a))}</span></div>`).join('')}</div>` : '';
|
||||
const bg = bdApplis.filter(a => (a.portee || 'groupe') === 'groupe' && a.consommateur === g);
|
||||
return `<div class="ch-grp">
|
||||
<div class="ch-grp-tete"><span>${echapper(g)}</span><span class="ch-fleche">→</span><span class="mono">${echapper(g)}.yml</span></div>
|
||||
${corps}
|
||||
${basesHtml}
|
||||
${lignesDsn(bg, a => 'BD partagée ' + (a.usage || ''))}
|
||||
</div>`;
|
||||
}).join('');
|
||||
const appHtml = apps.map(app => {
|
||||
const dsns = basesDeApp(app, hote.nom);
|
||||
const bd = dsns.length ? lignesDsn(dsns, a => (a.portee || 'groupe') + ' · ' + (a.usage || ''))
|
||||
: '<div class="ch-stub">aucune base liée</div>';
|
||||
return `<div class="ch-grp">
|
||||
<div class="ch-grp-tete"><span class="badge-bd">app</span><span>${echapper(app.id)}</span><span class="ch-fleche">→</span><span class="mono">${echapper(app.groupe)}</span></div>
|
||||
${bd}
|
||||
</div>`;
|
||||
}).join('');
|
||||
const appSection = apps.length ? `<div class="section-tete" style="margin:6px 2px 2px">Applications sur cet hôte <span class="compte">${apps.length}</span></div>${appHtml}` : '';
|
||||
return grpHtml + appSection;
|
||||
}
|
||||
|
||||
function dessinerArbre() {
|
||||
|
|
@ -828,10 +876,16 @@ HTML = r"""<!doctype html>
|
|||
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 || '',
|
||||
consommateur: x.consommateur || '', usage: x.usage || 'principale'}));
|
||||
consommateur: x.consommateur || '', portee: x.portee || 'groupe', usage: x.usage || 'principale'}));
|
||||
basesModifie = false;
|
||||
}
|
||||
|
||||
function chargerApplications(data) {
|
||||
const a = (data.applications || {}).applications || {};
|
||||
applications = Object.entries(a).map(([id, x]) => ({id, groupe: x.groupe || '', hote: x.hote || ''}));
|
||||
applicationsModifie = false;
|
||||
}
|
||||
|
||||
function dsnBase(a) {
|
||||
const s = bdServeurs.find(x => x.nom === a.serveur);
|
||||
if (!s) return '(serveur inconnu : ' + echapper(a.serveur || '—') + ')';
|
||||
|
|
@ -844,10 +898,10 @@ HTML = r"""<!doctype html>
|
|||
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 definirBaseBd(i, champ, v) { bdApplis[i][champ] = v; marquerBasesModifie(); if (champ === 'serveur' || champ === 'portee') 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: '', consommateur: '', usage: 'principale'}); marquerBasesModifie(); dessinerBases(); }
|
||||
function ajouterBaseBd() { bdApplis.push({cle: '', serveur: (bdServeurs[0] || {}).nom || '', base: '', proprietaire: '', secret: '', consommateur: '', portee: 'groupe', usage: 'principale'}); marquerBasesModifie(); dessinerBases(); }
|
||||
function retirerBaseBd(i) { bdApplis.splice(i, 1); marquerBasesModifie(); dessinerBases(); }
|
||||
|
||||
function dessinerBases() {
|
||||
|
|
@ -861,11 +915,16 @@ HTML = r"""<!doctype html>
|
|||
<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 optConso = (sel) => '<option value="">—</option>' + groupes.map(g => `<option value="${echapper(g)}" ${g === sel ? 'selected' : ''}>${echapper(g)}</option>`).join('');
|
||||
const sourceConso = (portee) => portee === 'hote' ? hotes.map(h => h.nom).filter(Boolean)
|
||||
: portee === 'application' ? applications.map(a => a.id)
|
||||
: groupes;
|
||||
const optConso = (portee, sel) => '<option value="">—</option>' + sourceConso(portee).map(v => `<option value="${echapper(v)}" ${v === sel ? 'selected' : ''}>${echapper(v)}</option>`).join('');
|
||||
const optPortee = (sel) => ['groupe', 'application', 'hote'].map(p => `<option value="${p}" ${p === sel ? 'selected' : ''}>${p}</option>`).join('');
|
||||
const applis = bdApplis.length ? bdApplis.map((a, i) => `
|
||||
<div class="base-ligne">
|
||||
<label class="champ"><span>Identifiant</span><input value="${echapper(a.cle)}" oninput="definirBaseBd(${i}, 'cle', this.value)"></label>
|
||||
<label class="champ"><span>Consommateur</span><select onchange="definirBaseBd(${i}, 'consommateur', this.value)">${optConso(a.consommateur)}</select></label>
|
||||
<label class="champ"><span>Portée</span><select onchange="definirBaseBd(${i}, 'portee', this.value)">${optPortee(a.portee || 'groupe')}</select></label>
|
||||
<label class="champ"><span>Consommateur</span><select onchange="definirBaseBd(${i}, 'consommateur', this.value)">${optConso(a.portee || 'groupe', a.consommateur)}</select></label>
|
||||
<label class="champ"><span>Usage</span><input value="${echapper(a.usage)}" placeholder="principale" oninput="definirBaseBd(${i}, 'usage', 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('')}
|
||||
|
|
@ -874,7 +933,7 @@ HTML = r"""<!doctype html>
|
|||
<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(a.consommateur || '—')} · ${echapper(dsnBase(a))}</div>
|
||||
<div class="dsn mono">${echapper(a.portee || 'groupe')} · ${echapper(a.consommateur || '—')} · ${echapper(dsnBase(a))}</div>
|
||||
</div>`).join('') : '<div class="vide" style="padding:14px">Aucune base applicative.</div>';
|
||||
cible.innerHTML = `
|
||||
<div class="section-grille">
|
||||
|
|
@ -892,7 +951,7 @@ HTML = r"""<!doctype html>
|
|||
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, consommateur: a.consommateur, usage: a.usage || 'principale'}; });
|
||||
bdApplis.forEach(a => { if (String(a.cle).trim()) bd[a.cle] = {serveur: a.serveur, base: a.base, proprietaire: a.proprietaire, secret: a.secret, consommateur: a.consommateur, portee: a.portee || 'groupe', usage: a.usage || 'principale'}; });
|
||||
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})
|
||||
});
|
||||
|
|
@ -903,6 +962,51 @@ HTML = r"""<!doctype html>
|
|||
message('Bases de données sauvegardées.', 'ok');
|
||||
}
|
||||
|
||||
function marquerApplicationsModifie() {
|
||||
applicationsModifie = true;
|
||||
const b = document.getElementById('btn-sauver-applications');
|
||||
if (b) { b.classList.add('attention'); b.textContent = 'Sauvegarder les applications •'; }
|
||||
}
|
||||
function definirApplication(i, champ, v) { applications[i][champ] = v; marquerApplicationsModifie(); if (champ === 'hote' || champ === 'groupe') dessinerApplications(); }
|
||||
function ajouterApplication() { applications.push({id: '', groupe: (groupes[0] || ''), hote: ''}); marquerApplicationsModifie(); dessinerApplications(); }
|
||||
function retirerApplication(i) { applications.splice(i, 1); marquerApplicationsModifie(); dessinerApplications(); }
|
||||
|
||||
function dessinerApplications() {
|
||||
const cible = document.getElementById('grilles');
|
||||
const optGroupe = (sel) => groupes.map(g => `<option value="${echapper(g)}" ${g === sel ? 'selected' : ''}>${echapper(g)}</option>`).join('');
|
||||
const optHote = (sel) => '<option value="">—</option>' + hotes.map(h => h.nom).filter(Boolean).map(n => `<option value="${echapper(n)}" ${n === sel ? 'selected' : ''}>${echapper(n)}</option>`).join('');
|
||||
const lignes = applications.length ? applications.map((a, i) => {
|
||||
const dsns = basesDeApp(a, a.hote);
|
||||
const resume = dsns.length ? dsns.map(b => `<span class="mono">${echapper((b.portee || 'groupe'))}:${echapper(dsnBase(b))}</span>`).join('<br>') : '<span class="ch-stub">aucune base liée</span>';
|
||||
return `<div class="base-ligne">
|
||||
<label class="champ"><span>Identifiant</span><input value="${echapper(a.id)}" placeholder="boutique" oninput="definirApplication(${i}, 'id', this.value)"></label>
|
||||
<label class="champ"><span>Groupe</span><select onchange="definirApplication(${i}, 'groupe', this.value)">${optGroupe(a.groupe)}</select></label>
|
||||
<label class="champ"><span>Hôte</span><select onchange="definirApplication(${i}, 'hote', this.value)">${optHote(a.hote)}</select></label>
|
||||
<button type="button" class="danger" title="Retirer" onclick="retirerApplication(${i})">✕</button>
|
||||
<div class="dsn">${resume}</div>
|
||||
</div>`;
|
||||
}).join('') : '<div class="vide" style="padding:14px">Aucune application. Une VM peut en porter plusieurs.</div>';
|
||||
cible.innerHTML = `
|
||||
<div class="section-grille">
|
||||
<div class="section-tete">Applications <span class="compte">${applications.length}</span><button type="button" class="fantome" style="margin-left:auto" onclick="ajouterApplication()">+ Application</button></div>
|
||||
<div class="bases-liste">${lignes}</div>
|
||||
</div>
|
||||
<div style="margin:4px 2px 20px"><button type="button" class="primaire ${applicationsModifie ? 'attention' : ''}" id="btn-sauver-applications" onclick="sauvegarderApplications()">Sauvegarder les applications${applicationsModifie ? ' •' : ''}</button></div>`;
|
||||
}
|
||||
|
||||
async function sauvegarderApplications() {
|
||||
const apps = {};
|
||||
applications.forEach(a => { if (String(a.id).trim()) apps[a.id] = {groupe: a.groupe, hote: a.hote}; });
|
||||
const rep = await fetch('/api/applications', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json', 'X-Jeton': JETON}, body: JSON.stringify({applications: apps})
|
||||
});
|
||||
const data = await rep.json();
|
||||
if (!rep.ok) { message(data.erreur || 'Sauvegarde refusée.', 'erreur'); return; }
|
||||
chargerApplications(data);
|
||||
dessinerApplications();
|
||||
message('Applications sauvegardées.', 'ok');
|
||||
}
|
||||
|
||||
function dessinerDependances() {
|
||||
const cible = document.getElementById('dependances');
|
||||
const noms = Object.keys(dependances).sort();
|
||||
|
|
@ -1323,9 +1427,13 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
VERIF_OK.clear()
|
||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
||||
elif chemin == "/api/bases":
|
||||
valider_bases(donnees)
|
||||
valider_bases(donnees, charger_applications(FICHIER_APPLICATIONS))
|
||||
ecrire_bases(FICHIER_BASES, donnees)
|
||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
||||
elif chemin == "/api/applications":
|
||||
valider_applications(donnees)
|
||||
ecrire_applications(FICHIER_APPLICATIONS, 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":
|
||||
|
|
|
|||
|
|
@ -37,10 +37,19 @@ def charger_bases_donnees(path: Path | None) -> dict:
|
|||
return data
|
||||
|
||||
|
||||
def valider_bases(registre: dict) -> None:
|
||||
"""Valide la coherence du registre des bases de donnees."""
|
||||
PORTEES_BD = {"groupe", "hote", "application"}
|
||||
|
||||
|
||||
def valider_bases(registre: dict, applications: dict | None = None) -> None:
|
||||
"""Valide la coherence du registre des bases de donnees.
|
||||
|
||||
Si `applications` (le registre des applications) est fourni, les bases de
|
||||
portee 'application' sont verifiees : leur consommateur doit etre une
|
||||
application declaree.
|
||||
"""
|
||||
serveurs = registre.get("serveurs_bd") or {}
|
||||
bases = registre.get("bases_donnees") or {}
|
||||
apps = (applications or {}).get("applications") 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():
|
||||
|
|
@ -58,22 +67,90 @@ def valider_bases(registre: dict) -> None:
|
|||
raise ValueError(f"Base '{cle}': champ '{champ}' requis.")
|
||||
if base["serveur"] not in serveurs:
|
||||
raise ValueError(f"Base '{cle}': serveur '{base['serveur']}' inconnu.")
|
||||
portee = str(base.get("portee", "groupe")).strip()
|
||||
if portee not in PORTEES_BD:
|
||||
attendu = ", ".join(sorted(PORTEES_BD))
|
||||
raise ValueError(f"Base '{cle}': portee '{portee}' inconnue (attendu: {attendu}).")
|
||||
if portee == "groupe" and not est_groupe_operationnel(base["consommateur"]):
|
||||
raise ValueError(f"Base '{cle}': portee groupe mais consommateur '{base['consommateur']}' non operationnel.")
|
||||
if portee == "application" and apps and base["consommateur"] not in apps:
|
||||
raise ValueError(f"Base '{cle}': application consommatrice '{base['consommateur']}' inconnue (docs/applications.yml).")
|
||||
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 _base_resolue(cle: str, base: dict, serveurs: dict) -> dict:
|
||||
return {"cle": cle, "base": base, "serveur": serveurs.get(base.get("serveur"), {})}
|
||||
|
||||
|
||||
def bases_du_groupe(registre: dict, groupe: str) -> list[dict]:
|
||||
"""Bases consommees par un groupe applicatif, avec leur serveur resolu."""
|
||||
"""Bases de portee 'groupe' consommees par un groupe applicatif."""
|
||||
serveurs = registre.get("serveurs_bd") or {}
|
||||
resultat = []
|
||||
for cle, base in (registre.get("bases_donnees") or {}).items():
|
||||
if base.get("consommateur") == groupe:
|
||||
resultat.append({"cle": cle, "base": base, "serveur": serveurs.get(base.get("serveur"), {})})
|
||||
if str(base.get("portee", "groupe")) == "groupe" and base.get("consommateur") == groupe:
|
||||
resultat.append(_base_resolue(cle, base, serveurs))
|
||||
return resultat
|
||||
|
||||
|
||||
def bases_de_application(registre: dict, application: str | None = None,
|
||||
groupe: str | None = None, hote: str | None = None) -> list[dict]:
|
||||
"""Bases vues par une application instanciee, toutes portees confondues.
|
||||
|
||||
Une application `A` (du groupe `G`, sur l'hote `H`) recoit les bases ou :
|
||||
(portee=application ET consommateur=A) OU (portee=groupe ET consommateur=G)
|
||||
OU (portee=hote ET consommateur=H).
|
||||
"""
|
||||
serveurs = registre.get("serveurs_bd") or {}
|
||||
resultat = []
|
||||
for cle, base in (registre.get("bases_donnees") or {}).items():
|
||||
portee = str(base.get("portee", "groupe"))
|
||||
conso = base.get("consommateur")
|
||||
if ((portee == "application" and application is not None and conso == application)
|
||||
or (portee == "groupe" and groupe is not None and conso == groupe)
|
||||
or (portee == "hote" and hote is not None and conso == hote)):
|
||||
resultat.append(_base_resolue(cle, base, serveurs))
|
||||
return resultat
|
||||
|
||||
|
||||
def charger_applications(path: Path | None) -> dict:
|
||||
"""Charge le registre des applications (applications -> groupe, hote)."""
|
||||
if not path or not path.exists():
|
||||
return {"applications": {}}
|
||||
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("applications", {})
|
||||
return data
|
||||
|
||||
|
||||
def valider_applications(registre: dict) -> None:
|
||||
"""Valide la coherence du registre des applications."""
|
||||
apps = registre.get("applications") or {}
|
||||
if not isinstance(apps, dict):
|
||||
raise ValueError("applications doit etre une table.")
|
||||
for nom, app in apps.items():
|
||||
if not isinstance(app, dict):
|
||||
raise ValueError(f"Application '{nom}': table attendue.")
|
||||
for champ in ("groupe", "hote"):
|
||||
if not str(app.get(champ, "")).strip():
|
||||
raise ValueError(f"Application '{nom}': champ '{champ}' requis.")
|
||||
if not est_groupe_operationnel(app["groupe"]):
|
||||
raise ValueError(f"Application '{nom}': groupe '{app['groupe']}' non operationnel.")
|
||||
|
||||
|
||||
def applications_de_hote(registre: dict, hote: str) -> list[dict]:
|
||||
"""Applications instanciees sur un hote donne (plusieurs possibles)."""
|
||||
return [
|
||||
{"id": nom, **app}
|
||||
for nom, app in (registre.get("applications") or {}).items()
|
||||
if app.get("hote") == hote
|
||||
]
|
||||
|
||||
|
||||
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")
|
||||
|
|
|
|||
Reference in a new issue