2025-11-27 12:31:26 +01:00
#!/usr/bin/env python3
"""
2025-11-27 12:51:08 +01:00
Script de Gestion des Ressources LINSTOR pour Proxmox via API
2025-11-27 12:31:26 +01:00
Auteur : BENE Maël
2025-11-27 12:51:08 +01:00
Version : 2.0
2025-11-27 12:31:26 +01:00
Date : 2025 - 11 - 27
Description :
2025-11-27 12:51:08 +01:00
Ce script gère automatiquement les ressources LINSTOR pour les VMs Proxmox
en utilisant l ' API Proxmox au lieu de commandes SSH directes.
2025-11-27 12:31:26 +01:00
Il assure que les ressources existent avec la taille correcte avant le déploiement .
Fonctionnalités :
2025-11-27 12:51:08 +01:00
- Vérifie l ' existence d ' une ressource via API Proxmox
2025-11-27 12:31:26 +01:00
- Crée la ressource si elle n ' existe pas
- Redimensionne la ressource si la taille ne correspond pas ( uniquement augmentation )
- Opérations idempotentes ( peut être exécuté plusieurs fois en toute sécurité )
"""
import json
import sys
import argparse
import re
import os
2025-11-27 12:51:08 +01:00
import urllib3
import requests
2025-11-27 12:31:26 +01:00
from pathlib import Path
2025-11-27 12:51:08 +01:00
# Désactive les avertissements SSL pour les certificats auto-signés
urllib3 . disable_warnings ( urllib3 . exceptions . InsecureRequestWarning )
2025-11-27 12:31:26 +01:00
# Configuration des ressources par défaut
# Ces valeurs peuvent être modifiées selon vos besoins
RESOURCE_CONFIG = {
' vm-1000-disk-0 ' : {
' node ' : ' acemagician ' ,
' size ' : ' 100G ' ,
' storage_pool ' : ' pve-storage '
} ,
' vm-1001-disk-0 ' : {
' node ' : ' elitedesk ' ,
' size ' : ' 100G ' ,
' storage_pool ' : ' pve-storage '
} ,
' vm-1002-disk-0 ' : {
' node ' : ' thinkpad ' ,
' size ' : ' 20G ' ,
' storage_pool ' : ' local-lvm '
}
}
2025-11-27 12:51:08 +01:00
class ProxmoxAPI :
""" Client pour l ' API Proxmox """
2025-11-27 12:31:26 +01:00
2025-11-27 12:51:08 +01:00
def __init__ ( self , api_url , token_id , token_secret , verify_ssl = False , verbose = False ) :
self . api_url = api_url . rstrip ( ' / ' )
self . token_id = token_id
self . token_secret = token_secret
self . verify_ssl = verify_ssl
2025-11-27 12:31:26 +01:00
self . verbose = verbose
2025-11-27 12:51:08 +01:00
self . headers = {
' Authorization ' : f ' PVEAPIToken= { token_id } = { token_secret } '
}
2025-11-27 12:31:26 +01:00
def log ( self , message ) :
""" Affiche un message de log si le mode verbose est activé """
if self . verbose :
print ( f " [INFO] { message } " )
2025-11-27 12:51:08 +01:00
def _request ( self , method , endpoint , data = None ) :
""" Effectue une requête HTTP vers l ' API Proxmox """
url = f " { self . api_url } { endpoint } "
self . log ( f " { method } { endpoint } " )
2025-11-27 12:41:39 +01:00
2025-11-27 12:31:26 +01:00
try :
2025-11-27 12:51:08 +01:00
if method == ' GET ' :
response = requests . get ( url , headers = self . headers , verify = self . verify_ssl )
elif method == ' POST ' :
response = requests . post ( url , headers = self . headers , data = data , verify = self . verify_ssl )
elif method == ' PUT ' :
response = requests . put ( url , headers = self . headers , data = data , verify = self . verify_ssl )
elif method == ' DELETE ' :
response = requests . delete ( url , headers = self . headers , verify = self . verify_ssl )
else :
raise ValueError ( f " Méthode HTTP non supportée: { method } " )
response . raise_for_status ( )
return response . json ( )
except requests . exceptions . RequestException as e :
print ( f " [ERREUR] Requête API échouée: { e } " , file = sys . stderr )
if hasattr ( e . response , ' text ' ) :
print ( f " [ERREUR] Réponse: { e . response . text } " , file = sys . stderr )
2025-11-27 12:31:26 +01:00
return None
2025-11-27 12:51:08 +01:00
def linstor_resource_definitions_list ( self ) :
""" Liste toutes les définitions de ressources LINSTOR """
result = self . _request ( ' GET ' , ' /cluster/linstor/resource-definitions ' )
if result and ' data ' in result :
return result [ ' data ' ]
return [ ]
def linstor_resource_definition_create ( self , resource_name ) :
""" Crée une définition de ressource LINSTOR """
data = { ' id ' : resource_name }
result = self . _request ( ' POST ' , ' /cluster/linstor/resource-definitions ' , data )
return result is not None
def linstor_volume_definitions_list ( self , resource_name ) :
""" Liste les définitions de volumes pour une ressource """
result = self . _request ( ' GET ' , f ' /cluster/linstor/resource-definitions/ { resource_name } /volumes ' )
if result and ' data ' in result :
return result [ ' data ' ]
return [ ]
def linstor_volume_definition_create ( self , resource_name , size_kib ) :
""" Crée une définition de volume LINSTOR """
data = {
' volume-number ' : ' 0 ' ,
' size-kib ' : str ( size_kib )
}
result = self . _request ( ' POST ' , f ' /cluster/linstor/resource-definitions/ { resource_name } /volumes ' , data )
return result is not None
def linstor_volume_definition_resize ( self , resource_name , volume_number , size_kib ) :
""" Redimensionne un volume LINSTOR """
data = { ' size-kib ' : str ( size_kib ) }
result = self . _request ( ' PUT ' , f ' /cluster/linstor/resource-definitions/ { resource_name } /volumes/ { volume_number } ' , data )
return result is not None
def linstor_resources_list ( self , resource_name = None ) :
""" Liste les ressources LINSTOR """
if resource_name :
result = self . _request ( ' GET ' , f ' /cluster/linstor/resources/ { resource_name } ' )
else :
result = self . _request ( ' GET ' , ' /cluster/linstor/resources ' )
if result and ' data ' in result :
return result [ ' data ' ]
return [ ]
def linstor_resource_create ( self , node , resource_name , storage_pool ) :
""" Crée une ressource LINSTOR sur un nœud """
data = {
' node ' : node ,
' storage-pool ' : storage_pool
}
result = self . _request ( ' POST ' , f ' /cluster/linstor/resources/ { resource_name } ' , data )
return result is not None
class LinstorManager :
""" Gestionnaire des ressources LINSTOR via API Proxmox """
def __init__ ( self , proxmox_api , verbose = False ) :
self . api = proxmox_api
self . verbose = verbose
def log ( self , message ) :
""" Affiche un message de log si le mode verbose est activé """
if self . verbose :
print ( f " [INFO] { message } " )
2025-11-27 12:31:26 +01:00
def parse_size ( self , size_str ) :
2025-11-27 12:51:08 +01:00
""" Convertit une chaîne de taille (ex: ' 100G ' , ' 1024M ' ) en KiB """
2025-11-27 12:31:26 +01:00
size_str = size_str . strip ( ) . upper ( )
# Correspond au nombre et à l'unité
match = re . match ( r ' ^( \ d+(?: \ . \ d+)?) \ s*([KMGT]?)I?B?$ ' , size_str )
if not match :
raise ValueError ( f " Format de taille invalide: { size_str } " )
number , unit = match . groups ( )
number = float ( number )
2025-11-27 12:51:08 +01:00
# Convertit en KiB
2025-11-27 12:31:26 +01:00
multipliers = {
2025-11-27 12:51:08 +01:00
' ' : 1 / 1024 , # Bytes vers KiB
' K ' : 1 , # KiB
' M ' : 1024 , # MiB vers KiB
' G ' : 1024 * * 2 , # GiB vers KiB
' T ' : 1024 * * 3 , # TiB vers KiB
2025-11-27 12:31:26 +01:00
}
return int ( number * multipliers . get ( unit , 1 ) )
2025-11-27 12:51:08 +01:00
def format_size ( self , kib_value ) :
""" Formate les KiB en taille lisible """
bytes_value = kib_value * 1024
2025-11-27 12:31:26 +01:00
for unit in [ ' ' , ' K ' , ' M ' , ' G ' , ' T ' ] :
if bytes_value < 1024.0 :
return f " { bytes_value : .0f } { unit } iB " if unit else f " { bytes_value : .0f } B "
bytes_value / = 1024.0
return f " { bytes_value : .2f } PiB "
def resource_exists ( self , resource_name ) :
""" Vérifie si une définition de ressource LINSTOR existe """
2025-11-27 12:51:08 +01:00
resources = self . api . linstor_resource_definitions_list ( )
for res in resources :
if res . get ( ' name ' ) == resource_name :
self . log ( f " La définition de ressource ' { resource_name } ' existe " )
return True
2025-11-27 12:31:26 +01:00
return False
def get_resource_size ( self , resource_name ) :
2025-11-27 12:51:08 +01:00
""" Récupère la taille actuelle d ' un volume de ressource (en KiB) """
volumes = self . api . linstor_volume_definitions_list ( resource_name )
for vol in volumes :
if vol . get ( ' volume_number ' ) == 0 or vol . get ( ' volume-number ' ) == 0 :
size_kib = vol . get ( ' size_kib ' ) or vol . get ( ' size-kib ' , 0 )
self . log ( f " Taille actuelle de ' { resource_name } ' : { self . format_size ( size_kib ) } " )
return size_kib
2025-11-27 12:31:26 +01:00
return None
def get_resource_nodes ( self , resource_name ) :
""" Récupère la liste des nœuds où la ressource est déployée """
2025-11-27 12:51:08 +01:00
resources = self . api . linstor_resources_list ( resource_name )
2025-11-27 12:31:26 +01:00
nodes = set ( )
2025-11-27 12:51:08 +01:00
for res in resources :
node = res . get ( ' node_name ' ) or res . get ( ' node-name ' )
if node :
nodes . add ( node )
2025-11-27 12:31:26 +01:00
return list ( nodes )
def create_resource_definition ( self , resource_name ) :
""" Crée une définition de ressource LINSTOR """
self . log ( f " Création de la définition de ressource ' { resource_name } ' " )
2025-11-27 12:51:08 +01:00
if self . api . linstor_resource_definition_create ( resource_name ) :
print ( f " ✓ Définition de ressource ' { resource_name } ' créée " )
return True
return False
2025-11-27 12:31:26 +01:00
def create_volume_definition ( self , resource_name , size ) :
""" Crée une définition de volume LINSTOR """
2025-11-27 12:51:08 +01:00
size_kib = self . parse_size ( size )
2025-11-27 12:31:26 +01:00
self . log ( f " Création de la définition de volume pour ' { resource_name } ' avec taille { size } " )
2025-11-27 12:51:08 +01:00
if self . api . linstor_volume_definition_create ( resource_name , size_kib ) :
print ( f " ✓ Définition de volume créée pour ' { resource_name } ' avec taille { size } " )
return True
return False
2025-11-27 12:31:26 +01:00
def create_resource ( self , node , resource_name , storage_pool ) :
""" Crée une ressource LINSTOR sur un nœud spécifique """
self . log ( f " Création de la ressource ' { resource_name } ' sur le nœud ' { node } ' avec le pool de stockage ' { storage_pool } ' " )
2025-11-27 12:51:08 +01:00
if self . api . linstor_resource_create ( node , resource_name , storage_pool ) :
print ( f " ✓ Ressource ' { resource_name } ' créée sur le nœud ' { node } ' " )
return True
return False
2025-11-27 12:31:26 +01:00
def resize_volume ( self , resource_name , new_size ) :
""" Redimensionne un volume LINSTOR (uniquement augmentation) """
2025-11-27 12:51:08 +01:00
new_size_kib = self . parse_size ( new_size )
2025-11-27 12:31:26 +01:00
self . log ( f " Redimensionnement du volume ' { resource_name } ' à { new_size } " )
2025-11-27 12:51:08 +01:00
if self . api . linstor_volume_definition_resize ( resource_name , 0 , new_size_kib ) :
print ( f " ✓ Volume ' { resource_name } ' redimensionné à { new_size } " )
return True
return False
2025-11-27 12:31:26 +01:00
def ensure_resource ( self , resource_name , node , size , storage_pool ) :
"""
Assure qu ' une ressource LINSTOR existe avec la taille correcte
Args :
resource_name : Nom de la ressource ( ex : ' vm-1000-disk-0 ' )
node : Nom du nœud cible ( ex : ' acemagician ' )
size : Taille désirée ( ex : ' 100G ' )
storage_pool : Nom du pool de stockage ( ex : ' pve-storage ' )
Returns :
True si succès , False sinon
"""
print ( f " \n === Gestion de la ressource ' { resource_name } ' sur le nœud ' { node } ' === " )
# Vérifie si la définition de ressource existe
if not self . resource_exists ( resource_name ) :
print ( f " La ressource ' { resource_name } ' n ' existe pas. Création... " )
# Crée la définition de ressource
if not self . create_resource_definition ( resource_name ) :
return False
# Crée la définition de volume
if not self . create_volume_definition ( resource_name , size ) :
return False
# Crée la ressource sur le nœud
if not self . create_resource ( node , resource_name , storage_pool ) :
return False
print ( f " ✓ Ressource ' { resource_name } ' créée avec succès " )
return True
# La ressource existe - vérifier la taille
print ( f " La ressource ' { resource_name } ' existe déjà. Vérification de la taille... " )
current_size = self . get_resource_size ( resource_name )
2025-11-27 12:51:08 +01:00
desired_size_kib = self . parse_size ( size )
2025-11-27 12:31:26 +01:00
if current_size is None :
print ( f " [ATTENTION] Impossible de déterminer la taille actuelle de ' { resource_name } ' " )
return False
2025-11-27 12:51:08 +01:00
if current_size < desired_size_kib :
2025-11-27 12:31:26 +01:00
print ( f " Taille actuelle ( { self . format_size ( current_size ) } ) inférieure à la taille désirée ( { size } ) " )
print ( f " Redimensionnement de la ressource ' { resource_name } ' à { size } ... " )
if not self . resize_volume ( resource_name , size ) :
return False
print ( f " ✓ Ressource ' { resource_name } ' redimensionnée avec succès " )
2025-11-27 12:51:08 +01:00
elif current_size > desired_size_kib :
2025-11-27 12:31:26 +01:00
print ( f " [ATTENTION] Taille actuelle ( { self . format_size ( current_size ) } ) supérieure à la taille désirée ( { size } ) " )
print ( f " [ATTENTION] LINSTOR ne supporte pas la réduction de volumes. Conservation de la taille actuelle. " )
else :
print ( f " ✓ Taille correspondante ( { self . format_size ( current_size ) } ). Aucune action nécessaire. " )
# Vérifie si la ressource est déployée sur le bon nœud
deployed_nodes = self . get_resource_nodes ( resource_name )
if node not in deployed_nodes :
print ( f " Ressource ' { resource_name } ' non déployée sur le nœud ' { node } ' . Déploiement... " )
if not self . create_resource ( node , resource_name , storage_pool ) :
return False
print ( f " ✓ Ressource ' { resource_name } ' déployée avec succès sur le nœud ' { node } ' " )
else :
self . log ( f " Ressource ' { resource_name } ' déjà déployée sur le nœud ' { node } ' " )
return True
def parse_terraform_config ( terraform_dir ) :
"""
Parse les fichiers Terraform pour extraire la configuration des ressources
Args :
terraform_dir : Chemin vers le répertoire racine Terraform
Returns :
Dictionnaire de configuration des ressources
"""
config = { }
terraform_path = Path ( terraform_dir )
# Cherche tous les répertoires pve*
for pve_dir in terraform_path . glob ( ' pve* ' ) :
if not pve_dir . is_dir ( ) :
continue
main_tf = pve_dir / ' main.tf '
if not main_tf . exists ( ) :
continue
try :
with open ( main_tf , ' r ' ) as f :
content = f . read ( )
# Extrait VMID
vmid_match = re . search ( r ' vmid \ s*= \ s*( \ d+) ' , content )
if not vmid_match :
continue
vmid = vmid_match . group ( 1 )
# Extrait target_node
node_match = re . search ( r ' target_node \ s*= \ s* " ([^ " ]+) " ' , content )
if not node_match :
continue
node = node_match . group ( 1 )
# Extrait disk size depuis var reference
size_match = re . search ( r ' size \ s*= \ s*var \ .( \ w+) \ .disk_size ' , content )
if size_match :
var_name = size_match . group ( 1 )
# Cherche la valeur par défaut dans variables.tf ou dans le workflow
# Pour simplifier, on utilise les valeurs par défaut
if ' etcd ' in var_name . lower ( ) :
size = ' 20G '
else :
size = ' 100G '
else :
size = ' 100G '
# Extrait storage pool
storage_match = re . search ( r ' storage \ s*= \ s*var \ .( \ w+) ' , content )
if storage_match :
storage_var = storage_match . group ( 1 )
if ' etcd ' in storage_var . lower ( ) :
storage_pool = ' local-lvm '
else :
2025-11-27 12:51:08 +01:00
storage_pool = ' pve-storage '
2025-11-27 12:31:26 +01:00
else :
2025-11-27 12:51:08 +01:00
storage_pool = ' pve-storage '
2025-11-27 12:31:26 +01:00
resource_name = f " vm- { vmid } -disk-0 "
config [ resource_name ] = {
' node ' : node ,
' size ' : size ,
' storage_pool ' : storage_pool
}
except Exception as e :
print ( f " [ATTENTION] Erreur lors de la lecture de { main_tf } : { e } " , file = sys . stderr )
continue
return config
def main ( ) :
parser = argparse . ArgumentParser (
2025-11-27 12:51:08 +01:00
description = ' Gestion des ressources LINSTOR pour les VMs Proxmox via API ' ,
2025-11-27 12:31:26 +01:00
formatter_class = argparse . RawDescriptionHelpFormatter
)
parser . add_argument ( ' --resource ' , help = ' Nom de la ressource (ex: vm-1000-disk-0) ' )
parser . add_argument ( ' --node ' , help = ' Nom du nœud cible (ex: acemagician) ' )
parser . add_argument ( ' --size ' , help = ' Taille désirée (ex: 100G) ' )
parser . add_argument ( ' --storage-pool ' , help = ' Nom du pool de stockage (ex: pve-storage) ' )
parser . add_argument ( ' --terraform-dir ' , help = ' Chemin vers le répertoire Terraform pour lecture automatique de la config ' )
parser . add_argument ( ' --all ' , action = ' store_true ' , help = ' Traiter toutes les ressources configurées ' )
2025-11-27 12:51:08 +01:00
# Paramètres API Proxmox
parser . add_argument ( ' --api-url ' , help = ' URL de l \' API Proxmox (ex: https://192.168.100.10:8006/api2/json) ' ,
default = os . environ . get ( ' PROXMOX_API_URL ' ) )
parser . add_argument ( ' --token-id ' , help = ' ID du token API Proxmox ' ,
default = os . environ . get ( ' PROXMOX_TOKEN_ID ' ) )
parser . add_argument ( ' --token-secret ' , help = ' Secret du token API Proxmox ' ,
default = os . environ . get ( ' PROXMOX_TOKEN_SECRET ' ) )
2025-11-27 12:31:26 +01:00
parser . add_argument ( ' --verbose ' , ' -v ' , action = ' store_true ' , help = ' Active la sortie détaillée ' )
args = parser . parse_args ( )
2025-11-27 12:51:08 +01:00
# Vérifie que les credentials API sont fournis
if not args . api_url or not args . token_id or not args . token_secret :
print ( " [ERREUR] Les credentials API Proxmox sont requis (--api-url, --token-id, --token-secret) " , file = sys . stderr )
print ( " [INFO] Vous pouvez aussi les définir via les variables d ' environnement PROXMOX_API_URL, PROXMOX_TOKEN_ID, PROXMOX_TOKEN_SECRET " , file = sys . stderr )
sys . exit ( 1 )
# Initialise le client API
api = ProxmoxAPI (
api_url = args . api_url ,
token_id = args . token_id ,
token_secret = args . token_secret ,
verify_ssl = False ,
verbose = args . verbose
)
manager = LinstorManager ( proxmox_api = api , verbose = args . verbose )
2025-11-27 12:31:26 +01:00
# Mode 1: Lecture automatique depuis Terraform
if args . terraform_dir :
print ( " === Lecture de la configuration depuis les fichiers Terraform === " )
config = parse_terraform_config ( args . terraform_dir )
if not config :
print ( " [ERREUR] Aucune configuration trouvée dans les fichiers Terraform " , file = sys . stderr )
sys . exit ( 1 )
print ( f " Configuration trouvée pour { len ( config ) } ressource(s) " )
all_success = True
for resource_name , res_config in config . items ( ) :
success = manager . ensure_resource (
resource_name = resource_name ,
node = res_config [ ' node ' ] ,
size = res_config [ ' size ' ] ,
storage_pool = res_config [ ' storage_pool ' ]
)
if not success :
all_success = False
if all_success :
print ( " \n ✓ Toutes les ressources sont prêtes " )
sys . exit ( 0 )
else :
print ( " \n ✗ Certaines ressources ont échoué " , file = sys . stderr )
sys . exit ( 1 )
# Mode 2: Traiter toutes les ressources de la configuration par défaut
elif args . all :
print ( " === Traitement de toutes les ressources configurées === " )
all_success = True
for resource_name , config in RESOURCE_CONFIG . items ( ) :
success = manager . ensure_resource (
resource_name = resource_name ,
node = config [ ' node ' ] ,
size = config [ ' size ' ] ,
storage_pool = config [ ' storage_pool ' ]
)
if not success :
all_success = False
if all_success :
print ( " \n ✓ Toutes les ressources sont prêtes " )
sys . exit ( 0 )
else :
print ( " \n ✗ Certaines ressources ont échoué " , file = sys . stderr )
sys . exit ( 1 )
# Mode 3: Traiter une ressource spécifique
elif args . resource and args . node and args . size and args . storage_pool :
success = manager . ensure_resource (
resource_name = args . resource ,
node = args . node ,
size = args . size ,
storage_pool = args . storage_pool
)
if success :
print ( f " \n ✓ Ressource ' { args . resource } ' prête sur le nœud ' { args . node } ' " )
sys . exit ( 0 )
else :
print ( f " \n ✗ Échec de la gestion de la ressource ' { args . resource } ' " , file = sys . stderr )
sys . exit ( 1 )
else :
parser . print_help ( )
print ( " \n [ERREUR] Arguments insuffisants. Utilisez --terraform-dir, --all, ou spécifiez --resource --node --size --storage-pool " , file = sys . stderr )
sys . exit ( 1 )
if __name__ == ' __main__ ' :
main ( )