#!/usr/bin/env python3 """Gere les hotes d'un inventaire Ansible statique Set-OPS.""" from __future__ import annotations import argparse from pathlib import Path import sys import yaml from inventory_rules import ( GROUPE_HOTES_ACTIFS, GROUPE_HOTES_PLANIFIES, GROUPES_ETAT_HOTE, charger_dependances, est_groupe_operationnel, ) def charger_inventaire(path: Path) -> dict: if not path.exists(): return {"all": {"children": {}}} with path.open("r", encoding="utf-8") as inventory_file: data = yaml.safe_load(inventory_file) or {} if not isinstance(data, dict): raise ValueError(f"{path} ne contient pas une table YAML.") data.setdefault("all", {}) data["all"].setdefault("children", {}) return data def ecrire_inventaire(path: Path, data: dict) -> None: with path.open("w", encoding="utf-8") as inventory_file: yaml.safe_dump( data, inventory_file, default_flow_style=False, sort_keys=False, allow_unicode=True, ) def enfants(data: dict) -> dict: return data.setdefault("all", {}).setdefault("children", {}) def assurer_groupe(data: dict, group: str) -> dict: group_data = enfants(data).setdefault(group, {}) group_data.setdefault("hosts", {}) return group_data def variables_hote(data: dict, host: str) -> dict: merged: dict = {} for group_data in enfants(data).values(): hosts = group_data.get("hosts", {}) if host in hosts and isinstance(hosts[host], dict): merged.update(hosts[host]) 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(): if host in group_data.get("hosts", {}): groups.append(group) return groups def retirer_hote_de_tous_les_groupes(data: dict, host: str) -> None: for group_data in enfants(data).values(): group_data.get("hosts", {}).pop(host, 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) 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: vars_for_host = variables_hote(data, host) current_groups = groupes_hote(data, host) if not current_groups: raise ValueError(f"Hote absent de l'inventaire: {host}") retirer_hote_de_tous_les_groupes(data, host) for group in current_groups: if group in GROUPES_ETAT_HOTE: assurer_groupe(data, group)["hosts"][host] = dict(vars_for_host) for group in groups: assurer_groupe(data, group)["hosts"][host] = dict(vars_for_host) def analyser_groupes(raw_groups: str) -> list[str]: groups = [group.strip() for group in raw_groups.replace(",", " ").split() if group.strip()] if not groups: raise ValueError("Au moins un groupe est requis.") return groups def afficher_hote(data: dict, host: str) -> None: groups = groupes_hote(data, host) if not groups: raise ValueError(f"Hote absent de l'inventaire: {host}") yaml.safe_dump( { "hote": host, "groupes": groups, "variables": variables_hote(data, host), }, sys.stdout, default_flow_style=False, sort_keys=False, allow_unicode=True, ) def afficher_playbooks_hote(data: dict, host: str, playbook_dir: Path) -> None: groups = groupes_hote(data, host) if not groups: raise ValueError(f"Hote absent de l'inventaire: {host}") missing_playbooks: list[str] = [] playbooks: list[Path] = [] for group in groups: if not est_groupe_operationnel(group): continue playbook = playbook_dir / f"{group}.yml" if playbook.exists(): playbooks.append(playbook) else: missing_playbooks.append(group) if missing_playbooks: missing = ", ".join(missing_playbooks) raise ValueError(f"Playbook de groupe manquant pour: {missing}") if not playbooks: raise ValueError(f"Aucun playbook de groupe opérationnel pour: {host}") for playbook in playbooks: print(playbook) def verifier_hote_actif(data: dict, host: str) -> None: hosts = enfants(data).get(GROUPE_HOTES_ACTIFS, {}).get("hosts", {}) if host not in hosts: 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] = {} 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] = [] for group, group_data in enfants(data).items(): hosts = group_data.get("hosts", {}) if not hosts: continue if not est_groupe_operationnel(group): continue playbook = playbook_dir / f"{group}.yml" if not playbook.exists(): missing_playbooks.append(group) if missing_playbooks: 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.") parser.add_argument("--inventaire", required=True, type=Path) parser.add_argument("--dependances", type=Path) subparsers = parser.add_subparsers(dest="command", required=True) add_parser = subparsers.add_parser("ajouter", help="Ajoute ou met a jour un hote dans un ou plusieurs groupes.") add_parser.add_argument("--hote", required=True) 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.") groups_parser.add_argument("--hote", required=True) groups_parser.add_argument("--groupes", required=True) groups_parser.add_argument("--dossier-playbooks", type=Path) show_parser = subparsers.add_parser("afficher", help="Affiche les groupes et variables d'un hote.") show_parser.add_argument("--hote", required=True) playbooks_parser = subparsers.add_parser("playbooks", help="Affiche les playbooks de groupes pour un hote.") playbooks_parser.add_argument("--hote", required=True) playbooks_parser.add_argument("--dossier-playbooks", required=True, type=Path) 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( data, args.hote, analyser_groupes(args.groupes), args.adresse_ip, args.utilisateur_ansible, args.vmid, ) 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) elif args.command == "playbooks": 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 return 0 if __name__ == "__main__": raise SystemExit(main())