Rendre explicites les bindings appli<->BD (consommateur, multi-BD)
Modele: - Chaque base porte un "consommateur" (le groupe applicatif qui l'utilise) et un "usage" (principale/cache...). Une application peut avoir plusieurs bases. - Le binding appli<->base n'est plus une convention de nom mais un champ gere. Regles partagees (inventory_rules): consommateur requis a la validation; helper bases_du_groupe(registre, groupe). GUI (make inventaire-ui): - Vue "Bases": menu deroulant Consommateur + champ Usage par base -> on fait les connexions ici; plusieurs bases par appli supportees. - Vue "Chaine" detaillee: chaque groupe affiche ses roles ET ses bases avec leur chaine de connexion (appli -> base -> serveur de BD -> hote). CLI (bases_donnees.py): ajouter-base --consommateur --usage; lister affiche "base <- consommateur (usage): DSN". Roles serveurs_keycloak / serveurs_forgejo / serveurs_icinga: resolution de leur base par consommateur (groupe), plus de cle codee en dur. Valide: ansible-lint (roles), node --check (JS), round-trips GUI et CLI, multi-BD verifie (serveurs_web_dorsaux -> 2 bases), make inventaire-verifier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
25d0c73275
commit
33b1933a6d
11 changed files with 75 additions and 37 deletions
|
|
@ -47,6 +47,7 @@
|
|||
- variables de production DNS et runbook `docs/dns-interne.md`.
|
||||
|
||||
### Modifié
|
||||
- Bindings appli↔BD explicites et **plusieurs bases par application** : chaque base porte un `consommateur` (le groupe applicatif qui l'utilise) et un `usage` (principale/cache…). La GUI (vue « Bases » : menu déroulant **Consommateur** + champ **Usage**) et la CLI (`ajouter-base --consommateur --usage`) gèrent le lien ; la vue **Chaîne devient détaillée** (chaque groupe affiche ses rôles ET ses bases avec leur chaîne de connexion). Rôles `serveurs_keycloak`/`serveurs_forgejo`/`serveurs_icinga` migrés pour résoudre leur base par consommateur (plus de clé codée en dur).
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -1,24 +1,17 @@
|
|||
---
|
||||
# Registre des bases de donnees applicatives Set-OPS.
|
||||
#
|
||||
# Patron : 1 appli -> 1 base nommee -> 1 compte proprietaire -> 1 chaine de connexion.
|
||||
#
|
||||
# Deux niveaux :
|
||||
# Trois 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.
|
||||
# "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).
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Deux consommateurs : serveurs_postgresql (cree base + compte) et le role applicatif
|
||||
# (construit sa connexion). Secret partage, source unique.
|
||||
# "secret" nomme une variable Ansible Vault (jamais le mot de passe en clair).
|
||||
|
||||
serveurs_bd:
|
||||
pg-principal:
|
||||
|
|
@ -33,13 +26,19 @@ bases_donnees:
|
|||
base: keycloak
|
||||
proprietaire: keycloak
|
||||
secret: vault_bd_keycloak
|
||||
consommateur: serveurs_keycloak
|
||||
usage: principale
|
||||
icingadb:
|
||||
serveur: pg-principal
|
||||
base: icingadb
|
||||
proprietaire: icingadb
|
||||
secret: vault_bd_icingadb
|
||||
consommateur: serveurs_icinga
|
||||
usage: principale
|
||||
forgejo:
|
||||
serveur: pg-principal
|
||||
base: forgejo
|
||||
proprietaire: forgejo
|
||||
secret: vault_bd_forgejo
|
||||
consommateur: serveurs_forgejo
|
||||
usage: principale
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ serveurs_forgejo_service: "forgejo"
|
|||
serveurs_forgejo_hostname: "forge.chezlepro.internal"
|
||||
serveurs_forgejo_http_port: 3000
|
||||
|
||||
# Base de donnees (registre docs/bases-donnees.yml).
|
||||
serveurs_forgejo_db_cle: "forgejo"
|
||||
# Base de donnees : resolue depuis le registre par le groupe consommateur.
|
||||
serveurs_forgejo_groupe: "serveurs_forgejo"
|
||||
serveurs_forgejo_db_host: "10.1.13.11" # data-01
|
||||
|
||||
# Relais courriel.
|
||||
|
|
|
|||
|
|
@ -11,16 +11,20 @@
|
|||
ansible.builtin.include_vars:
|
||||
file: "{{ role_path }}/../../docs/bases-donnees.yml"
|
||||
|
||||
- name: Resoudre l'entree de base Forgejo
|
||||
- name: Resoudre l'entree de base Forgejo (par consommateur)
|
||||
ansible.builtin.set_fact:
|
||||
serveurs_forgejo_entree: "{{ (bases_donnees | default({}))[serveurs_forgejo_db_cle] | default({}) }}"
|
||||
serveurs_forgejo_entree: >-
|
||||
{{ ((bases_donnees | default({})) | dict2items
|
||||
| selectattr('value.consommateur', 'defined')
|
||||
| selectattr('value.consommateur', 'equalto', serveurs_forgejo_groupe)
|
||||
| map(attribute='value') | list | first) | default({}) }}
|
||||
|
||||
- name: Exiger l'entree de registre et son secret
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- serveurs_forgejo_entree.base is defined
|
||||
- serveurs_forgejo_entree.secret is defined
|
||||
fail_msg: "Entree '{{ serveurs_forgejo_db_cle }}' absente de docs/bases-donnees.yml."
|
||||
fail_msg: "Aucune base avec consommateur '{{ serveurs_forgejo_groupe }}' dans docs/bases-donnees.yml."
|
||||
|
||||
- name: Resoudre le mot de passe de base (Vault)
|
||||
ansible.builtin.set_fact:
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ serveurs_icinga_depot_source: >-
|
|||
deb [signed-by=/usr/share/keyrings/icinga-archive-keyring.gpg]
|
||||
https://packages.icinga.com/debian icinga-{{ ansible_distribution_release }} main
|
||||
|
||||
# Base Icinga DB : entree du registre docs/bases-donnees.yml.
|
||||
serveurs_icinga_db_cle: "icingadb"
|
||||
# Base Icinga DB : resolue depuis le registre par le groupe consommateur.
|
||||
serveurs_icinga_groupe: "serveurs_icinga"
|
||||
serveurs_icinga_db_host: "10.1.13.11" # data-01 (serveurs_postgresql)
|
||||
|
||||
# Redis dedie a Icinga DB (paquet icingadb-redis).
|
||||
|
|
|
|||
|
|
@ -26,16 +26,20 @@
|
|||
ansible.builtin.include_vars:
|
||||
file: "{{ role_path }}/../../docs/bases-donnees.yml"
|
||||
|
||||
- name: Resoudre l'entree de base Icinga DB
|
||||
- name: Resoudre l'entree de base Icinga DB (par consommateur)
|
||||
ansible.builtin.set_fact:
|
||||
serveurs_icinga_entree: "{{ (bases_donnees | default({}))[serveurs_icinga_db_cle] | default({}) }}"
|
||||
serveurs_icinga_entree: >-
|
||||
{{ ((bases_donnees | default({})) | dict2items
|
||||
| selectattr('value.consommateur', 'defined')
|
||||
| selectattr('value.consommateur', 'equalto', serveurs_icinga_groupe)
|
||||
| map(attribute='value') | list | first) | default({}) }}
|
||||
|
||||
- name: Exiger l'entree de registre et son secret
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- serveurs_icinga_entree.base is defined
|
||||
- serveurs_icinga_entree.secret is defined
|
||||
fail_msg: "Entree '{{ serveurs_icinga_db_cle }}' absente de docs/bases-donnees.yml."
|
||||
fail_msg: "Aucune base avec consommateur '{{ serveurs_icinga_groupe }}' dans docs/bases-donnees.yml."
|
||||
|
||||
- name: Resoudre le mot de passe de base (Vault)
|
||||
ansible.builtin.set_fact:
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ serveurs_keycloak_service: "keycloak"
|
|||
# Publication (derriere serveurs_nginx, TLS termine a l'edge).
|
||||
serveurs_keycloak_hostname: "keycloak.chezlepro.internal"
|
||||
|
||||
# Base de donnees : entree du registre docs/bases-donnees.yml.
|
||||
serveurs_keycloak_db_cle: "keycloak"
|
||||
# Base de donnees : resolue depuis le registre par le groupe consommateur.
|
||||
serveurs_keycloak_groupe: "serveurs_keycloak"
|
||||
# IP interne du serveur PostgreSQL (data-01, cf. nomenclature).
|
||||
serveurs_keycloak_db_host: "10.1.13.11"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,13 @@
|
|||
ansible.builtin.include_vars:
|
||||
file: "{{ role_path }}/../../docs/bases-donnees.yml"
|
||||
|
||||
- name: Resoudre l'entree de base de donnees Keycloak
|
||||
- name: Resoudre l'entree de base de donnees Keycloak (par consommateur)
|
||||
ansible.builtin.set_fact:
|
||||
serveurs_keycloak_entree: "{{ (bases_donnees | default({}))[serveurs_keycloak_db_cle] | default({}) }}"
|
||||
serveurs_keycloak_entree: >-
|
||||
{{ ((bases_donnees | default({})) | dict2items
|
||||
| selectattr('value.consommateur', 'defined')
|
||||
| selectattr('value.consommateur', 'equalto', serveurs_keycloak_groupe)
|
||||
| map(attribute='value') | list | first) | default({}) }}
|
||||
|
||||
- name: Exiger une entree de registre et son secret
|
||||
ansible.builtin.assert:
|
||||
|
|
@ -19,7 +23,7 @@
|
|||
- serveurs_keycloak_entree.base is defined
|
||||
- serveurs_keycloak_entree.proprietaire is defined
|
||||
- serveurs_keycloak_entree.secret is defined
|
||||
fail_msg: "Entree '{{ serveurs_keycloak_db_cle }}' absente de docs/bases-donnees.yml."
|
||||
fail_msg: "Aucune base avec consommateur '{{ serveurs_keycloak_groupe }}' dans docs/bases-donnees.yml."
|
||||
|
||||
- name: Resoudre le mot de passe de base de donnees (Vault)
|
||||
ansible.builtin.set_fact:
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ def lister(registre: dict) -> None:
|
|||
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')}]")
|
||||
conso = base.get("consommateur", "?")
|
||||
usage = base.get("usage", "-")
|
||||
print(f" {cle} <- {conso} ({usage}): {chaine_connexion(base, srv)} [secret {base.get('secret')}]")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
|
@ -61,6 +63,8 @@ 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("--usage", default="principale")
|
||||
|
||||
pr = sub.add_parser("retirer-base", help="Retire une base applicative.")
|
||||
pr.add_argument("--cle", required=True)
|
||||
|
|
@ -87,6 +91,7 @@ def main() -> int:
|
|||
registre["bases_donnees"][args.cle] = {
|
||||
"serveur": args.serveur, "base": args.base,
|
||||
"proprietaire": args.proprietaire, "secret": args.secret,
|
||||
"consommateur": args.consommateur, "usage": args.usage,
|
||||
}
|
||||
valider_bases(registre)
|
||||
ecrire(registre)
|
||||
|
|
|
|||
|
|
@ -498,6 +498,9 @@ HTML = r"""<!doctype html>
|
|||
.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; }
|
||||
.ch-bases { display: grid; gap: 4px; margin: 6px 0 0 23px; }
|
||||
.ch-base { font-size: 11.5px; color: var(--muted); overflow-wrap: anywhere; }
|
||||
.badge-bd { display: inline-block; font-size: 10px; font-weight: 800; padding: 1px 6px; border-radius: 999px; background: color-mix(in srgb, var(--bleu) 18%, transparent); color: var(--bleu); }
|
||||
|
||||
.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; }
|
||||
|
|
@ -792,9 +795,13 @@ HTML = r"""<!doctype html>
|
|||
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>` : '';
|
||||
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}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
|
@ -820,7 +827,8 @@ HTML = r"""<!doctype html>
|
|||
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 || ''}));
|
||||
cle, serveur: x.serveur || '', base: x.base || '', proprietaire: x.proprietaire || '', secret: x.secret || '',
|
||||
consommateur: x.consommateur || '', usage: x.usage || 'principale'}));
|
||||
basesModifie = false;
|
||||
}
|
||||
|
||||
|
|
@ -839,7 +847,7 @@ HTML = r"""<!doctype html>
|
|||
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 ajouterBaseBd() { bdApplis.push({cle: '', serveur: (bdServeurs[0] || {}).nom || '', base: '', proprietaire: '', secret: '', consommateur: '', usage: 'principale'}); marquerBasesModifie(); dessinerBases(); }
|
||||
function retirerBaseBd(i) { bdApplis.splice(i, 1); marquerBasesModifie(); dessinerBases(); }
|
||||
|
||||
function dessinerBases() {
|
||||
|
|
@ -853,9 +861,12 @@ 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 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>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>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('')}
|
||||
</select></label>
|
||||
|
|
@ -863,7 +874,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(dsnBase(a))}</div>
|
||||
<div class="dsn mono">${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">
|
||||
|
|
@ -881,7 +892,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}; });
|
||||
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'}; });
|
||||
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})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ def valider_bases(registre: dict) -> None:
|
|||
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"):
|
||||
for champ in ("serveur", "base", "proprietaire", "secret", "consommateur"):
|
||||
if not str(base.get(champ, "")).strip():
|
||||
raise ValueError(f"Base '{cle}': champ '{champ}' requis.")
|
||||
if base["serveur"] not in serveurs:
|
||||
|
|
@ -64,6 +64,16 @@ def valider_bases(registre: dict) -> None:
|
|||
noms_bases.add(couple)
|
||||
|
||||
|
||||
def bases_du_groupe(registre: dict, groupe: str) -> list[dict]:
|
||||
"""Bases consommees par un groupe applicatif, avec leur serveur resolu."""
|
||||
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"), {})})
|
||||
return resultat
|
||||
|
||||
|
||||
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