Some checks are pending
verifier / verifier (push) Waiting to run
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>
215 lines
8.3 KiB
Python
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())
|