Ajouter les dependances causales des groupes

This commit is contained in:
Daniel Allaire 2026-06-21 21:31:03 -04:00
parent df70c580bd
commit 1d25338d26
12 changed files with 326 additions and 21 deletions

View file

@ -297,6 +297,7 @@ Quand un nouveau groupe opérationnel est ajouté :
- créer le playbook homonyme dans `playbooks/groupes/` ;
- documenter son intention opérationnelle ;
- déclarer ses dépendances causales dans `docs/dependances-groupes.yml` si son exécution requiert un autre service actif ;
- prévoir les rôles nécessaires ;
- valider au minimum sa syntaxe ;
- l'ajouter aux facilités d'exploitation si l'opérateur doit l'utiliser directement.
@ -367,6 +368,14 @@ Grafana → datasources et dashboards côté plateforme
Quand un nouveau service central est ajouté, prévoir aussi le ou les rôles clients nécessaires pour intégrer les VM existantes et futures.
Les dépendances entre groupes doivent être déclarées dans :
```text
docs/dependances-groupes.yml
```
Ces dépendances servent à refuser un déploiement lorsque les prérequis actifs sont absents et à préparer les futurs checks de supervision.
Les intégrations clientes doivent être :
- idempotentes ;

View file

@ -5,6 +5,8 @@
### Ajouté
- Ajout de `make hote-planifier` pour renseigner un hôte, son VMID et ses groupes sans création ni déploiement immédiat.
- Ajout de `scripts/inventory_rules.py` pour centraliser les règles partagées entre la gestion CLI et l'interface locale d'inventaire.
- Ajout de `docs/dependances-groupes.yml` comme registre exploitable des dépendances causales entre groupes.
- Ajout du contrat `serveurs_powerdns` afin de représenter la capacité DNS centrale requise par `clients_dns`.
### Modifié
- Renforcement de la validation d'inventaire :
@ -16,6 +18,7 @@
- Alignement de l'aide Makefile et de la documentation sur le flux opérateur : planifier, activer/créer, déployer.
- Retrait des groupes génériques obsolètes `serveurs_bases_donnees` et `serveurs_supervision` au profit des groupes de services précis.
- Consolidation des groupes Icinga en `serveurs_icinga` afin de représenter une seule capacité de supervision serveur sur `mon-01`.
- Validation des prérequis actifs avant déploiement d'un groupe, déploiement d'un hôte ou sauvegarde active via l'interface d'inventaire.
## 2026-06-21

View file

