Homelab/scripts/manage_linstor_resources.py
Tellsanguis 27afd8dad9
Some checks failed
CD - Deploy Infrastructure / Terraform Validation (push) Successful in 16s
CD - Deploy Infrastructure / Deploy on pve1 (push) Failing after 8s
CD - Deploy Infrastructure / Deploy on pve2 (push) Failing after 7s
CD - Deploy Infrastructure / Deploy on pve3 (push) Failing after 7s
CD - Deploy Infrastructure / Validate K3s Cluster (push) Has been skipped
CD - Deploy Infrastructure / Deployment Notification (push) Failing after 1s
feat(cicd): Ajouter gestion automatique des ressources DRBD Linstor
- Créer script Python pour gérer les ressources DRBD avant déploiement
  * Vérifie l'existence des ressources Linstor
  * Crée les ressources si nécessaire avec réplication
  * Augmente la taille si elle est insuffisante
  * Noms fixes: pm-a7f3c8e1 (VMID 1000) et pm-b4d2f9a3 (VMID 1001)

- Modifier workflow CI/CD pour intégrer le script Python
  * Ajouter étape de configuration SSH avec secret LINSTOR_SSH_PRIVATE_KEY
  * Exécuter le script avant tofu apply sur pve1 et pve2

- Corriger configuration Terraform des VMs
  * Ajouter vga { type = "std" } pour Standard VGA sur toutes les VMs
  * Ajouter cpu { type = "host" } pour meilleure performance
  * Ajouter replace_triggered_by pour détecter les changements de config
  * Ajouter force_create = true sur pve3 pour gérer VM existante

- Résoudre problèmes identifiés
  * "No Bootable Device" - Résolu avec Standard VGA et CPU host
  * "vmId already in use" - Résolu avec force_create sur etcd-witness
  * Détection des modifications de VM - Résolu avec replace_triggered_by

Documentation SSH créée dans cicd_backup/SETUP_SSH_LINSTOR.md
2025-11-27 18:06:15 +01:00

