Phase 2 : le serveur (VM) entre dans le plan (lecture seule)
Registre docs/serveurs.yml (VM = fonction + etat + placement/taille), bootstrape depuis l'inventaire qui reste autorite. La derivation nomenclature (fonction+rang -> VMID/IP/VLAN/passerelle) est portee en Python (source unique) et reconciliee avec l'inventaire. - inventory_rules : deriver_nomenclature, fonction_seq, charger/valider/ reconcilier_serveur. - scripts/serveurs.py : lister / verifier / bootstrap + cibles make, integre a inventaire-verifier. - GUI : vue Serveurs (lecture seule) plan + derive + statut reconciliation. Bootstrap : 13 serveurs, tous reconcilies (le derive reproduit exactement l'inventaire -> diff vide atteignable en Phase 3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dd664dcddb
commit
1c96fb0acc
6 changed files with 387 additions and 1 deletions
|
|
@ -8,6 +8,7 @@
|
|||
- **Refonte « application-hub », Phase 1 — les liens deviennent explicites (inventaire inchangé, zéro régression).** L'application porte désormais tous ses liens : `groupe` (capacité/rôle), `hote` (VM), `port`, **`requiert`** (autres applications dont elle dépend) et **`expose`** (FQDN publics qui la publient). L'**exposition DNS quitte `docs/domaines.yml`** (le bloc `exposition` est retiré) et vit sur l'application via `expose` ; `docs/domaines.yml` ne décrit plus que les zones (autorité, edge, secondaires, dnssec, mail). `serveurs_nginx` dérive ses vhosts des **applications** (`expose` + edge du domaine), résolvant `application → hôte → IP` (rôle + `expositions.conf.j2` réécrits). Nouvelle règle partagée `expositions_des_applications` (+ `domaine_parent`) dans `inventory_rules.py`, exposée à Ansible par le filter plugin ; `valider_applications` valide `requiert` (applis connues), `expose` (domaine parent déclaré) et `port`. CLI (`applications.py --port/--requiert/--expose`, `domaines.py` affiche les expositions dérivées) et **GUI** (champs Port/Requiert/Expose sur la fiche Application, persistés) suivent ; l'API expose aussi le registre des domaines. Le groupe n'est plus une cible de liaison, seulement une capacité. *(Renommage `nomenclature.domaines:` → `fonctions:` reporté en Phase 1b ; génération de l'inventaire depuis le plan en Phase 3.)*
|
||||
|
||||
### Ajouté
|
||||
- **Phase 2 — le serveur (VM) entre dans le plan (lecture seule, l'inventaire reste autorité).** Nouveau registre `docs/serveurs.yml` (VM = `fonction` + `etat` + placement/dimensionnement Proxmox), **bootstrapé depuis l'inventaire** (`make serveurs-bootstrap`). La dérivation nomenclature (fonction + rang → VMID/IP/VLAN/passerelle) est portée en Python (`deriver_nomenclature`, source unique, miroir de l'auto-proposition du GUI) et la **réconciliation** compare le dérivé aux valeurs de l'inventaire (`reconcilier_serveur`). CLI `scripts/serveurs.py` (`lister`/`verifier`/`bootstrap`) + cibles `make serveurs` / `serveurs-verifier` / `serveurs-bootstrap`, intégré à `make inventaire-verifier`. GUI : vue **Serveurs** (lecture seule) affichant le plan + valeurs dérivées + statut de réconciliation. Bootstrap initial : **13 serveurs, tous réconciliés** (le dérivé reproduit exactement l'inventaire → la génération de Phase 3 pourra viser un diff vide).
|
||||
- **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`.
|
||||
|
||||
|
|
|
|||
12
Makefile
12
Makefile
|
|
@ -370,11 +370,21 @@ 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/serveurs.py verifier
|
||||
python3 scripts/applications.py verifier
|
||||
python3 scripts/bases_donnees.py verifier
|
||||
python3 scripts/domaines.py verifier
|
||||
|
||||
.PHONY: bases bases-verifier domaines domaines-verifier applications applications-verifier
|
||||
.PHONY: bases bases-verifier domaines domaines-verifier applications applications-verifier serveurs serveurs-verifier serveurs-bootstrap
|
||||
serveurs:
|
||||
python3 scripts/serveurs.py lister
|
||||
|
||||
serveurs-verifier:
|
||||
python3 scripts/serveurs.py verifier
|
||||
|
||||
serveurs-bootstrap:
|
||||
python3 scripts/serveurs.py bootstrap
|
||||
|
||||
bases:
|
||||
python3 scripts/bases_donnees.py lister
|
||||
|
||||
|
|
|
|||
98
docs/serveurs.yml
Normal file
98
docs/serveurs.yml
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Registre des serveurs (VM) du plan Set-OPS.
|
||||
# Bootstrape depuis l'inventaire (make serveurs-bootstrap). L'inventaire
|
||||
# reste autorite en Phase 2 ; VMID/IP/VLAN/passerelle sont DERIVES de la
|
||||
# fonction via docs/nomenclature.yml (non stockes ici).
|
||||
---
|
||||
serveurs:
|
||||
collab-01:
|
||||
fonction: collab
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
data-01:
|
||||
fonction: data
|
||||
etat: planifie
|
||||
noeud: asgard
|
||||
stockage: TrueNAS
|
||||
disque: 160G
|
||||
memoire: 2048
|
||||
coeurs: 4
|
||||
forge-01:
|
||||
fonction: forge
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
idm-01:
|
||||
fonction: idm
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
infra-dns-01:
|
||||
fonction: infra-dns
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
infra-edge-01:
|
||||
fonction: infra-edge
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
infra-mail-01:
|
||||
fonction: infra-mail
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
infra-pki-01:
|
||||
fonction: infra-pki
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
mon-01:
|
||||
fonction: mon
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
obs-01:
|
||||
fonction: obs
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 4
|
||||
web-dorsal-01:
|
||||
fonction: web-dorsal
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
web-frontal-01:
|
||||
fonction: web-frontal
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
web-frontal-02:
|
||||
fonction: web-frontal
|
||||
etat: planifie
|
||||
stockage: TrueNAS
|
||||
disque: 16G
|
||||
memoire: 2048
|
||||
coeurs: 2
|
||||
|
|
@ -27,8 +27,10 @@ from inventory_rules import (
|
|||
charger_dependances,
|
||||
charger_domaines,
|
||||
charger_nomenclature,
|
||||
charger_serveurs,
|
||||
est_groupe_operationnel,
|
||||
groupes_operationnels_connus,
|
||||
reconcilier_serveur,
|
||||
valider_applications,
|
||||
valider_bases,
|
||||
)
|
||||
|
|
@ -42,6 +44,7 @@ FICHIER_NOMENCLATURE = RACINE / "docs/nomenclature.yml"
|
|||
FICHIER_BASES = RACINE / "docs/bases-donnees.yml"
|
||||
FICHIER_APPLICATIONS = RACINE / "docs/applications.yml"
|
||||
FICHIER_DOMAINES = RACINE / "docs/domaines.yml"
|
||||
FICHIER_SERVEURS = RACINE / "docs/serveurs.yml"
|
||||
|
||||
# Garde-fous des executions (verifier / deployer) depuis l'interface.
|
||||
JETON = secrets.token_urlsafe(18)
|
||||
|
|
@ -169,6 +172,26 @@ def liste_hotes(data: dict) -> list[dict]:
|
|||
return hotes
|
||||
|
||||
|
||||
def serveurs_reconcilies(data: dict) -> list:
|
||||
"""Serveurs du plan + valeurs derivees + statut de reconciliation (lecture)."""
|
||||
registre = charger_serveurs(FICHIER_SERVEURS)
|
||||
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
||||
hotes = {h["nom"]: h for h in liste_hotes(data) if h.get("nom")}
|
||||
resultat = []
|
||||
for nom, srv in (registre.get("serveurs") or {}).items():
|
||||
rec = reconcilier_serveur(nom, srv, hotes.get(nom), nomenclature)
|
||||
d = rec["derive"]
|
||||
statut = "absent" if rec["absent_inventaire"] else ("divergence" if rec["divergences"] else "reconcilie")
|
||||
resultat.append({
|
||||
"nom": nom, "fonction": srv.get("fonction", ""), "etat": srv.get("etat", ""),
|
||||
"noeud": srv.get("noeud", ""), "stockage": srv.get("stockage", ""),
|
||||
"disque": srv.get("disque", ""), "memoire": srv.get("memoire", ""), "coeurs": srv.get("coeurs", ""),
|
||||
"vmid": d.get("vmid", ""), "adresse_ip": d.get("adresse_ip", ""), "vlan": d.get("vlan", ""),
|
||||
"statut": statut, "divergences": rec["divergences"],
|
||||
})
|
||||
return resultat
|
||||
|
||||
|
||||
def inventaire_api(path: Path) -> dict:
|
||||
data = charger_yaml(path)
|
||||
dependencies = charger_dependances(FICHIER_DEPENDANCES)
|
||||
|
|
@ -182,6 +205,7 @@ def inventaire_api(path: Path) -> dict:
|
|||
"bases": charger_bases_donnees(FICHIER_BASES),
|
||||
"applications": charger_applications(FICHIER_APPLICATIONS),
|
||||
"domaines": charger_domaines(FICHIER_DOMAINES),
|
||||
"serveurs": serveurs_reconcilies(data),
|
||||
"hotes": liste_hotes(data),
|
||||
}
|
||||
|
||||
|
|
@ -553,6 +577,7 @@ HTML = r"""<!doctype html>
|
|||
<div class="barre">
|
||||
<div class="vue-bascule">
|
||||
<button type="button" id="btn-vue-cartes" class="on" onclick="setVue('cartes')">Inventaire</button>
|
||||
<button type="button" id="btn-vue-serveurs" onclick="setVue('serveurs')">Serveurs</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>
|
||||
|
|
@ -612,6 +637,7 @@ HTML = r"""<!doctype html>
|
|||
let basesModifie = false;
|
||||
let applications = [];
|
||||
let applicationsModifie = false;
|
||||
let serveurs = [];
|
||||
let selection = -1;
|
||||
let selectionNom = null;
|
||||
let ongletActif = 'reseau';
|
||||
|
|
@ -656,6 +682,7 @@ HTML = r"""<!doctype html>
|
|||
chaine = data.chaine || {};
|
||||
chargerBases(data);
|
||||
chargerApplications(data);
|
||||
serveurs = data.serveurs || [];
|
||||
hotes = data.hotes;
|
||||
estProduction = !!data.production;
|
||||
modifie = false; verifie = {};
|
||||
|
|
@ -791,6 +818,7 @@ HTML = r"""<!doctype html>
|
|||
function setVue(v) {
|
||||
vuePrincipale = v;
|
||||
document.getElementById('btn-vue-cartes').classList.toggle('on', v === 'cartes');
|
||||
document.getElementById('btn-vue-serveurs').classList.toggle('on', v === 'serveurs');
|
||||
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');
|
||||
|
|
@ -799,6 +827,7 @@ HTML = r"""<!doctype html>
|
|||
|
||||
function dessinerGrilles() {
|
||||
document.getElementById('chips').style.display = vuePrincipale === 'cartes' ? '' : 'none';
|
||||
if (vuePrincipale === 'serveurs') { dessinerServeurs(); return; }
|
||||
if (vuePrincipale === 'chaine') { dessinerArbre(); return; }
|
||||
if (vuePrincipale === 'applications') { dessinerApplications(); return; }
|
||||
if (vuePrincipale === 'bases') { dessinerBases(); return; }
|
||||
|
|
@ -1029,6 +1058,33 @@ HTML = r"""<!doctype html>
|
|||
message('Applications sauvegardées.', 'ok');
|
||||
}
|
||||
|
||||
function dessinerServeurs() {
|
||||
const cible = document.getElementById('grilles');
|
||||
if (!serveurs.length) { cible.innerHTML = '<div class="vide"><b>Aucun serveur dans le plan</b>Lance « make serveurs-bootstrap » pour générer docs/serveurs.yml depuis l\\'inventaire.</div>'; return; }
|
||||
const clsStatut = (s) => s === 'reconcilie' ? 'actif' : (s === 'divergence' ? 'bloque' : 'attente');
|
||||
const libStatut = (s) => s === 'reconcilie' ? 'réconcilié' : (s === 'divergence' ? 'divergence' : 'absent inv.');
|
||||
const lignes = serveurs.map(s => `
|
||||
<div class="base-ligne">
|
||||
<label class="champ"><span>Serveur</span><span class="mono">${echapper(s.nom)}</span></label>
|
||||
<label class="champ"><span>Fonction</span><span>${echapper(s.fonction)}</span></label>
|
||||
<label class="champ"><span>État</span><span>${echapper(s.etat || '—')}</span></label>
|
||||
<label class="champ"><span>VMID (dérivé)</span><span class="mono">${echapper(s.vmid)}</span></label>
|
||||
<label class="champ"><span>IP (dérivée)</span><span class="mono">${echapper(s.adresse_ip)}</span></label>
|
||||
<label class="champ"><span>VLAN</span><span class="mono">${echapper(s.vlan)}</span></label>
|
||||
<label class="champ"><span>Placement</span><span>${echapper([s.noeud, s.stockage].filter(Boolean).join(' / ') || '—')}</span></label>
|
||||
<label class="champ"><span>Taille</span><span>${echapper([s.disque, s.memoire && (s.memoire + 'Mo'), s.coeurs && (s.coeurs + 'c')].filter(Boolean).join(' · ') || '—')}</span></label>
|
||||
<span class="badge-statut ${clsStatut(s.statut)}">${libStatut(s.statut)}</span>
|
||||
${(s.divergences && s.divergences.length) ? `<div class="dsn mono">${s.divergences.map(echapper).join(' ; ')}</div>` : ''}
|
||||
</div>`).join('');
|
||||
const nbDiv = serveurs.filter(s => s.statut !== 'reconcilie').length;
|
||||
cible.innerHTML = `
|
||||
<div class="section-grille">
|
||||
<div class="section-tete">Serveurs du plan <span class="compte">${serveurs.length}</span>${nbDiv ? ' · ' + nbDiv + ' non réconcilié(s)' : ' · tous réconciliés'}</div>
|
||||
<div class="bases-liste">${lignes}</div>
|
||||
<div class="dsn" style="padding:8px 2px;color:#94a3b8">Lecture seule (Phase 2). VMID / IP / VLAN sont <b>dérivés</b> de la fonction via la nomenclature ; « réconcilié » = identique à l'inventaire. La génération de l'inventaire depuis le plan viendra en Phase 3.</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function dessinerDependances() {
|
||||
const cible = document.getElementById('dependances');
|
||||
const noms = Object.keys(dependances).sort();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
|
@ -293,6 +294,94 @@ def charger_nomenclature(path: Path | None) -> dict:
|
|||
return data
|
||||
|
||||
|
||||
ETATS_SERVEUR = {"actif", "planifie"}
|
||||
|
||||
|
||||
def fonction_seq(nom: str) -> tuple[str, int | None]:
|
||||
"""Decompose un hostname 'fonction-NN' en (fonction, NN)."""
|
||||
m = re.match(r"^(.+)-(\d+)$", str(nom or ""))
|
||||
if m:
|
||||
return m.group(1), int(m.group(2))
|
||||
return str(nom or ""), None
|
||||
|
||||
|
||||
def deriver_nomenclature(fonction: str, seq: int, nomenclature: dict) -> dict | None:
|
||||
"""Derive VMID / VLAN / IP / passerelle / CIDR depuis la fonction et le rang.
|
||||
|
||||
Source unique de la derivation (miroir Python de l'auto-proposition du GUI).
|
||||
"""
|
||||
fonctions = nomenclature.get("fonctions") or {}
|
||||
categories = nomenclature.get("categories") or {}
|
||||
f = fonctions.get(fonction)
|
||||
if not f or seq is None:
|
||||
return None
|
||||
cat = f.get("categorie")
|
||||
svc = f.get("service")
|
||||
c = categories.get(cat) or categories.get(str(cat))
|
||||
if not c:
|
||||
return None
|
||||
base3 = ".".join(str(c.get("sous_reseau", "")).split(".")[:3])
|
||||
return {
|
||||
"vmid": f"9{cat}{svc}{int(seq):02d}",
|
||||
"vlan": c.get("vlan"),
|
||||
"adresse_ip": f"{base3}.{svc * 10 + int(seq)}",
|
||||
"passerelle": c.get("passerelle"),
|
||||
"cidr": nomenclature.get("cidr_hote", 24),
|
||||
}
|
||||
|
||||
|
||||
def charger_serveurs(path: Path | None) -> dict:
|
||||
"""Charge le registre des serveurs (VM) du plan."""
|
||||
if not path or not path.exists():
|
||||
return {"serveurs": {}}
|
||||
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", {})
|
||||
return data
|
||||
|
||||
|
||||
def valider_serveurs(registre: dict, nomenclature: dict | None = None) -> None:
|
||||
"""Valide la coherence du registre des serveurs."""
|
||||
serveurs = registre.get("serveurs") or {}
|
||||
fonctions = (nomenclature or {}).get("fonctions") or {}
|
||||
if not isinstance(serveurs, dict):
|
||||
raise ValueError("serveurs doit etre une table.")
|
||||
for nom, srv in serveurs.items():
|
||||
if not isinstance(srv, dict):
|
||||
raise ValueError(f"Serveur '{nom}': table attendue.")
|
||||
fonction = str(srv.get("fonction", "")).strip()
|
||||
if not fonction:
|
||||
raise ValueError(f"Serveur '{nom}': champ 'fonction' requis.")
|
||||
if fonctions and fonction not in fonctions:
|
||||
raise ValueError(f"Serveur '{nom}': fonction '{fonction}' inconnue de la nomenclature.")
|
||||
etat = str(srv.get("etat", "")).strip()
|
||||
if etat and etat not in ETATS_SERVEUR:
|
||||
raise ValueError(f"Serveur '{nom}': etat '{etat}' inconnu (attendu: {', '.join(sorted(ETATS_SERVEUR))}).")
|
||||
|
||||
|
||||
def reconcilier_serveur(nom: str, srv: dict, hote_inventaire: dict | None, nomenclature: dict) -> dict:
|
||||
"""Compare les valeurs derivees (fonction+rang) aux valeurs de l'inventaire.
|
||||
|
||||
Renvoie {derive, inventaire, divergences} ; divergences vide = reconcilie.
|
||||
"""
|
||||
_, seq = fonction_seq(nom)
|
||||
derive = deriver_nomenclature(str(srv.get("fonction", "")), seq, nomenclature) or {}
|
||||
inv = hote_inventaire or {}
|
||||
champs = {"vmid": "vmid", "adresse_ip": "adresse_ip", "vlan": "vlan", "passerelle": "passerelle"}
|
||||
divergences = []
|
||||
for cle_derivee, cle_inv in champs.items():
|
||||
if cle_derivee not in derive:
|
||||
continue
|
||||
attendu = str(derive[cle_derivee])
|
||||
present = str(inv.get(cle_inv, "")).strip()
|
||||
if present and present != attendu:
|
||||
divergences.append(f"{cle_derivee}: inventaire={present} != derive={attendu}")
|
||||
return {"derive": derive, "inventaire": inv, "divergences": divergences,
|
||||
"absent_inventaire": hote_inventaire is None}
|
||||
|
||||
|
||||
def charger_dependances(path: Path | None) -> dict:
|
||||
if not path:
|
||||
return {}
|
||||
|
|
|
|||
132
scripts/serveurs.py
Normal file
132
scripts/serveurs.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Registre des serveurs (VM) du plan Set-OPS.
|
||||
|
||||
Phase 2 : l'inventaire reste AUTORITE. Ce registre est bootstrape depuis lui
|
||||
(reconciliation en lecture). La generation de l'inventaire depuis le plan
|
||||
viendra en Phase 3 (make instancier).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from inventory_gui import charger_yaml, liste_hotes
|
||||
from inventory_rules import (
|
||||
charger_nomenclature,
|
||||
charger_serveurs,
|
||||
fonction_seq,
|
||||
reconcilier_serveur,
|
||||
valider_serveurs,
|
||||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
FICHIER = RACINE / "docs/serveurs.yml"
|
||||
FICHIER_NOMENCLATURE = RACINE / "docs/nomenclature.yml"
|
||||
INVENTAIRE = RACINE / "inventories/production/hosts.yml"
|
||||
|
||||
# Champs de placement / dimensionnement NON derivables (proviennent du plan).
|
||||
CHAMPS_PLAN = [("noeud", "noeud"), ("stockage", "stockage"),
|
||||
("disque_taille", "disque"), ("memoire", "memoire"), ("coeurs", "coeurs")]
|
||||
|
||||
|
||||
def ecrire(registre: dict) -> None:
|
||||
entete = (
|
||||
"# Registre des serveurs (VM) du plan Set-OPS.\n"
|
||||
"# Bootstrape depuis l'inventaire (make serveurs-bootstrap). L'inventaire\n"
|
||||
"# reste autorite en Phase 2 ; VMID/IP/VLAN/passerelle sont DERIVES de la\n"
|
||||
"# fonction via docs/nomenclature.yml (non stockes ici).\n"
|
||||
"---\n"
|
||||
)
|
||||
with FICHIER.open("w", encoding="utf-8") as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump({"serveurs": registre.get("serveurs", {}) or {}},
|
||||
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
||||
|
||||
def hotes_inventaire() -> dict:
|
||||
return {h["nom"]: h for h in liste_hotes(charger_yaml(INVENTAIRE)) if h.get("nom")}
|
||||
|
||||
|
||||
def bootstrap() -> dict:
|
||||
serveurs = {}
|
||||
for nom, h in hotes_inventaire().items():
|
||||
fonction, _ = fonction_seq(nom)
|
||||
srv = {"fonction": fonction, "etat": h.get("etat", "planifie")}
|
||||
for cle_src, cle_dst in CHAMPS_PLAN:
|
||||
v = str(h.get(cle_src, "")).strip()
|
||||
if v:
|
||||
srv[cle_dst] = int(v) if v.isdigit() else v
|
||||
serveurs[nom] = srv
|
||||
return {"serveurs": serveurs}
|
||||
|
||||
|
||||
def lister() -> None:
|
||||
registre = charger_serveurs(FICHIER)
|
||||
serveurs = registre.get("serveurs", {})
|
||||
if not serveurs:
|
||||
print("Aucun serveur. Lance: make serveurs-bootstrap")
|
||||
return
|
||||
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
||||
hotes = hotes_inventaire()
|
||||
for nom, srv in serveurs.items():
|
||||
rec = reconcilier_serveur(nom, srv, hotes.get(nom), nomenclature)
|
||||
d = rec["derive"]
|
||||
if rec["absent_inventaire"]:
|
||||
statut = "ABSENT de l'inventaire"
|
||||
elif rec["divergences"]:
|
||||
statut = "DIVERGENCE"
|
||||
else:
|
||||
statut = "reconcilie"
|
||||
print(f"{nom} [fonction {srv.get('fonction')}, etat {srv.get('etat', '?')}]"
|
||||
f" derive: vmid {d.get('vmid', '?')}, ip {d.get('adresse_ip', '?')}, vlan {d.get('vlan', '?')} -> {statut}")
|
||||
for dv in rec["divergences"]:
|
||||
print(f" ! {dv}")
|
||||
|
||||
|
||||
def verifier() -> int:
|
||||
registre = charger_serveurs(FICHIER)
|
||||
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
||||
valider_serveurs(registre, nomenclature)
|
||||
hotes = hotes_inventaire()
|
||||
divergents = []
|
||||
for nom, srv in registre.get("serveurs", {}).items():
|
||||
rec = reconcilier_serveur(nom, srv, hotes.get(nom), nomenclature)
|
||||
if rec["divergences"] or rec["absent_inventaire"]:
|
||||
divergents.append(nom)
|
||||
if divergents:
|
||||
print(f"Registre valide, mais {len(divergents)} serveur(s) non reconcilie(s): {', '.join(divergents)}")
|
||||
else:
|
||||
print("Registre des serveurs valide et reconcilie avec l'inventaire.")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Registre des serveurs (VM) du plan Set-OPS.")
|
||||
sub = parser.add_subparsers(dest="commande", required=True)
|
||||
sub.add_parser("lister", help="Affiche les serveurs et leur reconciliation avec l'inventaire.")
|
||||
sub.add_parser("verifier", help="Valide le registre et signale les divergences.")
|
||||
sub.add_parser("bootstrap", help="(Re)genere docs/serveurs.yml depuis l'inventaire actuel.")
|
||||
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.commande == "lister":
|
||||
lister()
|
||||
elif args.commande == "verifier":
|
||||
return verifier()
|
||||
elif args.commande == "bootstrap":
|
||||
registre = bootstrap()
|
||||
valider_serveurs(registre, charger_nomenclature(FICHIER_NOMENCLATURE))
|
||||
ecrire(registre)
|
||||
print(f"docs/serveurs.yml genere depuis l'inventaire ({len(registre['serveurs'])} serveurs).")
|
||||
except Exception as exc:
|
||||
print(f"erreur: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in a new issue