@ -8,6 +8,7 @@ export ANSIBLE_SSH_ARGS ?= -F /dev/null -o ControlMaster=no
INVENTAIRE_LAB ?= inventories/lab/hosts.yml
INVENTAIRE_PRODUCTION ?= inventories/production/hosts.yml
FICHIER_INVENTAIRE ?= $(INVENTAIRE_PRODUCTION)
FICHIER_DEPENDANCES ?= docs/dependances-groupes.yml
GROUPE_MODELE ?= modeles_vm
GROUPE_DEBIAN ?= serveurs_debian
GROUPE_HOTES_ACTIFS ?= hotes_actifs
@ -141,7 +142,7 @@ aide:
@printf '%s\n' 'Variables frequentes'
@printf '%s\n' ' HOTE=web-01 GROUPE=serveurs_debian GROUPES="serveurs_debian serveurs_durcis"'
@printf '%s\n' ' VMID=101 VLAN=12 ADRESSE_IP=192.168.12.101 PASSERELLE=192.168.12.1'
@printf '%s\n' ' FICHIER_INVENTAIRE=inventories/production/hosts.yml CONFIRMER=true'
@printf '%s\n' ' FICHIER_INVENTAIRE=inventories/production/hosts.yml FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true'
.PHONY: lint
lint: ansible-runtime
@ -187,7 +188,7 @@ hote-ajouter: ansible-runtime
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote ADRESSE_IP=adresse_ip.'; \
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) ajouter --hote $(HOTE) --adresse-ip $(ADRESSE_IP) --utilisateur-ansible $(UTILISATEUR_ANSIBLE) --vmid "$(VMID)" --groupes "$(GROUPES)" --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) ajouter --hote $(HOTE) --adresse-ip $(ADRESSE_IP) --utilisateur-ansible $(UTILISATEUR_ANSIBLE) --vmid "$(VMID)" --groupes "$(GROUPES)" --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
$(MAKE) inventaire-verifier
$(MAKE) hote-afficher HOTE="$(HOTE)" FICHIER_INVENTAIRE="$(FICHIER_INVENTAIRE)"
@ -196,7 +197,7 @@ hote-planifier: ansible-runtime
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote GROUPES="groupe1 groupe2".'; \
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) ajouter --hote $(HOTE) --vmid "$(VMID)" --groupes "$(GROUPES)" --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) ajouter --hote $(HOTE) --vmid "$(VMID)" --groupes "$(GROUPES)" --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
$(MAKE) inventaire-verifier
$(MAKE) hote-afficher HOTE="$(HOTE)" FICHIER_INVENTAIRE="$(FICHIER_INVENTAIRE)"
@ -205,7 +206,7 @@ hote-groupes: ansible-runtime
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote GROUPES="groupe1 groupe2".'; \
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) definir-groupes --hote $(HOTE) --groupes "$(GROUPES)" --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) definir-groupes --hote $(HOTE) --groupes "$(GROUPES)" --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
$(MAKE) inventaire-verifier
$(MAKE) hote-afficher HOTE="$(HOTE)" FICHIER_INVENTAIRE="$(FICHIER_INVENTAIRE)"
@ -225,6 +226,7 @@ appliquer: ansible-runtime
printf '%s\n' 'Refus: aucun playbook pour ce groupe: $(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml'; \
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE)
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)'
deployer:
@ -234,6 +236,7 @@ deployer:
fi
@python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES) > /dev/null
@python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) verifier-actif --hote $(HOTE)
@python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-hote --hote $(HOTE)
$(MAKE) _verifier-acces-hote LIMITE="$(HOTE)"
$(MAKE) _verifier-privileges-hote LIMITE="$(HOTE)"
@playbooks="$$(python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES))" || exit $$?; \
@ -322,6 +325,7 @@ inventaire-verifier: ansible-runtime
ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null
ansible-inventory -i $(INVENTAIRE_PRODUCTION) --list > /dev/null
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) verifier-playbooks --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
inventaire-lister: ansible-runtime
ansible-inventory -i $(FICHIER_INVENTAIRE) --list

View file