313 lines
9.7 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de gestion des ressources DRBD Linstor pour les VMs K3s
Exécuté avant le déploiement Terraform pour s'assurer que les ressources
de stockage DRBD sont créées et dimensionnées correctement.
"""
import subprocess
import sys
import json
import argparse
import os
from typing import Dict, Optional, Tuple
# Clé SSH globale (peut être définie via variable d'environnement)
SSH_KEY_PATH = os.environ.get("SSH_KEY_PATH", None)
# Noms de ressources DRBD fixes pour chaque nœud
RESOURCE_NAMES = {
1000: "pm-a7f3c8e1", # acemagician - k3s-server-1
1001: "pm-b4d2f9a3", # elitedesk - k3s-server-2
}
# Configuration des nœuds Proxmox
NODE_CONFIG = {
1000: {"node": "acemagician", "vm_name": "k3s-server-1"},
1001: {"node": "elitedesk", "vm_name": "k3s-server-2"},
}
def run_ssh_command(command: str, host: str = "192.168.100.30", ssh_key: Optional[str] = None) -> Tuple[int, str, str]:
"""
Exécute une commande SSH sur le contrôleur Linstor (thinkpad - 192.168.100.30).
Args:
command: Commande à exécuter
host: Hôte sur lequel exécuter la commande (défaut: 192.168.100.30)
ssh_key: Chemin vers la clé SSH privée (optionnel)
Returns:
Tuple (code_retour, stdout, stderr)
"""
ssh_cmd = ["ssh", "-o", "StrictHostKeyChecking=no"]
# Ajouter la clé SSH si spécifiée
if ssh_key:
ssh_cmd.extend(["-i", ssh_key])
ssh_cmd.extend([f"root@{host}", command])
try:
result = subprocess.run(
ssh_cmd,
capture_output=True,
text=True,
timeout=30
)
return result.returncode, result.stdout, result.stderr
except subprocess.TimeoutExpired:
return 1, "", "Timeout lors de l'exécution de la commande SSH"
except Exception as e:
return 1, "", f"Erreur lors de l'exécution SSH: {str(e)}"
def check_resource_exists(resource_name: str, ssh_key: Optional[str] = None) -> bool:
"""
Vérifie si une ressource DRBD existe.
Args:
resource_name: Nom de la ressource à vérifier
ssh_key: Chemin vers la clé SSH privée (optionnel)
Returns:
True si la ressource existe, False sinon
"""
returncode, stdout, stderr = run_ssh_command(
f"linstor resource list --resource {resource_name} --machine-readable",
ssh_key=ssh_key
)
if returncode == 0:
# Vérifie si la sortie contient des informations sur la ressource
return len(stdout.strip()) > 0 and "resources" in stdout
return False
def get_resource_size(resource_name: str, ssh_key: Optional[str] = None) -> Optional[int]:
"""
Récupère la taille actuelle d'une ressource DRBD en GiB.
Args:
resource_name: Nom de la ressource
ssh_key: Chemin vers la clé SSH privée (optionnel)
Returns:
Taille en GiB ou None si erreur
"""
returncode, stdout, stderr = run_ssh_command(
f"linstor volume-definition list --resource {resource_name} --machine-readable",
ssh_key=ssh_key
)
if returncode != 0:
return None
try:
# Parse la sortie JSON de Linstor
data = json.loads(stdout)
if data and len(data) > 0:
volume_defs = data[0].get("volume_definitions", [])
if volume_defs and len(volume_defs) > 0:
# Taille en KiB, conversion en GiB
size_kib = volume_defs[0].get("size_kib", 0)
return size_kib // (1024 * 1024)
except (json.JSONDecodeError, KeyError, IndexError) as e:
print(f"Erreur lors du parsing de la taille: {e}", file=sys.stderr)
return None
return None
def create_resource(resource_name: str, size_gib: int, nodes: list, ssh_key: Optional[str] = None) -> bool:
"""
Crée une nouvelle ressource DRBD avec réplication.
Args:
resource_name: Nom de la ressource à créer
size_gib: Taille en GiB
nodes: Liste des nœuds pour la réplication
ssh_key: Chemin vers la clé SSH privée (optionnel)
Returns:
True si succès, False sinon
"""
print(f"Création de la ressource {resource_name} avec {size_gib}GiB...")
# Étape 1: Créer la définition de ressource
returncode, stdout, stderr = run_ssh_command(
f"linstor resource-definition create {resource_name}",
ssh_key=ssh_key
)
if returncode != 0:
print(f"Erreur lors de la création de la définition: {stderr}", file=sys.stderr)
return False
# Étape 2: Créer la définition de volume
returncode, stdout, stderr = run_ssh_command(
f"linstor volume-definition create {resource_name} {size_gib}GiB",
ssh_key=ssh_key
)
if returncode != 0:
print(f"Erreur lors de la création du volume: {stderr}", file=sys.stderr)
return False
# Étape 3: Déployer la ressource sur les nœuds avec réplication
for node in nodes:
returncode, stdout, stderr = run_ssh_command(
f"linstor resource create {node} {resource_name} --storage-pool linstor_thinpool",
ssh_key=ssh_key
)
if returncode != 0:
print(f"Erreur lors du déploiement sur {node}: {stderr}", file=sys.stderr)
return False
print(f"✓ Ressource {resource_name} créée avec succès")
return True
def resize_resource(resource_name: str, new_size_gib: int, ssh_key: Optional[str] = None) -> bool:
"""
Augmente la taille d'une ressource DRBD existante.
Args:
resource_name: Nom de la ressource à redimensionner
new_size_gib: Nouvelle taille en GiB
ssh_key: Chemin vers la clé SSH privée (optionnel)
Returns:
True si succès, False sinon
"""
print(f"Redimensionnement de la ressource {resource_name} à {new_size_gib}GiB...")
returncode, stdout, stderr = run_ssh_command(
f"linstor volume-definition set-size {resource_name} 0 {new_size_gib}GiB",
ssh_key=ssh_key
)
if returncode != 0:
print(f"Erreur lors du redimensionnement: {stderr}", file=sys.stderr)
return False
print(f"✓ Ressource {resource_name} redimensionnée avec succès")
return True
def manage_vm_resource(vmid: int, size_gib: int, dry_run: bool = False, ssh_key: Optional[str] = None) -> bool:
"""
Gère la ressource DRBD pour une VM spécifique.
Args:
vmid: ID de la VM
size_gib: Taille souhaitée en GiB
dry_run: Si True, affiche les actions sans les exécuter
ssh_key: Chemin vers la clé SSH privée (optionnel)
Returns:
True si succès, False sinon
"""
if vmid not in RESOURCE_NAMES:
print(f"VMID {vmid} non configuré", file=sys.stderr)
return False
resource_name = RESOURCE_NAMES[vmid]
node_info = NODE_CONFIG[vmid]
print(f"\n{'='*60}")
print(f"Gestion de la ressource pour VM {vmid} ({node_info['vm_name']})")
print(f"Ressource DRBD: {resource_name}")
print(f"Nœud Proxmox: {node_info['node']}")
print(f"Taille souhaitée: {size_gib}GiB")
print(f"{'='*60}\n")
# Vérifie si la ressource existe
resource_exists = check_resource_exists(resource_name, ssh_key=ssh_key)
if not resource_exists:
print(f"La ressource {resource_name} n'existe pas.")
if dry_run:
print(f"[DRY-RUN] Créerait la ressource {resource_name} avec {size_gib}GiB")
return True
# Créer la ressource sur les 3 nœuds pour réplication
nodes = ["acemagician", "elitedesk", "thinkpad"]
return create_resource(resource_name, size_gib, nodes, ssh_key=ssh_key)
else:
print(f"La ressource {resource_name} existe déjà.")
# Vérifie la taille actuelle
current_size = get_resource_size(resource_name, ssh_key=ssh_key)
if current_size is None:
print("Impossible de récupérer la taille actuelle", file=sys.stderr)
return False
print(f"Taille actuelle: {current_size}GiB")
if current_size == size_gib:
print(f"✓ La taille correspond déjà ({size_gib}GiB), aucune action nécessaire")
return True
elif current_size < size_gib:
print(f"La taille doit être augmentée de {current_size}GiB à {size_gib}GiB")
if dry_run:
print(f"[DRY-RUN] Redimensionnerait {resource_name} à {size_gib}GiB")
return True
return resize_resource(resource_name, size_gib, ssh_key=ssh_key)
else:
print(f"⚠ La taille actuelle ({current_size}GiB) est supérieure à la taille souhaitée ({size_gib}GiB)")
print("La réduction de taille n'est pas supportée, conservation de la taille actuelle")
return True
def main():
"""Point d'entrée principal du script."""
parser = argparse.ArgumentParser(
description="Gestion des ressources DRBD Linstor pour les VMs K3s"
)
parser.add_argument(
"--vmid",
type=int,
required=True,
choices=[1000, 1001],
help="ID de la VM (1000=acemagician, 1001=elitedesk)"
)
parser.add_argument(
"--size",
type=int,
required=True,
help="Taille du disque en GiB"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Affiche les actions sans les exécuter"
)
parser.add_argument(
"--ssh-key",
type=str,
default=SSH_KEY_PATH,
help="Chemin vers la clé SSH privée (défaut: variable d'environnement SSH_KEY_PATH)"
)
args = parser.parse_args()
# Exécute la gestion de la ressource
success = manage_vm_resource(args.vmid, args.size, args.dry_run, ssh_key=args.ssh_key)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()