Consolider le flux operationnel des inventaires

This commit is contained in:
Daniel Allaire 2026-06-21 20:47:44 -04:00
parent f941b66d39
commit 72c3ac6007
11 changed files with 180 additions and 42 deletions

View file

@ -256,7 +256,7 @@ Les cibles d'exploitation des VM doivent privilégier les groupes :
```text
make deployer HOTE=web-01
make deployer-groupe GROUPE=serveurs_debian
make appliquer GROUPE=serveurs_debian
make hote-planifier HOTE=obs-01 VMID=94101 GROUPES="serveurs_debian serveurs_durcis serveurs_prometheus"
```
Éviter les cibles parallèles qui réappliquent les mêmes rôles par couche, par exemple `make socle`, `make durcissement`, `make converger` ou `make deployer-vm`.
@ -336,7 +336,8 @@ clients_ldap
clients_supervision
clients_metriques
serveurs_web
serveurs_bases_donnees
serveurs_postgresql
serveurs_prometheus
```
Ne pas cibler `all` par défaut pour un playbook qui ne s'applique pas réellement à tous les hôtes.

View file

@ -1,5 +1,21 @@
# CHANGELOG — Set-OPS
## 2026-06-22
### 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.
### Modifié
- Renforcement de la validation d'inventaire :
- un hôte doit être soit planifié, soit actif, mais pas les deux ;
- un hôte actif doit avoir `ansible_host` ;
- un hôte doit porter au moins un groupe opérationnel ;
- les VMID dupliqués sont refusés.
- Conservation du VMID dans l'inventaire lors de `make creer-vm` et `make hote-ajouter`.
- 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.
## 2026-06-21
### Ajouté

View file

@ -95,12 +95,14 @@ aide:
@printf '%s\n' ' make config'
@printf '%s\n' ''
@printf '%s\n' 'Hotes'
@printf '%s\n' ' Planifier un hote sans le deployer:'
@printf '%s\n' ' make hote-planifier HOTE=obs-01 VMID=94101 GROUPES="serveurs_debian serveurs_durcis serveurs_prometheus serveurs_loki"'
@printf '%s\n' ' Remettre un hote en conformite selon ses groupes:'
@printf '%s\n' ' make deployer HOTE=web-01'
@printf '%s\n' ' Afficher un hote:'
@printf '%s\n' ' make hote-afficher HOTE=web-01'
@printf '%s\n' ' Ajouter un hote existant:'
@printf '%s\n' ' make hote-ajouter HOTE=web-01 ADRESSE_IP=192.168.12.101 GROUPES="serveurs_debian serveurs_durcis"'
@printf '%s\n' ' make hote-ajouter HOTE=web-01 VMID=99101 ADRESSE_IP=192.168.12.101 GROUPES="serveurs_debian serveurs_durcis"'
@printf '%s\n' ' Modifier ses groupes:'
@printf '%s\n' ' make hote-groupes HOTE=web-01 GROUPES="serveurs_debian serveurs_durcis"'
@printf '%s\n' ' Diagnostiquer:'
@ -109,8 +111,6 @@ aide:
@printf '%s\n' 'Groupes'
@printf '%s\n' ' Appliquer un groupe complet:'
@printf '%s\n' ' make deployer-groupe GROUPE=serveurs_debian'
@printf '%s\n' ' Rejouer directement le playbook d un groupe:'
@printf '%s\n' ' make appliquer GROUPE=serveurs_debian'
@printf '%s\n' ' Convention:'
@printf '%s\n' ' groupe serveurs_debian -> playbooks/groupes/serveurs_debian.yml'
@printf '%s\n' ''
@ -173,7 +173,7 @@ syntaxe-proxmox: ansible-runtime
.PHONY: verifier
verifier: lint inventaire-verifier syntaxe
.PHONY: inventaire hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production
.PHONY: inventaire hote-planifier hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production
inventaire: inventaire-production
config:
@ -187,7 +187,16 @@ 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) --groupes "$(GROUPES)" --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
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)
$(MAKE) inventaire-verifier
$(MAKE) hote-afficher HOTE="$(HOTE)" FICHIER_INVENTAIRE="$(FICHIER_INVENTAIRE)"
hote-planifier: ansible-runtime
@if [[ -z "$(HOTE)" ]]; then \
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)
$(MAKE) inventaire-verifier
$(MAKE) hote-afficher HOTE="$(HOTE)" FICHIER_INVENTAIRE="$(FICHIER_INVENTAIRE)"
@ -307,7 +316,7 @@ creer-vm:
exit 2; \
fi
$(MAKE) cloner-vm HOTE="$(HOTE)" VMID_MODELE="$(VMID_MODELE)" VMID="$(VMID)" NOEUD_PROXMOX="$(NOEUD_PROXMOX)" STOCKAGE_PROXMOX="$(STOCKAGE_PROXMOX)" FORMAT_DISQUE="$(FORMAT_DISQUE)" TAILLE_DISQUE="$(TAILLE_DISQUE)" DISQUE_PROXMOX="$(DISQUE_PROXMOX)" ADRESSE_IP="$(ADRESSE_IP)" CIDR="$(CIDR)" PASSERELLE="$(PASSERELLE)" DNS="$(DNS)" DHCP="$(DHCP)" CIUSER="$(CIUSER)" CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" PONT_PROXMOX="$(PONT_PROXMOX)" VLAN="$(VLAN)" DEMARRER="$(DEMARRER)" CLONE_COMPLET="$(CLONE_COMPLET)"
$(MAKE) hote-ajouter HOTE="$(HOTE)" ADRESSE_IP="$(ADRESSE_IP)" GROUPES="$(GROUPES)" FICHIER_INVENTAIRE="$(FICHIER_INVENTAIRE)"
$(MAKE) hote-ajouter HOTE="$(HOTE)" ADRESSE_IP="$(ADRESSE_IP)" VMID="$(VMID)" GROUPES="$(GROUPES)" FICHIER_INVENTAIRE="$(FICHIER_INVENTAIRE)"
inventaire-verifier: ansible-runtime
ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null

View file

@ -66,10 +66,16 @@ Ouvrir l'interface locale de gestion d'inventaire :
make inventaire-ui
```
Planifier une VM sans la créer ni la déployer :
```bash
make hote-planifier HOTE=obs-01 VMID=94101 GROUPES="serveurs_debian serveurs_durcis serveurs_prometheus serveurs_loki"
```
Ajouter une VM à l'inventaire production et l'associer aux groupes qui détermineront sa configuration :
```bash
make hote-ajouter HOTE=web-01 ADRESSE_IP=192.168.12.101 GROUPES="serveurs_debian serveurs_durcis"
make hote-ajouter HOTE=web-01 VMID=99101 ADRESSE_IP=192.168.12.101 GROUPES="serveurs_debian serveurs_durcis"
make hote-groupes HOTE=web-01 GROUPES="serveurs_debian serveurs_durcis"
```
@ -117,3 +123,5 @@ Déployer ou remettre en conformité un groupe :
```bash
make deployer-groupe GROUPE=serveurs_debian
```
Les déploiements de groupes ciblent automatiquement les hôtes actifs seulement.