@ -87,6 +87,8 @@ Les groupes opérationnels avec des hôtes, comme `serveurs_web` ou `clients_dns
Le catalogue des services et intégrations prévus est dans `docs/catalogue-services.md`.
Les dépendances causales entre groupes sont dans `docs/dependances-groupes.yml`.
La nomenclature des noms de VM et des VMID est dans `docs/nomenclature-vm.md`.
Créer un clone depuis le modèle Debian 13 via l'API Proxmox :

View file

@ -28,6 +28,7 @@ Un service central peut partager un hôte avec d'autres services du même domain
| Forgejo | `serveurs_forgejo` | `playbooks/groupes/serveurs_forgejo.yml` | `forgejo` |
| Sendmail MTA | `serveurs_sendmail` | `playbooks/groupes/serveurs_sendmail.yml` | `sendmail` |
| step-ca | `serveurs_step_ca` | `playbooks/groupes/serveurs_step_ca.yml` | `step_ca` |
| PowerDNS | `serveurs_powerdns` | `playbooks/groupes/serveurs_powerdns.yml` | `powerdns` |
| Redis | `serveurs_redis` | `playbooks/groupes/serveurs_redis.yml` | `redis` |
| NGINX WAF et reverse proxy | `serveurs_nginx` | `playbooks/groupes/serveurs_nginx.yml` | `nginx` |
| Nextcloud | `serveurs_nextcloud` | `playbooks/groupes/serveurs_nextcloud.yml` | `nextcloud` |
@ -40,6 +41,7 @@ Un service central peut partager un hôte avec d'autres services du même domain
| `infra-pki-01` | step-ca |
| `infra-edge-01` | NGINX WAF et reverse proxy |
| `infra-mail-01` | Sendmail MTA |
| `infra-dns-01` | PowerDNS |
| `idm-01` | OpenLDAP, Keycloak |
| `data-01` | PostgreSQL, Redis |
| `obs-01` | Prometheus, Loki, Grafana |
@ -65,58 +67,63 @@ L'ordre ci-dessous privilégie les dépendances structurantes avant les applicat
### Phase 1 - Fondations transversales
1. `serveurs_step_ca`
1. `serveurs_powerdns`
- Service central : DNS interne autoritaire et/ou résolution interne selon le design retenu.
- Intégration à prévoir : `clients_dns`.
- Raison : les autres intégrations auront besoin de noms stables plutôt que d'adresses IP.
2. `serveurs_step_ca`
- Service central : autorité de certification interne et ACME.
- Intégration à prévoir : `clients_pki`.
- Raison : les autres services auront besoin de certificats fiables avant d'être exposés proprement.
2. `serveurs_nginx`
3. `serveurs_nginx`
- Service central : reverse proxy, terminaison TLS, publication HTTP(S), WAF.
- Intégration à prévoir : publication des services HTTP derrière le proxy.
- Raison : plusieurs services seront consommés par navigateur ou API et doivent passer par un point d'entrée cohérent.
3. `serveurs_sendmail`
4. `serveurs_sendmail`
- Service central : relais SMTP sortant.
- Intégration à prévoir : `clients_smtp`.
- Raison : les notifications, réinitialisations de mot de passe et alertes doivent fonctionner tôt.
### Phase 2 - Données et identité
4. `serveurs_postgresql`
5. `serveurs_postgresql`
- Service central : base de données relationnelle partagée.
- Intégration à prévoir : bases dédiées par application, comptes applicatifs et sauvegardes.
- Raison : Keycloak, Grafana, Icinga Web 2, Forgejo et Nextcloud peuvent dépendre de PostgreSQL.
5. `serveurs_openldap`
6. `serveurs_openldap`
- Service central : annuaire interne.
- Intégration à prévoir : `clients_ldap`.
- Raison : l'identité Unix et l'annuaire doivent exister avant les intégrations d'authentification avancées.
6. `serveurs_keycloak`
7. `serveurs_keycloak`
- Service central : SSO/OIDC/SAML.
- Intégration à prévoir : applications web derrière `serveurs_nginx`.
- Raison : les applications devraient être branchées au SSO dès leur arrivée plutôt qu'après coup.
### Phase 3 - Observabilité minimale
7. `serveurs_prometheus`
8. `serveurs_prometheus`
- Service central : métriques.
- Intégration à prévoir : `clients_metriques`.
- Raison : les prochains services doivent être mesurables dès leur déploiement.
8. `serveurs_loki`
9. `serveurs_loki`
- Service central : journaux centralisés.
- Intégration à prévoir : `clients_journaux`.
- Raison : les journaux centralisés accélèrent le diagnostic des services suivants.
9. `serveurs_grafana`
10. `serveurs_grafana`
- Service central : tableaux de bord.
- Intégration à prévoir : datasources Prometheus et Loki, authentification Keycloak.
- Raison : Grafana consolide les métriques et journaux après leur mise en place.
### Phase 4 - Supervision active
10. `serveurs_icinga`
11. `serveurs_icinga`
- Service central : supervision active, interface web et vues métiers Icinga.
- Composants prévus : Icinga 2, Icinga Web 2, Icinga BPM.
- Intégrations à prévoir : `clients_supervision`, PostgreSQL, NGINX, Keycloak si retenu.
@ -124,22 +131,22 @@ L'ordre ci-dessous privilégie les dépendances structurantes avant les applicat
### Phase 5 - Services applicatifs internes
11. `serveurs_redis`
12. `serveurs_redis`
- Service central : cache et files internes.
- Intégration à prévoir : Nextcloud et autres applications qui en ont besoin.
- Raison : Redis est une dépendance applicative, pas une fondation globale.
12. `serveurs_forgejo`
13. `serveurs_forgejo`
- Service central : forge Git.
- Intégration à prévoir : PostgreSQL, NGINX, Keycloak, SMTP, sauvegardes.
- Raison : la forge devient plus utile après SSO, TLS, SMTP et observabilité.
13. `serveurs_nextcloud`
14. `serveurs_nextcloud`
- Service central : collaboration fichiers.
- Intégration à prévoir : PostgreSQL, Redis, NGINX, Keycloak, SMTP, sauvegardes.
- Raison : Nextcloud dépend de plusieurs fondations et doit arriver après elles.
14. `serveurs_collabora`
15. `serveurs_collabora`
- Service central : édition documentaire en ligne.
- Intégration à prévoir : Nextcloud, NGINX, certificats.
- Raison : Collabora est une extension de Nextcloud et doit venir après lui.
@ -154,3 +161,14 @@ L'ordre ci-dessous privilégie les dépendances structurantes avant les applicat
- Tout rôle serveur doit documenter ses ports, secrets, sauvegardes, dépendances et groupes clients associés.
Les groupes clients peuvent être ajoutés aux VM existantes quand le service central correspondant est réellement disponible.
## Dépendances causales
Les dépendances exécutables sont déclarées dans `docs/dependances-groupes.yml`.
Ce fichier sert à deux usages :
- bloquer le déploiement d'un groupe tant que ses prérequis actifs ne sont pas présents ;
- fournir une source exploitable pour les futurs modèles de supervision.
Un hôte peut porter un groupe client à l'état planifié. Le déploiement est refusé tant que le groupe serveur requis n'a pas au moins un hôte actif.

View file

@ -0,0 +1,86 @@
---
groupes:
clients_dns:
requiert_groupes_actifs:
- serveurs_powerdns
raison: "Les resolvers clients ne doivent pas pointer vers un service DNS absent."
surveillance: "Verifier resolution interne et disponibilite du service DNS."
clients_pki:
requiert_groupes_actifs:
- serveurs_step_ca
raison: "La confiance CA et ACME client dependent de l'autorite interne."
surveillance: "Verifier validite CA, emission ACME et expiration des certificats."
clients_ldap:
requiert_groupes_actifs:
- serveurs_openldap
raison: "La configuration NSS/PAM/SSSD depend de l'annuaire LDAP."
surveillance: "Verifier bind LDAP, latence et expiration des certificats LDAP."
clients_supervision:
requiert_groupes_actifs:
- serveurs_icinga
raison: "Les agents ou checks clients doivent se rattacher a une plateforme Icinga active."
surveillance: "Verifier enregistrement des hotes, fraicheur des checks et notifications."
clients_metriques:
requiert_groupes_actifs:
- serveurs_prometheus
raison: "Les exporters clients doivent etre collectes par Prometheus."
surveillance: "Verifier targets Prometheus, scrape duration et erreurs de collecte."
clients_journaux:
requiert_groupes_actifs:
- serveurs_loki
raison: "L'expedition des journaux depend du collecteur central Loki."
surveillance: "Verifier ingestion Loki, retard et volume de journaux."
clients_smtp:
requiert_groupes_actifs:
- serveurs_sendmail
raison: "Les notifications locales doivent relayer vers un MTA actif."
surveillance: "Verifier file d'attente, relais SMTP et echecs de livraison."
serveurs_keycloak:
requiert_groupes_actifs:
- serveurs_postgresql
raison: "Keycloak doit utiliser une base PostgreSQL geree."
surveillance: "Verifier connexion base, etat realm et disponibilite OIDC."
serveurs_grafana:
requiert_groupes_actifs:
- serveurs_prometheus
- serveurs_loki
raison: "Grafana est utile comme interface aux metriques et journaux centraux."
surveillance: "Verifier datasources Prometheus/Loki et authentification."
serveurs_icinga:
requiert_groupes_actifs:
- serveurs_postgresql
raison: "La plateforme Icinga Web/BPM depend d'une base relationnelle."
surveillance: "Verifier moteur Icinga, base, interface web et notifications."
serveurs_forgejo:
requiert_groupes_actifs:
- serveurs_postgresql
- serveurs_nginx
- serveurs_sendmail
raison: "Forgejo depend d'une base, d'une publication HTTP(S) et du courriel."
surveillance: "Verifier HTTP(S), base, files Git et envoi courriel."
serveurs_nextcloud:
requiert_groupes_actifs:
- serveurs_postgresql
- serveurs_redis
- serveurs_nginx
- serveurs_sendmail
raison: "Nextcloud depend d'une base, d'un cache, d'un frontal web et du courriel."
surveillance: "Verifier jobs, base, Redis, HTTP(S), stockage et courriel."
serveurs_collabora:
requiert_groupes_actifs:
- serveurs_nextcloud
- serveurs_nginx
raison: "Collabora est expose via le frontal web et integre a Nextcloud."
surveillance: "Verifier connectivite Nextcloud, websocket et disponibilite HTTP(S)."

View file

@ -18,6 +18,7 @@ Exemples :
infra-pki-01
infra-edge-01
infra-mail-01
infra-dns-01
idm-01
data-01
obs-01
@ -35,6 +36,7 @@ Les noms courts historiques `web-01` et `web-02` restent valides pour les deux p
| `infra-pki-01` | `serveurs_step_ca` |
| `infra-edge-01` | `serveurs_nginx` |
| `infra-mail-01` | `serveurs_sendmail` |
| `infra-dns-01` | `serveurs_powerdns` |
| `idm-01` | `serveurs_openldap`, `serveurs_keycloak` |
| `data-01` | `serveurs_postgresql`, `serveurs_redis` |
| `obs-01` | `serveurs_prometheus`, `serveurs_loki`, `serveurs_grafana` |

View file

@ -20,6 +20,8 @@ all:
proxmox_vmid: 91201
infra-mail-01:
proxmox_vmid: 91301
infra-dns-01:
proxmox_vmid: 91401
idm-01:
proxmox_vmid: 92101
data-01:
@ -43,6 +45,7 @@ all:
infra-pki-01: {}
infra-edge-01: {}
infra-mail-01: {}
infra-dns-01: {}
idm-01: {}
data-01: {}
obs-01: {}
@ -60,6 +63,7 @@ all:
infra-pki-01: {}
infra-edge-01: {}
infra-mail-01: {}
infra-dns-01: {}
idm-01: {}
data-01: {}
obs-01: {}
@ -104,6 +108,9 @@ all:
serveurs_step_ca:
hosts:
infra-pki-01: {}
serveurs_powerdns:
hosts:
infra-dns-01: {}
serveurs_redis:
hosts:
data-01: {}

View file

@ -0,0 +1,10 @@
---
- name: Appliquer le groupe serveurs_powerdns
hosts: serveurs_powerdns
become: true
gather_facts: true
tasks:
- name: Indiquer que le service PowerDNS reste à définir
ansible.builtin.debug:
msg: "Aucun rôle n'est encore associé au groupe serveurs_powerdns."

View file

@ -15,6 +15,7 @@ from inventory_rules import (
GROUPE_HOTES_ACTIFS,
GROUPE_HOTES_PLANIFIES,
GROUPES_ETAT_HOTE,
charger_dependances,
est_groupe_operationnel,
groupes_operationnels_connus,
)
@ -22,6 +23,7 @@ from inventory_rules import (
RACINE = Path(__file__).resolve().parents[1]
INVENTAIRE_DEFAUT = RACINE / "inventories/production/hosts.yml"
DOSSIER_PLAYBOOKS_GROUPES = RACINE / "playbooks/groupes"
FICHIER_DEPENDANCES = RACINE / "docs/dependances-groupes.yml"
def charger_yaml(path: Path) -> dict:
@ -99,9 +101,11 @@ def liste_hotes(data: dict) -> list[dict]:
def inventaire_api(path: Path) -> dict:
data = charger_yaml(path)
dependencies = charger_dependances(FICHIER_DEPENDANCES)
return {
"inventaire": str(path.relative_to(RACINE)),
"groupes": groupes_disponibles(data),
"dependances": dependencies,
"hotes": liste_hotes(data),
}
@ -145,7 +149,30 @@ def construire_inventaire(payload: dict, groupes_connus: list[str]) -> dict:
return {"all": {"children": enfants_data}}
def valider_payload(payload: dict, groupes_connus: list[str]) -> None:
def valider_dependances_payload(payload: dict, dependencies: dict) -> None:
hotes = payload.get("hotes", [])
active_groups: set[str] = set()
for hote in hotes:
if hote.get("etat") != "actif":
continue
active_groups.update(hote.get("groupes", []))
missing: list[str] = []
for hote in hotes:
if hote.get("etat") != "actif":
continue
nom = str(hote.get("nom", "")).strip()
for groupe in hote.get("groupes", []):
for required_group in dependencies.get(groupe, {}).get("requiert_groupes_actifs", []):
if required_group not in active_groups:
missing.append(f"{nom}: {groupe} requiert {required_group} actif")
if missing:
raise ValueError("Dependances manquantes: " + "; ".join(missing))
def valider_payload(payload: dict, groupes_connus: list[str], dependencies: dict) -> None:
noms: set[str] = set()
vmids: dict[str, str] = {}
for hote in payload.get("hotes", []):
@ -170,6 +197,8 @@ def valider_payload(payload: dict, groupes_connus: list[str]) -> None:
raise ValueError(f"VMID en double: {vmid} pour {vmids[vmid]} et {nom}")
vmids[vmid] = nom
valider_dependances_payload(payload, dependencies)
HTML = r"""<!doctype html>
<html lang="fr">
@ -242,6 +271,7 @@ HTML = r"""<!doctype html>
</main>
<script>
let groupes = [];
let dependances = {};
let hotes = [];
function message(texte, erreur=false) {
@ -254,6 +284,7 @@ HTML = r"""<!doctype html>
const reponse = await fetch('/api/inventaire');
const data = await reponse.json();
groupes = data.groupes;
dependances = data.dependances || {};
hotes = data.hotes;
document.getElementById('inventaire').textContent = data.inventaire;
dessiner();
@ -295,7 +326,7 @@ HTML = r"""<!doctype html>
<td><input value="${echapper(hote.utilisateur_ansible || '')}" oninput="definir(${index}, 'utilisateur_ansible', this.value)"></td>
<td class="vmid"><input value="${echapper(String(hote.vmid || ''))}" oninput="definir(${index}, 'vmid', this.value)"></td>
<td class="groupes"><div class="groupes-liste">${groupes.map(groupe => `
<label><input type="checkbox" ${(hote.groupes || []).includes(groupe) ? 'checked' : ''} onchange="basculerGroupe(${index}, '${groupe}', this.checked)"> ${groupe}</label>
<label title="${echapper(titreGroupe(groupe))}"><input type="checkbox" ${(hote.groupes || []).includes(groupe) ? 'checked' : ''} onchange="basculerGroupe(${index}, '${groupe}', this.checked)"> ${groupe}</label>
`).join('')}</div></td>
<td><button type="button" class="danger" onclick="supprimerHote(${index})">Retirer</button></td>
`;
@ -307,6 +338,13 @@ HTML = r"""<!doctype html>
return valeur.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function titreGroupe(groupe) {
const config = dependances[groupe] || {};
const requis = config.requiert_groupes_actifs || [];
if (!requis.length) return 'Aucun prérequis actif déclaré.';
return `Prérequis actif: ${requis.join(', ')}`;
}
async function sauvegarder() {
const reponse = await fetch('/api/inventaire', {
method: 'POST',
@ -318,6 +356,7 @@ HTML = r"""<!doctype html>
message(data.erreur || 'Sauvegarde refusée.', true);
return;
}
dependances = data.dependances || {};
groupes = data.groupes;
hotes = data.hotes;
dessiner();
@ -366,7 +405,8 @@ class Gestionnaire(BaseHTTPRequestHandler):
payload = json.loads(self.rfile.read(longueur).decode("utf-8"))
courant = charger_yaml(self.inventaire)
groupes_connus = groupes_disponibles(courant)
valider_payload(payload, groupes_connus)
dependencies = charger_dependances(FICHIER_DEPENDANCES)
valider_payload(payload, groupes_connus, dependencies)
nouveau = construire_inventaire(payload, groupes_connus)
ecrire_yaml(self.inventaire, nouveau)
self.repondre_json(200, inventaire_api(self.inventaire))

