Refactor backend: Split models and proxmox_client into modules, add alembic
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m27s
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m27s
This commit is contained in:
194
backend/app/proxmox/mixins/lxc.py
Normal file
194
backend/app/proxmox/mixins/lxc.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from typing import List, Dict, Optional, Union, Any
|
||||
import time
|
||||
import urllib3
|
||||
import shlex
|
||||
from loguru import logger
|
||||
|
||||
class LxcMixin:
|
||||
def start_container(self, node: str, vmid: int) -> Optional[str]:
|
||||
"""Запустить LXC контейнер. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return None
|
||||
|
||||
try:
|
||||
upid = self.proxmox.nodes(node).lxc(vmid).status.start.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запуска LXC {vmid} на {node}: {e}")
|
||||
return None
|
||||
|
||||
def stop_container(self, node: str, vmid: int, force: bool = False) -> Optional[str]:
|
||||
"""Остановить LXC контейнер. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return None
|
||||
|
||||
try:
|
||||
upid = self.proxmox.nodes(node).lxc(vmid).status.stop.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки LXC {vmid} на {node}: {e}")
|
||||
return None
|
||||
|
||||
def restart_container(self, node: str, vmid: int) -> Optional[str]:
|
||||
"""Перезапустить LXC контейнер. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return None
|
||||
|
||||
try:
|
||||
upid = self.proxmox.nodes(node).lxc(vmid).status.reboot.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка перезапуска LXC {vmid} на {node}: {e}")
|
||||
return None
|
||||
|
||||
def shutdown_container(self, node: str, vmid: int, timeout: int = 60) -> Optional[str]:
|
||||
"""Корректно выключить LXC. Возвращает UPID задачи."""
|
||||
if not self.proxmox:
|
||||
return None
|
||||
try:
|
||||
upid = self.proxmox.nodes(node).lxc(vmid).status.shutdown.post(timeout=timeout)
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка корректного выключения LXC {vmid} на {node}: {e}")
|
||||
return None
|
||||
|
||||
def clone_container(self, node: str, vmid: int, new_vmid: int, hostname: str,
|
||||
full: bool = True, target_node: Optional[str] = None,
|
||||
target_storage: Optional[str] = None,
|
||||
description: Optional[str] = None) -> Optional[str]:
|
||||
"""Клонировать LXC контейнер."""
|
||||
if not self.proxmox:
|
||||
return None
|
||||
try:
|
||||
params = {'newid': new_vmid, 'hostname': hostname, 'full': 1 if full else 0}
|
||||
if target_storage:
|
||||
params['storage'] = target_storage
|
||||
if target_node and target_node != node:
|
||||
params['target'] = target_node
|
||||
if description:
|
||||
params['description'] = description
|
||||
upid = self.proxmox.nodes(node).lxc(vmid).clone.post(**params)
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка клонирования LXC {vmid} -> {new_vmid}: {e}")
|
||||
return None
|
||||
|
||||
def change_container_password(self, node: str, vmid: int, username: str, password: str) -> Dict:
|
||||
"""
|
||||
Сменить пароль пользователя в LXC через `pct exec` (chpasswd).
|
||||
Контейнер должен быть запущен.
|
||||
"""
|
||||
if not self.proxmox:
|
||||
return {'success': False, 'error': 'Proxmox connection not initialized'}
|
||||
try:
|
||||
result = self.proxmox.nodes(node).lxc(vmid).status.current.get()
|
||||
if result.get('status') != 'running':
|
||||
return {'success': False, 'error': 'Container is not running'}
|
||||
# Используем printf + chpasswd; shlex.quote защищает от shell-injection
|
||||
# Proxmox exec API ожидает отдельные поля: command + args[N]
|
||||
entry = shlex.quote(f'{username}:{password}')
|
||||
cmd_str = f"printf '%s\\n' {entry} | chpasswd"
|
||||
data = {'command': 'bash', 'args[0]': '-c', 'args[1]': cmd_str}
|
||||
res = self.proxmox.nodes(node).lxc(vmid).exec.post(**data)
|
||||
return {'success': True, 'result': res}
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось сменить пароль в LXC {vmid}: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def set_container_notes(self, node: str, vmid: int, description: str) -> bool:
|
||||
"""Установить description (заметки) у LXC."""
|
||||
if not self.proxmox:
|
||||
return False
|
||||
try:
|
||||
self.proxmox.nodes(node).lxc(vmid).config.put(description=description or '')
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления заметок LXC {vmid}: {e}")
|
||||
return False
|
||||
|
||||
def force_stop_container(self, node: str, vmid: int) -> bool:
|
||||
"""Принудительно остановить LXC контейнер (через SSH - аналог kill -9)"""
|
||||
if not self.proxmox or not self.host:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Пробуем остановить через SSH команду
|
||||
# Сначала получаем информацию о контейнере
|
||||
container_info = self.proxmox.nodes(node).lxc(vmid).status.current.get()
|
||||
if container_info.get('status') != 'running':
|
||||
logger.info(f"Контейнер {vmid} на {node} уже остановлен")
|
||||
return True
|
||||
|
||||
# Используем обычный stop - Proxmox должен остановить его
|
||||
try:
|
||||
self.proxmox.nodes(node).lxc(vmid).status.stop.post()
|
||||
logger.info(f"Принудительная остановка контейнера {vmid} на {node} выполнена")
|
||||
return True
|
||||
except Exception as api_error:
|
||||
logger.warning(f"Прямой stop не сработал, ошибка: {api_error}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка принудительной остановки контейнера {vmid}: {e}")
|
||||
return False
|
||||
|
||||
def get_container_status(self, node: str, vmid: int) -> Optional[Dict]:
|
||||
"""Получить статус конкретного LXC контейнера"""
|
||||
if not self.proxmox:
|
||||
return None
|
||||
|
||||
try:
|
||||
status = self.proxmox.nodes(node).lxc(vmid).status.current.get()
|
||||
return status
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения статуса LXC {vmid} на {node}: {e}")
|
||||
return None
|
||||
|
||||
def get_container_stats(self, node: str, vmid: int) -> Optional[Dict]:
|
||||
"""Получить статистику LXC контейнера (CPU, память, диск)"""
|
||||
return self.get_container_status(node, vmid)
|
||||
|
||||
def get_container_rrddata(self, node: str, vmid: int, timeframe: str = "hour") -> Dict:
|
||||
"""
|
||||
Получить исторические данные контейнера для графиков
|
||||
|
||||
Args:
|
||||
node: Имя ноды
|
||||
vmid: ID контейнера
|
||||
timeframe: Период времени (hour, day, week, month, year)
|
||||
|
||||
Returns:
|
||||
Dict с временными рядами данных
|
||||
"""
|
||||
if not self.proxmox:
|
||||
return {}
|
||||
|
||||
try:
|
||||
rrddata = self.proxmox.nodes(node).lxc(vmid).rrddata.get(timeframe=timeframe)
|
||||
return {'data': rrddata, 'timeframe': timeframe}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения RRD данных LXC {vmid} на {node}: {e}")
|
||||
return {}
|
||||
|
||||
def get_container_vnc(self, node: str, vmid: int) -> Dict:
|
||||
"""
|
||||
Получить данные для VNC подключения к LXC контейнеру
|
||||
|
||||
Args:
|
||||
node: Имя ноды
|
||||
vmid: ID контейнера
|
||||
|
||||
Returns:
|
||||
Dict с данными VNC (port, ticket, cert, upid)
|
||||
"""
|
||||
if not self.proxmox:
|
||||
return {}
|
||||
|
||||
try:
|
||||
# Создать терминал прокси сессию для LXC
|
||||
vnc_data = self.proxmox.nodes(node).lxc(vmid).vncproxy.post(websocket=1)
|
||||
return vnc_data
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения VNC для LXC {vmid} на {node}: {e}")
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user