Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants. 1. Intégrations universelles (D-33/D-34, P26) Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01 n'étaient ni supervisés, ni journalisés, ni certifiés. Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le plan ne garde que les vrais choix et refuse la recopie. Les exemptions se dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations universelles auraient cessé d'être exigés et P18 serait passé au vert sur une voûte incomplète. Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41 lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas client_pki sur infra-pki-01. 2. Vue Intégrations : la matrice La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x intégrations : colonnes de politique en lecture seule, facultatives cochables sur place, ligne de couverture n/N qui rend le motif visible sans le juger. 3. Propriété des intrants (D-35/D-36, P27) Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière. Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive — l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et ses défauts de placement. Le panneau nomme désormais le propriétaire de chaque section : éditer une section « hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas. 26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
213 lines
8.3 KiB
Python
213 lines
8.3 KiB
Python
#!/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"
|
|
# Le CLUSTER appartient a l'HEBERGEUR : son fichier vit dans le depot que le symlink
|
|
# `underlay.yml` designe deja (D-14/D-17). Recopiees chez chaque tenant, ces cles
|
|
# avaient deja diverge. Repli sur le fichier du tenant si aucun underlay n'est monte
|
|
# (depot autonome) — `make config` reste utilisable tel quel.
|
|
_UNDERLAY = Path(os.environ.get("SETOPS_UNDERLAY") or (RACINE / "underlay.yml"))
|
|
FICHIER_PROXMOX_HEBERGEUR = (_UNDERLAY.resolve().parent / "proxmox-hebergeur.yml"
|
|
if _UNDERLAY.exists() else FICHIER_PROXMOX)
|
|
# Cles ecrites chez l'HEBERGEUR ; tout le reste (modele, defauts de placement) au tenant.
|
|
CLES_HEBERGEUR = {"proxmox_api_host", "proxmox_api_user", "proxmox_api_port",
|
|
"proxmox_validate_certs"}
|
|
# 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:
|
|
# Deux fichiers, deux proprietaires : on fusionne pour AFFICHER l'etat courant,
|
|
# on separe pour ECRIRE. Sans quoi `make config` reecrirait chez le tenant les
|
|
# cles du cluster, et la recopie recommencerait a la premiere execution.
|
|
courant = (VALEURS_DEFAUT | charger_yaml(FICHIER_PROXMOX)
|
|
| charger_yaml(FICHIER_PROXMOX_HEBERGEUR))
|
|
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]))
|
|
|
|
separe = FICHIER_PROXMOX_HEBERGEUR != FICHIER_PROXMOX
|
|
tenant = {k: v for k, v in nouveau.items() if not (separe and k in CLES_HEBERGEUR)}
|
|
ecrire_yaml(FICHIER_PROXMOX, tenant)
|
|
print(f"\nEcrit: {os.path.relpath(FICHIER_PROXMOX, RACINE)}")
|
|
if separe:
|
|
# Fusion : preserve les catalogues (noeuds/stockages/ponts) que ce script ne
|
|
# demande pas mais que le panneau ecrit dans le meme fichier.
|
|
heb = charger_yaml(FICHIER_PROXMOX_HEBERGEUR)
|
|
heb.update({k: nouveau[k] for k in CLES_HEBERGEUR if k in nouveau})
|
|
ecrire_yaml(FICHIER_PROXMOX_HEBERGEUR, heb)
|
|
print(f"Ecrit: {FICHIER_PROXMOX_HEBERGEUR} (HEBERGEUR — vaut pour tous ses tenants)")
|
|
|
|
|
|
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())
|