View file

@ -13,6 +13,7 @@ from inventory_rules import (
GROUPE_HOTES_ACTIFS,
GROUPE_HOTES_PLANIFIES,
GROUPES_ETAT_HOTE,
charger_dependances,
est_groupe_operationnel,
)
@ -181,6 +182,58 @@ def verifier_hote_actif(data: dict, host: str) -> None:
raise ValueError(f"Hote non actif dans l'inventaire: {host}")
def groupe_a_hote_actif(data: dict, group: str) -> bool:
active_hosts = set(enfants(data).get(GROUPE_HOTES_ACTIFS, {}).get("hosts", {}))
group_hosts = set(enfants(data).get(group, {}).get("hosts", {}))
return bool(active_hosts & group_hosts)
def verifier_dependances_groupes(data: dict, dependencies: dict, groups: list[str]) -> None:
missing: list[str] = []
for group in sorted(set(groups)):
for required_group in dependencies.get(group, {}).get("requiert_groupes_actifs", []):
if not groupe_a_hote_actif(data, required_group):
missing.append(f"{group} requiert {required_group} actif")
if missing:
raise ValueError("Dependances manquantes: " + "; ".join(missing))
def exiger_dependances(dependencies: dict, path: Path | None) -> None:
if not path:
raise ValueError("Fichier de dependances requis: utiliser --dependances.")
if not dependencies:
raise ValueError(f"Aucune dependance chargee depuis {path}.")
def verifier_dependances_hote(data: dict, dependencies: dict, host: str) -> None:
groups = [group for group in groupes_hote(data, host) if est_groupe_operationnel(group)]
verifier_dependances_groupes(data, dependencies, groups)
def verifier_dependances_actives(data: dict, dependencies: dict) -> None:
active_hosts = set(enfants(data).get(GROUPE_HOTES_ACTIFS, {}).get("hosts", {}))
active_groups: set[str] = set()
for group, group_data in enfants(data).items():
if not est_groupe_operationnel(group):
continue
if active_hosts & set(group_data.get("hosts", {})):
active_groups.add(group)
verifier_dependances_groupes(data, dependencies, sorted(active_groups))
def verifier_dependances_connues(data: dict, dependencies: dict, playbook_dir: Path | None) -> None:
for group, config in dependencies.items():
if playbook_dir and not (playbook_dir / f"{group}.yml").exists():
raise ValueError(f"Playbook de groupe manquant pour dependance: {group}")
for required_group in config.get("requiert_groupes_actifs", []):
if playbook_dir and not (playbook_dir / f"{required_group}.yml").exists():
raise ValueError(f"Playbook de groupe manquant pour prerequis: {required_group}")
def verifier_coherence_hotes(data: dict) -> None:
vmids: dict[str, str] = {}
@ -234,6 +287,7 @@ def verifier_playbooks_groupes(data: dict, playbook_dir: Path) -> None:
def main() -> int:
parser = argparse.ArgumentParser(description="Gere les hotes d'un inventaire Ansible statique Set-OPS.")
parser.add_argument("--inventaire", required=True, type=Path)
parser.add_argument("--dependances", type=Path)
subparsers = parser.add_subparsers(dest="command", required=True)
@ -260,16 +314,35 @@ def main() -> int:
active_parser = subparsers.add_parser("verifier-actif", help="Verifie qu'un hote est actif.")
active_parser.add_argument("--hote", required=True)
host_dependencies_parser = subparsers.add_parser(
"verifier-dependances-hote",
help="Verifie les prerequis actifs requis par les groupes d'un hote.",
)
host_dependencies_parser.add_argument("--hote", required=True)
group_dependencies_parser = subparsers.add_parser(
"verifier-dependances-groupe",
help="Verifie les prerequis actifs requis par un groupe.",
)
group_dependencies_parser.add_argument("--groupe", required=True)
verify_parser = subparsers.add_parser(
"verifier-playbooks",
help="Verifie que les groupes operationnels avec hotes ont un playbook homonyme.",
)
verify_parser.add_argument("--dossier-playbooks", required=True, type=Path)
dependencies_parser = subparsers.add_parser(
"verifier-dependances",
help="Verifie le registre de dependances et les prerequis des groupes actifs.",
)
dependencies_parser.add_argument("--dossier-playbooks", required=True, type=Path)
args = parser.parse_args()
try:
data = charger_inventaire(args.inventaire)
dependencies = charger_dependances(args.dependances)
if args.command == "ajouter":
ajouter_hote(
@ -282,11 +355,17 @@ def main() -> int:
)
if args.dossier_playbooks:
verifier_playbooks_groupes(data, args.dossier_playbooks)
if args.dependances:
verifier_dependances_connues(data, dependencies, args.dossier_playbooks)
verifier_dependances_actives(data, dependencies)
ecrire_inventaire(args.inventaire, data)
elif args.command == "definir-groupes":
definir_groupes_hote(data, args.hote, analyser_groupes(args.groupes))
if args.dossier_playbooks:
verifier_playbooks_groupes(data, args.dossier_playbooks)
if args.dependances:
verifier_dependances_connues(data, dependencies, args.dossier_playbooks)
verifier_dependances_actives(data, dependencies)
ecrire_inventaire(args.inventaire, data)
elif args.command == "afficher":
afficher_hote(data, args.hote)
@ -294,8 +373,18 @@ def main() -> int:
afficher_playbooks_hote(data, args.hote, args.dossier_playbooks)
elif args.command == "verifier-actif":
verifier_hote_actif(data, args.hote)
elif args.command == "verifier-dependances-hote":
exiger_dependances(dependencies, args.dependances)
verifier_dependances_hote(data, dependencies, args.hote)
elif args.command == "verifier-dependances-groupe":
exiger_dependances(dependencies, args.dependances)
verifier_dependances_groupes(data, dependencies, [args.groupe])
elif args.command == "verifier-playbooks":
verifier_playbooks_groupes(data, args.dossier_playbooks)
elif args.command == "verifier-dependances":
exiger_dependances(dependencies, args.dependances)
verifier_dependances_connues(data, dependencies, args.dossier_playbooks)
verifier_dependances_actives(data, dependencies)
except Exception as exc:
print(f"erreur: {exc}", file=sys.stderr)
return 2

View file

@ -5,6 +5,8 @@ from __future__ import annotations
from pathlib import Path
import yaml
GROUPES_OPERATIONNELS_PREFIXES = ("serveurs_", "clients_")
GROUPE_HOTES_ACTIFS = "hotes_actifs"
@ -20,3 +22,36 @@ def groupes_operationnels_connus(enfants: dict, dossier_playbooks: Path) -> list
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))
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