Set-OPS-Public/scripts/config_proxmox.py
Daniel Allaire 77809477b2 gabarit : modeleChezlepro -> modeleSetOPS, et une porte pour l'hebergeur
Le gabarit portait le nom du mauvais proprietaire. Les TROIS instances
(Chezlepro, Technolibre, lab) le declaraient et pointaient deja toutes sur le
MEME VMID 99998 — alors que le commentaire affirmait « chaque tenant a SON
golden template ». Faux depuis longtemps, et invisible en lisant un seul fichier.

Renomme sur le cluster et dans les trois instances. Le gabarit est un artefact du
MOTEUR, pas d'un tenant ; le nom d'un tenant sur le gabarit d'un autre etait un
piege qui n'attendait qu'un troisieme hebergeur. model_creer.py et
config_proxmox.py proposaient encore modele-debian13 : alignes.

Sans risque : le clonage se fait par VMID depuis le 2026-08-10, le nom ne sert
plus qu'a l'affichage.

NOUVELLE PORTE dans l'aiguillage : docs/preparer-un-site-hebergeur.md, pour
quelqu'un qui prete son materiel sans rien connaitre de Set-OPS. Ecrit a partir
du depot — VLAN et MTU d'underlay.yml, bloc par tenant de
inventory_rules.supernet_de(), privileges du jeton de config-proxmox.md,
dimensionnement (~460 Go, ~37 Go de RAM) mesure sur la flotte vivante.

Deux avertissements y figurent parce qu'ils ont deja coute cher ici : un gabarit
personnalise recopie son identite dans chaque clone, et un blocage contourne en
silence se paie en heures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:36:44 -04:00

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": "modeleSetOPS",
"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())