2026-06-21 20:47:44 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Regles partagees pour les inventaires statiques Set-OPS."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-06-21 21:31:03 -04:00
|
|
|
import yaml
|
|
|
|
|
|
2026-06-21 20:47:44 -04:00
|
|
|
|
|
|
|
|
GROUPES_OPERATIONNELS_PREFIXES = ("serveurs_", "clients_")
|
|
|
|
|
GROUPE_HOTES_ACTIFS = "hotes_actifs"
|
|
|
|
|
GROUPE_HOTES_PLANIFIES = "hotes_planifies"
|
|
|
|
|
GROUPES_ETAT_HOTE = {GROUPE_HOTES_ACTIFS, GROUPE_HOTES_PLANIFIES}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def est_groupe_operationnel(groupe: str) -> bool:
|
|
|
|
|
return groupe.startswith(GROUPES_OPERATIONNELS_PREFIXES)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def groupes_operationnels_connus(enfants: dict, dossier_playbooks: Path) -> list[str]:
|
|
|
|
|
groupes = set(enfants)
|
|
|
|
|
groupes.update(playbook.stem for playbook in dossier_playbooks.glob("*.yml"))
|
|
|
|
|
return sorted(groupe for groupe in groupes if est_groupe_operationnel(groupe))
|
2026-06-21 21:31:03 -04:00
|
|
|
|
|
|
|
|
|
2026-06-22 19:14:12 -04:00
|
|
|
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.")
|
2026-06-22 19:44:05 -04:00
|
|
|
for champ in ("serveur", "base", "proprietaire", "secret", "consommateur"):
|
2026-06-22 19:14:12 -04:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 19:44:05 -04:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 19:14:12 -04:00
|
|
|
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', '?')}"
|
|
|
|
|
|
|
|
|
|
|
Construire l'ecosysteme de services et outiller l'inventaire
Registres (source unique):
- docs/nomenclature.yml: domaines, VMID, plan d'adressage 10.1.0.0/16 segmente.
- docs/bases-donnees.yml: bases applicatives (1 appli -> 1 base -> 1 owner -> 1 DSN).
Roles de service:
- Reseau/edge/PKI/mail: nginx, step_ca, sendmail.
- Donnees/identite: postgresql (consommateur du registre BD), redis, openldap, keycloak.
- Observabilite: prometheus, loki, grafana.
- Supervision: icinga (coeur; Icinga Web 2 differe). Forge: forgejo.
Integrations clientes:
- clients_metriques, clients_journaux, clients_pki, clients_ldap, clients_smtp.
Inventaire et outillage:
- make inventaire-ui: refonte (cartes, theme sombre, onglets, vue Chaine VM->groupes->
playbooks->roles), saisie du provisioning, auto-proposition depuis la nomenclature,
deploiement securise (verifier/deployer, jeton anti-CSRF, verrou, mot de passe vault),
robustesse reseau (connexions fermees, favicon).
- Makefile: cible verifier-deploiement; detection d'un group_vars de production chiffre.
- Scission serveurs_web -> serveurs_web_frontaux/dorsaux; migration de l'adressage vers
10.1.x; retrait des hotes de test; planification des hotes; requirements.yml.
Chaque role valide en --syntax-check et ansible-lint (profil production).
Secrets references depuis Ansible Vault (jamais en clair); roles non testes live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:06:11 -04:00
|
|
|
def charger_nomenclature(path: Path | None) -> dict:
|
|
|
|
|
"""Charge le registre de nomenclature (domaines, categories, adressage)."""
|
|
|
|
|
if not path or not path.exists():
|
|
|
|
|
return {}
|
|
|
|
|
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.")
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
2026-06-21 21:31:03 -04:00
|
|
|
def charger_dependances(path: Path | None) -> dict:
|
|
|
|
|
if not path:
|
|
|
|
|
return {}
|
|
|
|
|
if not path.exists():
|
|
|
|
|
raise ValueError(f"Fichier de dependances introuvable: {path}")
|
|
|
|
|
|
|
|
|
|
with path.open("r", encoding="utf-8") as dependencies_file:
|
|
|
|
|
data = yaml.safe_load(dependencies_file) or {}
|
|
|
|
|
|
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
|
raise ValueError(f"{path} ne contient pas une table YAML.")
|
|
|
|
|
|
|
|
|
|
groups = data.get("groupes", {})
|
|
|
|
|
if not isinstance(groups, dict):
|
|
|
|
|
raise ValueError(f"{path} doit contenir une cle 'groupes' de type table.")
|
|
|
|
|
|
|
|
|
|
for group, config in groups.items():
|
|
|
|
|
if not est_groupe_operationnel(group):
|
|
|
|
|
raise ValueError(f"Groupe de dependance non operationnel: {group}")
|
|
|
|
|
if not isinstance(config, dict):
|
|
|
|
|
raise ValueError(f"Dependance invalide pour {group}: table attendue.")
|
|
|
|
|
required = config.get("requiert_groupes_actifs", [])
|
|
|
|
|
if required is None:
|
|
|
|
|
required = []
|
|
|
|
|
if not isinstance(required, list) or not all(isinstance(item, str) for item in required):
|
|
|
|
|
raise ValueError(f"requiert_groupes_actifs doit etre une liste de groupes pour {group}.")
|
|
|
|
|
for required_group in required:
|
|
|
|
|
if not est_groupe_operationnel(required_group):
|
|
|
|
|
raise ValueError(f"Prerequis non operationnel pour {group}: {required_group}")
|
|
|
|
|
|
|
|
|
|
return groups
|