Refactor backend: Split models and proxmox_client into modules, add alembic
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m27s

This commit is contained in:
2026-06-13 03:53:27 +05:00
parent 8a879f36fa
commit bfd92102cf
54 changed files with 6026 additions and 5137 deletions

View File

@@ -0,0 +1,310 @@
from typing import List, Dict, Optional, Union, Any
import time
import urllib3
from loguru import logger
class ClusterMixin:
def is_cluster(self) -> bool:
"""Проверить, является ли сервер частью кластера"""
if not self.proxmox:
return False
try:
# Проверяем через cluster/status - это наиболее надёжный способ
cluster_status = self.proxmox.cluster.status.get()
# В кластере будет тип 'cluster' с информацией
for item in cluster_status:
if item.get('type') == 'cluster':
# Это кластер
return True
# Также проверяем количество нод
nodes = [item for item in cluster_status if item.get('type') == 'node']
return len(nodes) > 1
except Exception as e:
# Если /cluster/status не работает, пробуем через ноды
try:
nodes = self.get_nodes()
return len(nodes) > 1
except Exception:
return False
def get_ha_resources(self) -> List[Dict]:
"""Получить список ресурсов в HA"""
if not self.proxmox:
return []
try:
resources = self.proxmox.cluster.ha.resources.get()
return resources
except Exception as e:
logger.error(f"Ошибка получения HA ресурсов: {e}")
return []
def get_ha_groups(self) -> List[Dict]:
"""Получить список HA групп"""
if not self.proxmox:
return []
try:
groups = self.proxmox.cluster.ha.groups.get()
return groups
except Exception as e:
logger.error(f"Ошибка получения HA групп: {e}")
return []
def is_in_ha(self, vmid: int, vm_type: str = 'vm') -> bool:
"""
Проверить, находится ли VM/контейнер в HA
Args:
vmid: ID виртуальной машины или контейнера
vm_type: 'vm' или 'ct'
"""
if not self.proxmox:
return False
try:
sid = f"{vm_type}:{vmid}"
resources = self.get_ha_resources()
return any(r.get('sid') == sid for r in resources)
except Exception:
return False
def add_to_ha(self, vmid: int, vm_type: str = 'vm', group: str = None,
max_restart: int = 1, max_relocate: int = 1,
state: str = 'started', comment: str = None) -> Dict:
"""
Добавить VM/контейнер в HA
Args:
vmid: ID виртуальной машины или контейнера
vm_type: 'vm' для QEMU, 'ct' для LXC
group: Имя HA группы (опционально)
max_restart: Максимальное количество перезапусков при сбое
max_relocate: Максимальное количество перемещений на другую ноду
state: Целевое состояние: 'started', 'stopped', 'enabled', 'disabled', 'ignored'
comment: Комментарий
Returns:
Dict с результатом операции
"""
if not self.proxmox:
return {'success': False, 'error': 'Not connected'}
try:
sid = f"{vm_type}:{vmid}"
# Проверяем, не добавлен ли уже
if self.is_in_ha(vmid, vm_type):
return {'success': False, 'error': 'Already in HA', 'already_in_ha': True}
params = {
'sid': sid,
'max_restart': max_restart,
'max_relocate': max_relocate,
'state': state
}
if group:
params['group'] = group
if comment:
params['comment'] = comment
self.proxmox.cluster.ha.resources.post(**params)
logger.info(f"Added {sid} to HA")
return {'success': True, 'sid': sid}
except Exception as e:
logger.error(f"Ошибка добавления {vmid} в HA: {e}")
return {'success': False, 'error': str(e)}
def remove_from_ha(self, vmid: int, vm_type: str = 'vm') -> Dict:
"""
Удалить VM/контейнер из HA
Args:
vmid: ID виртуальной машины или контейнера
vm_type: 'vm' для QEMU, 'ct' для LXC
Returns:
Dict с результатом операции
"""
if not self.proxmox:
return {'success': False, 'error': 'Not connected'}
try:
sid = f"{vm_type}:{vmid}"
# Проверяем, что ресурс в HA
if not self.is_in_ha(vmid, vm_type):
return {'success': False, 'error': 'Not in HA', 'not_in_ha': True}
self.proxmox.cluster.ha.resources(sid).delete()
logger.info(f"Removed {sid} from HA")
return {'success': True, 'sid': sid}
except Exception as e:
logger.error(f"Ошибка удаления {vmid} из HA: {e}")
return {'success': False, 'error': str(e)}
def get_ha_status(self, vmid: int, vm_type: str = 'vm') -> Optional[Dict]:
"""
Получить статус HA для VM/контейнера
Returns:
Dict со статусом HA или None если не в HA
"""
if not self.proxmox:
return None
try:
sid = f"{vm_type}:{vmid}"
resources = self.get_ha_resources()
for r in resources:
if r.get('sid') == sid:
return {
'in_ha': True,
'state': r.get('state'),
'group': r.get('group'),
'max_restart': r.get('max_restart'),
'max_relocate': r.get('max_relocate'),
'comment': r.get('comment')
}
return {'in_ha': False}
except Exception as e:
logger.error(f"Ошибка получения HA статуса {vmid}: {e}")
return None
def create_cluster(self, cluster_name: str, link0: str = None) -> Dict:
"""
Создать новый Proxmox кластер на этой ноде.
Требует аутентификации через пароль (root@pam).
Args:
cluster_name: Уникальное имя кластера (не изменяемо после создания)
link0: IP-адрес для corosync link0 (по умолчанию — IP ноды)
Returns:
Dict с UPID задачи или ошибкой
"""
if not self.proxmox:
return {'success': False, 'error': 'Not connected'}
try:
params = {'clustername': cluster_name}
if link0:
params['link0'] = link0
result = self.proxmox.cluster.config.post(**params)
logger.info(f"Cluster '{cluster_name}' creation started on {self.host}, UPID: {result}")
return {'success': True, 'upid': result, 'cluster_name': cluster_name}
except Exception as e:
logger.error(f"Ошибка создания кластера '{cluster_name}' на {self.host}: {e}")
return {'success': False, 'error': str(e)}
def get_cluster_join_info(self) -> Dict:
"""
Получить информацию для присоединения к кластеру (fingerprint, join token).
Вызывается на существующей кластерной ноде.
Returns:
Dict с полями: totem, nodelist, fingerprint, config_digest
"""
if not self.proxmox:
return {'success': False, 'error': 'Not connected'}
try:
info = self.proxmox.cluster.config.join.get()
logger.info(f"Got cluster join info from {self.host}")
return {'success': True, 'data': info}
except Exception as e:
logger.error(f"Ошибка получения join info с {self.host}: {e}")
return {'success': False, 'error': str(e)}
def join_cluster(self, cluster_host: str, rootpw: str, fingerprint: str,
link0: str = None) -> Dict:
"""
Присоединить эту ноду к существующему кластеру.
ВАЖНО: Нода должна быть пустой — /etc/pve будет перезаписан.
Args:
cluster_host: IP/hostname существующей кластерной ноды
rootpw: root-пароль кластерной ноды (нужен для join)
fingerprint: SHA-256 fingerprint кластерного сертификата
link0: Локальный IP для corosync link0 (по умолчанию — IP ноды)
Returns:
Dict с UPID задачи или ошибкой
"""
if not self.proxmox:
return {'success': False, 'error': 'Not connected'}
try:
params = {
'hostname': cluster_host,
'password': rootpw,
'fingerprint': fingerprint,
}
if link0:
params['link0'] = link0
result = self.proxmox.cluster.config.join.post(**params)
logger.info(f"Node {self.host} joining cluster at {cluster_host}, UPID: {result}")
return {'success': True, 'upid': result}
except Exception as e:
logger.error(f"Ошибка присоединения {self.host} к кластеру {cluster_host}: {e}")
return {'success': False, 'error': str(e)}
def delete_cluster_node(self, node_name: str) -> Dict:
"""
Удалить ноду из кластера (pvecm delnode).
Вызывается на ДРУГОЙ ноде кластера, не на удаляемой.
Удаляемая нода должна быть ВЫКЛЮЧЕНА.
Args:
node_name: Имя ноды для удаления (hostname)
Returns:
Dict с результатом
"""
if not self.proxmox:
return {'success': False, 'error': 'Not connected'}
try:
# Proxmox PVE 8 API: DELETE /nodes/{node}
self.proxmox.nodes(node_name).delete()
logger.info(f"Node '{node_name}' deleted from cluster via {self.host}")
return {'success': True, 'node': node_name}
except Exception as e:
logger.warning(f"REST API delete node failed, trying SSH fallback: {e}")
# SSH fallback через pvecm delnode
return self._delete_cluster_node_via_ssh(node_name)
def _delete_cluster_node_via_ssh(self, node_name: str) -> Dict:
"""SSH fallback для удаления ноды через pvecm delnode"""
from app.ssh_client import SSHClient
ssh = SSHClient(
hostname=self.host,
username='root',
password=getattr(self, '_password', None),
)
try:
if not ssh.connect():
return {'success': False, 'error': 'SSH connection failed'}
output, exit_code = ssh.execute(f"pvecm delnode {node_name}", return_exit_code=True)
ssh.close()
# "Could not kill node (error = CS_ERR_NOT_EXIST)" — ignorable per Proxmox docs
if exit_code == 0 or (output and 'CS_ERR_NOT_EXIST' in output):
logger.info(f"Node '{node_name}' removed via pvecm delnode (SSH)")
return {'success': True, 'node': node_name, 'method': 'ssh'}
else:
return {'success': False, 'error': output or 'pvecm delnode failed'}
except Exception as e:
logger.error(f"SSH pvecm delnode failed for {node_name}: {e}")
return {'success': False, 'error': str(e)}