Set-OPS-Public/scripts/config_proxmox.py
Daniel Allaire c18debc25e
Some checks are pending
verifier / verifier (push) Waiting to run
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.

- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
  _frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.

Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.

LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.

CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.

P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.

make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00

215 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
from inventory_rules import instance_courante, inventaire_force
RACINE = Path(__file__).resolve().parents[1]
INSTANCE = instance_courante()
def _inventaire_dir(instance: Path, *noms: str) -> Path:
"""Répertoire de l'inventaire modèle (porte la config Proxmox / le clonage)."""
forced = inventaire_force(instance) # refuse une cible hors de `instance`
if forced:
return 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())