Homelab/scripts/manage_linstor_resources.py

519 lines
20 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Script de Gestion des Ressources LINSTOR pour Proxmox via API
Auteur: BENE Maël
Version: 2.0
Date: 2025-11-27
Description:
Ce script gère automatiquement les ressources LINSTOR pour les VMs Proxmox
en utilisant l'API Proxmox au lieu de commandes SSH directes.
Il assure que les ressources existent avec la taille correcte avant le déploiement.
Fonctionnalités:
- Vérifie l'existence d'une ressource via API Proxmox
- 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
import urllib3
import requests
from pathlib import Path
# Désactive les avertissements SSL pour les certificats auto-signés
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 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'
}
}
class ProxmoxAPI:
"""Client pour l'API Proxmox"""
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
self.verbose = verbose
self.headers = {
'Authorization': f'PVEAPIToken={token_id}={token_secret}'
}
def log(self, message):
"""Affiche un message de log si le mode verbose est activé"""
if self.verbose:
print(f"[INFO] {message}")
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}")
try:
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)
return None
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}")
def parse_size(self, size_str):
"""Convertit une chaîne de taille (ex: '100G', '1024M') en KiB"""
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)
# Convertit en KiB
multipliers = {
'': 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
}
return int(number * multipliers.get(unit, 1))
def format_size(self, kib_value):
"""Formate les KiB en taille lisible"""
bytes_value = kib_value * 1024
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"""
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
return False
def get_resource_size(self, resource_name):
"""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
return None
def get_resource_nodes(self, resource_name):
"""Récupère la liste des nœuds où la ressource est déployée"""
resources = self.api.linstor_resources_list(resource_name)
nodes = set()
for res in resources:
node = res.get('node_name') or res.get('node-name')
if node:
nodes.add(node)
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}'")
if self.api.linstor_resource_definition_create(resource_name):
print(f"✓ Définition de ressource '{resource_name}' créée")
return True
return False
def create_volume_definition(self, resource_name, size):
"""Crée une définition de volume LINSTOR"""
size_kib = self.parse_size(size)
self.log(f"Création de la définition de volume pour '{resource_name}' avec taille {size}")
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
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}'")
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
def resize_volume(self, resource_name, new_size):
"""Redimensionne un volume LINSTOR (uniquement augmentation)"""
new_size_kib = self.parse_size(new_size)
self.log(f"Redimensionnement du volume '{resource_name}' à {new_size}")
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
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)
desired_size_kib = self.parse_size(size)
if current_size is None:
print(f"[ATTENTION] Impossible de déterminer la taille actuelle de '{resource_name}'")
return False
if current_size < desired_size_kib:
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")
elif current_size > desired_size_kib:
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:
storage_pool = 'pve-storage'
else:
storage_pool = 'pve-storage'
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(
description='Gestion des ressources LINSTOR pour les VMs Proxmox via API',
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')
# 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'))
parser.add_argument('--verbose', '-v', action='store_true', help='Active la sortie détaillée')
args = parser.parse_args()
# 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)
# 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()