View file

@ -61,6 +61,18 @@ Les hôtes prévus mais non encore déployés sont placés dans `hotes_planifies
Les hôtes réellement joignables par Ansible sont placés dans `hotes_actifs`.
Planifier un hôte :
```bash
make hote-planifier HOTE=obs-01 VMID=94101 GROUPES="serveurs_debian serveurs_durcis serveurs_prometheus serveurs_loki"
```
Activer un hôte existant ou déjà cloné :
```bash
make hote-ajouter HOTE=obs-01 VMID=94101 ADRESSE_IP=192.168.12.141 GROUPES="serveurs_debian serveurs_durcis serveurs_prometheus serveurs_loki"
```
Les déploiements groupés limitent automatiquement l'exécution à :
```text

View file

@ -100,6 +100,10 @@ Cloud-init ne remplace pas Ansible pour la configuration réelle du serveur.
Une VM déployée doit être placée dans les groupes d'inventaire qui décrivent l'état voulu.
Les hôtes prévus mais non créés restent dans `hotes_planifies`.
Les hôtes joignables par Ansible sont dans `hotes_actifs`.
Chaque groupe opérationnel doit avoir un playbook homonyme :
```text
@ -114,14 +118,16 @@ web-01 dans serveurs_debian -> applique le socle Debian
web-01 dans serveurs_durcis -> applique le durcissement commun
```
Un groupe sans playbook ne doit pas être assigné à une VM de production tant que son rôle n'est pas implémenté.
Un groupe sans playbook ne doit pas être assigné à une VM de production.
Un playbook de groupe peut être un contrat temporaire sans rôle tant que le service n'est pas encore implémenté. Il doit rester idempotent et explicite.
## 6. Socle Debian
Le groupe `serveurs_debian` maintient les composants de base :
```bash
make appliquer GROUPE=serveurs_debian
make deployer-groupe GROUPE=serveurs_debian
```
## 7. Hardening commun
@ -129,7 +135,7 @@ make appliquer GROUPE=serveurs_debian
Le groupe `serveurs_durcis` maintient les couches de sécurité communes :
```bash
make appliquer GROUPE=serveurs_durcis
make deployer-groupe GROUPE=serveurs_durcis
```
L'accès SSH par mot de passe est désactivé dès le socle. L'activation de nftables doit rester dépendante des règles réseau propres au rôle du serveur.
@ -139,7 +145,7 @@ L'accès SSH par mot de passe est désactivé dès le socle. L'activation de nft
Pour une VM déjà clonée et démarrée :
```bash
make hote-ajouter HOTE=web-01 ADRESSE_IP=192.168.12.101 GROUPES="serveurs_debian serveurs_durcis"
make hote-ajouter HOTE=web-01 VMID=99101 ADRESSE_IP=192.168.12.101 GROUPES="serveurs_debian serveurs_durcis"
make deployer HOTE=web-01
```
@ -159,6 +165,8 @@ Pour converger un groupe complet :
make deployer-groupe GROUPE=serveurs_debian
```
Cette commande limite le playbook au croisement entre le groupe demandé et `hotes_actifs`.
## 9. Variables template et conformité
Les variables du template servent à construire le golden template dans `inventories/lab/group_vars/modeles_vm.yml`.

View file

@ -16,11 +16,5 @@ all:
serveurs_web:
hosts: {}
serveurs_bases_donnees:
hosts: {}
serveurs_supervision:
hosts: {}
hotes_proxmox:
hosts: {}

View file

@ -74,10 +74,6 @@ all:
web-02:
ansible_host: 192.168.12.102
ansible_user: ansible
serveurs_bases_donnees:
hosts: {}
serveurs_supervision:
hosts: {}
serveurs_keycloak:
hosts:
idm-01: {}

View file

@ -11,12 +11,17 @@ from urllib.parse import urlparse
import yaml
from inventory_rules import (
GROUPE_HOTES_ACTIFS,
GROUPE_HOTES_PLANIFIES,
GROUPES_ETAT_HOTE,
est_groupe_operationnel,
groupes_operationnels_connus,
)
RACINE = Path(__file__).resolve().parents[1]
INVENTAIRE_DEFAUT = RACINE / "inventories/production/hosts.yml"
DOSSIER_PLAYBOOKS_GROUPES = RACINE / "playbooks/groupes"
GROUPES_OPERATIONNELS_PREFIXES = ("serveurs_", "clients_")
GROUPES_ETAT = ("hotes_actifs", "hotes_planifies")
def charger_yaml(path: Path) -> dict:
@ -41,9 +46,7 @@ def enfants(data: dict) -> dict:
def groupes_disponibles(data: dict) -> list[str]:
groupes = set(enfants(data))
groupes.update(playbook.stem for playbook in DOSSIER_PLAYBOOKS_GROUPES.glob("*.yml"))
return sorted(group for group in groupes if group.startswith(GROUPES_OPERATIONNELS_PREFIXES))
return groupes_operationnels_connus(enfants(data), DOSSIER_PLAYBOOKS_GROUPES)
def variables_hote(data: dict, hote: str) -> dict:
@ -65,15 +68,21 @@ def liste_hotes(data: dict) -> list[dict]:
for nom in sorted(noms):
groupes = []
etat = "planifie"
visible = False
for groupe, groupe_data in enfants(data).items():
if nom not in groupe_data.get("hosts", {}):
continue
if groupe == "hotes_actifs":
if groupe == GROUPE_HOTES_ACTIFS:
etat = "actif"
elif groupe in GROUPES_ETAT:
visible = True
elif groupe in GROUPES_ETAT_HOTE:
visible = True
continue
elif groupe.startswith(GROUPES_OPERATIONNELS_PREFIXES):
elif est_groupe_operationnel(groupe):
groupes.append(groupe)
visible = True
if not visible:
continue
variables = variables_hote(data, nom)
hotes.append(
{
@ -114,8 +123,8 @@ def donnees_hote(hote: dict) -> dict:
def construire_inventaire(payload: dict, groupes_connus: list[str]) -> dict:
enfants_data: dict = {
"modeles_vm": {"hosts": {}},
"hotes_actifs": {"hosts": {}},
"hotes_planifies": {"hosts": {}},
GROUPE_HOTES_ACTIFS: {"hosts": {}},
GROUPE_HOTES_PLANIFIES: {"hosts": {}},
}
for groupe in groupes_connus:
enfants_data.setdefault(groupe, {"hosts": {}})
@ -126,7 +135,7 @@ def construire_inventaire(payload: dict, groupes_connus: list[str]) -> dict:
if not nom:
continue
variables = donnees_hote(hote)
groupe_etat = "hotes_actifs" if hote.get("etat") == "actif" else "hotes_planifies"
groupe_etat = GROUPE_HOTES_ACTIFS if hote.get("etat") == "actif" else GROUPE_HOTES_PLANIFIES
enfants_data[groupe_etat]["hosts"][nom] = variables
for groupe in hote.get("groupes", []):
if groupe not in groupes_connus:
@ -138,6 +147,7 @@ def construire_inventaire(payload: dict, groupes_connus: list[str]) -> dict:
def valider_payload(payload: dict, groupes_connus: list[str]) -> None:
noms: set[str] = set()
vmids: dict[str, str] = {}
for hote in payload.get("hotes", []):
nom = str(hote.get("nom", "")).strip()
if not nom:
@ -145,12 +155,20 @@ def valider_payload(payload: dict, groupes_connus: list[str]) -> None:
if nom in noms:
raise ValueError(f"Hote en double: {nom}")
noms.add(nom)
if hote.get("etat") == "actif" and not str(hote.get("adresse_ip", "")).strip():
raise ValueError(f"Hote actif sans adresse IP: {nom}")
groupes = hote.get("groupes", [])
if not groupes:
raise ValueError(f"Aucun groupe operationnel pour: {nom}")
inconnus = sorted(set(groupes) - set(groupes_connus))
if inconnus:
raise ValueError(f"Groupes inconnus pour {nom}: {', '.join(inconnus)}")
vmid = str(hote.get("vmid", "")).strip()
if not vmid:
continue
if vmid in vmids:
raise ValueError(f"VMID en double: {vmid} pour {vmids[vmid]} et {nom}")
vmids[vmid] = nom
HTML = r"""<!doctype html>

View file

@ -9,11 +9,12 @@ import sys
import yaml
GROUPES_OPERATIONNELS_PREFIXES = ("serveurs_", "clients_")
GROUPE_HOTES_ACTIFS = "hotes_actifs"
GROUPE_HOTES_PLANIFIES = "hotes_planifies"
GROUPES_ETAT_HOTE = {GROUPE_HOTES_ACTIFS, GROUPE_HOTES_PLANIFIES}
from inventory_rules import (
GROUPE_HOTES_ACTIFS,
GROUPE_HOTES_PLANIFIES,
GROUPES_ETAT_HOTE,
est_groupe_operationnel,
)
def charger_inventaire(path: Path) -> dict:
@ -61,6 +62,13 @@ def variables_hote(data: dict, host: str) -> dict:
return merged
def tous_les_hotes(data: dict) -> list[str]:
hosts: set[str] = set()
for group_data in enfants(data).values():
hosts.update(group_data.get("hosts", {}))
return sorted(hosts)
def groupes_hote(data: dict, host: str) -> list[str]:
groups: list[str] = []
for group, group_data in enfants(data).items():
@ -74,12 +82,21 @@ def retirer_hote_de_tous_les_groupes(data: dict, host: str) -> None:
group_data.get("hosts", {}).pop(host, None)
def ajouter_hote(data: dict, host: str, groups: list[str], ip: str | None, ansible_user: str | None) -> None:
def ajouter_hote(
data: dict,
host: str,
groups: list[str],
ip: str | None,
ansible_user: str | None,
vmid: str | None,
) -> None:
vars_for_host = variables_hote(data, host)
if ip:
vars_for_host["ansible_host"] = ip
if ansible_user:
vars_for_host["ansible_user"] = ansible_user
if vmid:
vars_for_host["proxmox_vmid"] = int(vmid) if vmid.isdigit() else vmid
for group in groups:
assurer_groupe(data, group)["hosts"][host] = dict(vars_for_host)
@ -87,6 +104,9 @@ def ajouter_hote(data: dict, host: str, groups: list[str], ip: str | None, ansib
if ip:
assurer_groupe(data, GROUPE_HOTES_ACTIFS)["hosts"][host] = dict(vars_for_host)
assurer_groupe(data, GROUPE_HOTES_PLANIFIES)["hosts"].pop(host, None)
else:
assurer_groupe(data, GROUPE_HOTES_PLANIFIES)["hosts"][host] = dict(vars_for_host)
assurer_groupe(data, GROUPE_HOTES_ACTIFS)["hosts"].pop(host, None)
def definir_groupes_hote(data: dict, host: str, groups: list[str]) -> None:
@ -136,7 +156,7 @@ def afficher_playbooks_hote(data: dict, host: str, playbook_dir: Path) -> None:
playbooks: list[Path] = []
for group in groups:
if not group.startswith(GROUPES_OPERATIONNELS_PREFIXES):
if not est_groupe_operationnel(group):
continue
playbook = playbook_dir / f"{group}.yml"
if playbook.exists():
@ -161,6 +181,36 @@ def verifier_hote_actif(data: dict, host: str) -> None:
raise ValueError(f"Hote non actif dans l'inventaire: {host}")
def verifier_coherence_hotes(data: dict) -> None:
vmids: dict[str, str] = {}
for host in tous_les_hotes(data):
groups = groupes_hote(data, host)
state_groups = [group for group in groups if group in GROUPES_ETAT_HOTE]
operational_groups = [group for group in groups if est_groupe_operationnel(group)]
vars_for_host = variables_hote(data, host)
if not state_groups and not operational_groups:
continue
if len(state_groups) != 1:
raise ValueError(
f"{host} doit appartenir exactement a un groupe d'etat: "
f"{GROUPE_HOTES_ACTIFS} ou {GROUPE_HOTES_PLANIFIES}"
)
if not operational_groups:
raise ValueError(f"{host} doit appartenir a au moins un groupe operationnel.")
if GROUPE_HOTES_ACTIFS in state_groups and not vars_for_host.get("ansible_host"):
raise ValueError(f"{host} est actif mais n'a pas ansible_host.")
vmid = vars_for_host.get("proxmox_vmid")
if vmid in (None, ""):
continue
vmid_key = str(vmid)
if vmid_key in vmids and vmids[vmid_key] != host:
raise ValueError(f"VMID en double: {vmid_key} pour {vmids[vmid_key]} et {host}")
vmids[vmid_key] = host
def verifier_playbooks_groupes(data: dict, playbook_dir: Path) -> None:
missing_playbooks: list[str] = []
@ -168,7 +218,7 @@ def verifier_playbooks_groupes(data: dict, playbook_dir: Path) -> None:
hosts = group_data.get("hosts", {})
if not hosts:
continue
if not group.startswith(GROUPES_OPERATIONNELS_PREFIXES):
if not est_groupe_operationnel(group):
continue
playbook = playbook_dir / f"{group}.yml"
if not playbook.exists():
@ -178,6 +228,8 @@ def verifier_playbooks_groupes(data: dict, playbook_dir: Path) -> None:
missing = ", ".join(sorted(missing_playbooks))
raise ValueError(f"Playbook de groupe manquant pour: {missing}")
verifier_coherence_hotes(data)
def main() -> int:
parser = argparse.ArgumentParser(description="Gere les hotes d'un inventaire Ansible statique Set-OPS.")
@ -190,6 +242,7 @@ def main() -> int:
add_parser.add_argument("--groupes", required=True)
add_parser.add_argument("--adresse-ip")
add_parser.add_argument("--utilisateur-ansible")
add_parser.add_argument("--vmid")
add_parser.add_argument("--dossier-playbooks", type=Path)
groups_parser = subparsers.add_parser("definir-groupes", help="Remplace l'appartenance aux groupes pour un hote.")
@ -225,6 +278,7 @@ def main() -> int:
analyser_groupes(args.groupes),
args.adresse_ip,
args.utilisateur_ansible,
args.vmid,
)
if args.dossier_playbooks:
verifier_playbooks_groupes(data, args.dossier_playbooks)

View file

@ -0,0 +1,22 @@
#!/usr/bin/env python3
"""Regles partagees pour les inventaires statiques Set-OPS."""
from __future__ import annotations
from pathlib import Path
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))