Files

339 lines
12 KiB
Python

"""
ZFS Manager - Gestión específica de ZFS
"""
import subprocess
import json
import re
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
from .config import DEFAULT_CONFIG
@dataclass
class ZFSPool:
"""Representa un pool ZFS"""
name: str
health: str
size: int
allocated: int
free: int
capacity: int
dedup_ratio: float
ashift: int
devices: List[str]
raid_type: str
@dataclass
class ZFSDataset:
"""Representa un dataset ZFS"""
name: str
type: str
used: int
available: int
referenced: int
mountpoint: str
compression: str
readonly: bool
class ZFSManager:
"""Gestor de operaciones ZFS"""
def __init__(self, system_manager, console):
self.system = system_manager
self.console = console
self.zpool_cmd = DEFAULT_CONFIG["commands"]["zfs"]["zpool"]
self.zfs_cmd = DEFAULT_CONFIG["commands"]["zfs"]["zfs"]
def is_available(self) -> bool:
"""Verifica si ZFS está disponible"""
try:
self.system.run_command([self.zpool_cmd, 'list'], capture_output=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def list_pools(self) -> List[ZFSPool]:
"""Lista todos los pools ZFS"""
pools = []
try:
# Obtener información básica de pools
result = self.system.run_command([
self.zpool_cmd, 'list', '-H', '-p', '-o',
'name,health,size,allocated,free,capacity,dedupratio'
])
for line in result.stdout.strip().split('\n'):
if line:
parts = line.split('\t')
if len(parts) >= 7:
pool_name = parts[0]
# Obtener ashift y dispositivos
ashift, devices, raid_type = self._get_pool_details(pool_name)
pool = ZFSPool(
name=pool_name,
health=parts[1],
size=int(parts[2]),
allocated=int(parts[3]),
free=int(parts[4]),
capacity=int(parts[5]),
dedup_ratio=float(parts[6].rstrip('x')),
ashift=ashift,
devices=devices,
raid_type=raid_type
)
pools.append(pool)
except subprocess.CalledProcessError as e:
self.console.print(f"❌ Error listando pools ZFS: {e}", style="red")
return pools
def _get_pool_details(self, pool_name: str) -> Tuple[int, List[str], str]:
"""Obtiene detalles específicos de un pool"""
ashift = 12 # Default
devices = []
raid_type = "unknown"
try:
# Obtener estado del pool
result = self.system.run_command([self.zpool_cmd, 'status', pool_name])
status_output = result.stdout
# Extraer ashift
ashift_match = re.search(r'ashift=(\d+)', status_output)
if ashift_match:
ashift = int(ashift_match.group(1))
# Extraer dispositivos y tipo de RAID
lines = status_output.split('\n')
in_config = False
current_vdev_type = None
for line in lines:
line = line.strip()
if 'config:' in line.lower():
in_config = True
continue
if in_config and line and not line.startswith('NAME'):
if not line.startswith('\t') and not line.startswith(' '):
# Es el nombre del pool
continue
# Detectar tipo de vdev
if any(x in line for x in ['mirror', 'raidz1', 'raidz2', 'raidz3']):
if 'mirror' in line:
current_vdev_type = 'mirror'
raid_type = 'mirror'
elif 'raidz3' in line:
current_vdev_type = 'raidz3'
raid_type = 'raidz3'
elif 'raidz2' in line:
current_vdev_type = 'raidz2'
raid_type = 'raidz2'
elif 'raidz1' in line or 'raidz' in line:
current_vdev_type = 'raidz1'
raid_type = 'raidz1'
else:
# Es un dispositivo
device_match = re.search(r'(sd[a-z]+|nvme\d+n\d+)', line)
if device_match:
devices.append(device_match.group(1))
if raid_type == "unknown":
raid_type = "stripe" # Si no hay vdev específico, es stripe
except subprocess.CalledProcessError:
pass
return ashift, devices, raid_type
def list_datasets(self, pool_name: Optional[str] = None) -> List[ZFSDataset]:
"""Lista datasets ZFS"""
datasets = []
try:
cmd = [
self.zfs_cmd, 'list', '-H', '-p', '-o',
'name,type,used,available,referenced,mountpoint,compression,readonly'
]
if pool_name:
cmd.append(pool_name)
result = self.system.run_command(cmd)
for line in result.stdout.strip().split('\n'):
if line:
parts = line.split('\t')
if len(parts) >= 8:
dataset = ZFSDataset(
name=parts[0],
type=parts[1],
used=int(parts[2]),
available=int(parts[3]),
referenced=int(parts[4]),
mountpoint=parts[5],
compression=parts[6],
readonly=parts[7] == 'on'
)
datasets.append(dataset)
except subprocess.CalledProcessError as e:
self.console.print(f"❌ Error listando datasets: {e}", style="red")
return datasets
def create_pool(self, name: str, raid_type: str, devices: List[str],
ashift: Optional[int] = None, force: bool = False) -> bool:
"""Crea un nuevo pool ZFS"""
try:
self.console.print(f"🏗️ Creando pool ZFS '{name}' con tipo {raid_type}...")
# Construir comando
cmd = [self.zpool_cmd, 'create']
if force:
cmd.append('-f')
# Ashift
if ashift:
cmd.extend(['-o', f'ashift={ashift}'])
cmd.append(name)
# Tipo de RAID y dispositivos
if raid_type == 'mirror':
cmd.append('mirror')
elif raid_type in ['raidz1', 'raidz2', 'raidz3']:
cmd.append(raid_type.replace('1', '').replace('2', '2').replace('3', '3'))
# Agregar dispositivos
for device in devices:
cmd.append(f'/dev/{device}')
self.system.run_command(cmd)
self.console.print(f"✅ Pool '{name}' creado exitosamente", style="green")
return True
except subprocess.CalledProcessError as e:
self.console.print(f"❌ Error creando pool: {e}", style="red")
return False
def destroy_pool(self, name: str, force: bool = False) -> bool:
"""Destruye un pool ZFS"""
try:
cmd = [self.zpool_cmd, 'destroy']
if force:
cmd.append('-f')
cmd.append(name)
self.system.run_command(cmd)
self.console.print(f"✅ Pool '{name}' eliminado", style="green")
return True
except subprocess.CalledProcessError as e:
self.console.print(f"❌ Error eliminando pool: {e}", style="red")
return False
def create_dataset(self, name: str, mountpoint: Optional[str] = None,
properties: Optional[Dict[str, str]] = None) -> bool:
"""Crea un dataset ZFS"""
try:
cmd = [self.zfs_cmd, 'create']
# Propiedades
if properties:
for key, value in properties.items():
cmd.extend(['-o', f'{key}={value}'])
# Mountpoint
if mountpoint:
cmd.extend(['-o', f'mountpoint={mountpoint}'])
cmd.append(name)
self.system.run_command(cmd)
self.console.print(f"✅ Dataset '{name}' creado", style="green")
return True
except subprocess.CalledProcessError as e:
self.console.print(f"❌ Error creando dataset: {e}", style="red")
return False
def add_cache_device(self, pool_name: str, device: str, device_type: str = 'cache') -> bool:
"""Agrega dispositivo de cache (L2ARC) o log (SLOG) a un pool"""
try:
cmd = [self.zpool_cmd, 'add', pool_name]
if device_type == 'log':
cmd.append('log')
elif device_type == 'cache':
cmd.append('cache')
cmd.append(f'/dev/{device}')
self.system.run_command(cmd)
self.console.print(f"✅ Dispositivo {device_type} agregado al pool", style="green")
return True
except subprocess.CalledProcessError as e:
self.console.print(f"❌ Error agregando dispositivo: {e}", style="red")
return False
def get_pool_io_stats(self, pool_name: str) -> Dict:
"""Obtiene estadísticas de I/O de un pool"""
stats = {}
try:
result = self.system.run_command([self.zpool_cmd, 'iostat', '-v', pool_name])
# Parsear output de iostat
# TODO: Implementar parsing completo
stats['raw_output'] = result.stdout
except subprocess.CalledProcessError:
pass
return stats
def scrub_pool(self, pool_name: str) -> bool:
"""Inicia scrub en un pool"""
try:
self.system.run_command([self.zpool_cmd, 'scrub', pool_name])
self.console.print(f"✅ Scrub iniciado en pool '{pool_name}'", style="green")
return True
except subprocess.CalledProcessError as e:
self.console.print(f"❌ Error iniciando scrub: {e}", style="red")
return False
def calculate_optimal_ashift(self, devices: List[str]) -> int:
"""Calcula el ashift óptimo basado en los dispositivos"""
max_sector_size = 512
for device in devices:
try:
# Obtener tamaño de sector físico
result = self.system.run_command([
'lsblk', '-dno', 'PHY-SEC', f'/dev/{device}'
])
sector_size = int(result.stdout.strip())
max_sector_size = max(max_sector_size, sector_size)
except (subprocess.CalledProcessError, ValueError):
continue
# Calcular ashift basado en tamaño de sector
# Siempre usar mínimo 12 (4K) para compatibilidad con SSDs
if max_sector_size <= 512:
return 12 # 4K para compatibilidad
elif max_sector_size <= 4096:
return 12 # 4K
elif max_sector_size <= 8192:
return 13 # 8K
else:
return 14 # 16K