#!/usr/bin/env python3 """Assistant de configuration Proxmox pour Set-OPS.""" from __future__ import annotations from getpass import getpass from pathlib import Path import os import subprocess import sys import yaml RACINE = Path(__file__).resolve().parents[1] INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance")) def _inventaire_dir(instance: Path, *noms: str) -> Path: """Répertoire de l'inventaire modèle (porte la config Proxmox / le clonage).""" forced = os.environ.get("SETOPS_INVENTAIRE") if forced: return Path(forced).parent for nom in noms: d = instance / "inventories" / nom if (d / "hosts.yml").exists(): return d return instance / "inventories" / noms[0] _INV_DIR = _inventaire_dir(INSTANCE, "lab", "principal", "production") FICHIER_PROXMOX = _INV_DIR / "group_vars/proxmox.yml" # Voûte UNIQUE de l'instance : tous les secrets (token Proxmox + vault_*) au même # endroit, un seul mot de passe. Gabarit : exemples/vault.exemple.yml. FICHIER_VAULT = _INV_DIR / "group_vars/all/vault.yml" GABARIT_VAULT = RACINE / "exemples/vault.exemple.yml" VALEURS_DEFAUT = { "proxmox_api_host": "", "proxmox_api_user": "", "proxmox_api_port": "", "proxmox_validate_certs": False, "proxmox_clone_noeud": "", "proxmox_clone_vmid_modele": 9000, "proxmox_clone_source_nom": "modele-debian13", "proxmox_clone_stockage": "", "proxmox_clone_format": "", "proxmox_clone_complet": True, "proxmox_clone_timeout": 600, "proxmox_clone_disque": "scsi0", "proxmox_clone_interface": "net0", "proxmox_clone_pont": "vmbr0", "proxmox_clone_parefeu_interface": False, "proxmox_clone_demarrer": True, } LIBELLES = { "proxmox_api_host": "Hote API Proxmox", "proxmox_api_user": "Utilisateur API Proxmox", "proxmox_api_port": "Port API Proxmox", "proxmox_validate_certs": "Valider les certificats TLS", "proxmox_clone_noeud": "Noeud Proxmox par defaut", "proxmox_clone_vmid_modele": "VMID du modele Debian 13", "proxmox_clone_source_nom": "Nom logique du modele", "proxmox_clone_stockage": "Stockage Proxmox par defaut", "proxmox_clone_format": "Format disque par defaut", "proxmox_clone_complet": "Clone complet", "proxmox_clone_timeout": "Timeout operations Proxmox", "proxmox_clone_disque": "Disque principal", "proxmox_clone_interface": "Interface reseau", "proxmox_clone_pont": "Pont Proxmox", "proxmox_clone_parefeu_interface": "Pare-feu interface Proxmox", "proxmox_clone_demarrer": "Demarrer le clone apres creation", } def charger_yaml(path: Path) -> dict: if 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 def ecrire_yaml(path: Path, data: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8") as fichier: fichier.write("---\n") fichier.write("# Parametres non sensibles pour les operations Proxmox.\n") fichier.write("# Les secrets vont dans la voûte unique group_vars/all/vault.yml.\n\n") yaml.safe_dump(data, fichier, default_flow_style=False, sort_keys=False, allow_unicode=True) def demander(identifiant: str, courant: object) -> object: libelle = LIBELLES[identifiant] if isinstance(courant, bool): defaut = "oui" if courant else "non" reponse = input(f"{libelle} [{defaut}] : ").strip().lower() if not reponse: return courant return reponse in {"o", "oui", "y", "yes", "true", "1"} reponse = input(f"{libelle} [{courant}] : ").strip() if not reponse: return courant if isinstance(courant, int): return int(reponse) return reponse def configurer_proxmox() -> None: courant = VALEURS_DEFAUT | charger_yaml(FICHIER_PROXMOX) nouveau = {} print("Configuration Proxmox non sensible") print("Entrer pour conserver la valeur entre crochets.\n") print("Les valeurs Cloud-Init deja portees par le modele ne sont pas redemandees.\n") for identifiant in VALEURS_DEFAUT: nouveau[identifiant] = demander(identifiant, courant.get(identifiant, VALEURS_DEFAUT[identifiant])) ecrire_yaml(FICHIER_PROXMOX, nouveau) print(f"\nEcrit: {os.path.relpath(FICHIER_PROXMOX, RACINE)}") def demander_oui_non(question: str, defaut: bool = False) -> bool: suffixe = "O/n" if defaut else "o/N" reponse = input(f"{question} [{suffixe}] : ").strip().lower() if not reponse: return defaut return reponse in {"o", "oui", "y", "yes", "true", "1"} def configurer_vault() -> None: if not demander_oui_non("Configurer la voûte de secrets maintenant"): return if FICHIER_VAULT.exists(): print(f"Ouverture de la voûte existante: {os.path.relpath(FICHIER_VAULT, RACINE)}") subprocess.run(["ansible-vault", "edit", str(FICHIER_VAULT)], check=True) return # Voûte absente : on la sème à partir du gabarit (toutes les clés vides), # on y place le token Proxmox saisi, puis on chiffre. Les autres secrets se # renseignent ensuite via `ansible-vault edit`. token_id = input("Token ID Proxmox [set-ops] : ").strip() or "set-ops" token_secret = getpass("Token secret Proxmox : ").strip() if not token_secret: print("Secret vide: voûte non créée.") return contenu = charger_yaml(GABARIT_VAULT) contenu["proxmox_api_token_id"] = token_id contenu["proxmox_api_token_secret"] = token_secret FICHIER_VAULT.parent.mkdir(parents=True, exist_ok=True) try: fd = os.open(FICHIER_VAULT, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as fichier: yaml.safe_dump(contenu, fichier, default_flow_style=False, sort_keys=False, allow_unicode=True) subprocess.run(["ansible-vault", "encrypt", str(FICHIER_VAULT)], check=True) print(f"Voûte créée: {os.path.relpath(FICHIER_VAULT, RACINE)}") print("Renseigne les autres secrets avec : ansible-vault edit " f"{os.path.relpath(FICHIER_VAULT, RACINE)}") except Exception: if FICHIER_VAULT.exists(): FICHIER_VAULT.unlink() raise def main() -> int: try: configurer_proxmox() configurer_vault() except KeyboardInterrupt: print("\nConfiguration interrompue.", file=sys.stderr) return 130 except Exception as exc: print(f"erreur: {exc}", file=sys.stderr) return 2 return 0 if __name__ == "__main__": raise SystemExit(main())