Files
pvemanager/backend/app/proxmox/mixins/storage.py
Dilshod Rasulov f880c9e177
All checks were successful
Backend Tests / pytest (Python 3.12) (push) Successful in 52s
Deploy to Production / Trigger production update (push) Successful in 8s
fix(images): idempotent download (skip if file exists) + real failure reason
Retrying a template build failed at download because the qcow2 from a prior
attempt already existed and download-url refuses to overwrite. Now skip the
download when the volume already exists (volume_exists), and surface the PVE
task-log tail on download failure instead of a generic message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:38:50 +05:00

218 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import List, Dict, Optional, Union, Any
import time
import urllib3
from loguru import logger
class StorageMixin:
def get_node_isos(self, node: str) -> List[Dict]:
"""Получить список ISO-образов со всех ISO-хранилищ ноды."""
if not self.proxmox:
return []
result: List[Dict] = []
try:
storages = self.proxmox.nodes(node).storage.get()
for s in storages:
content = s.get('content', '')
if 'iso' not in content:
continue
storage_id = s.get('storage')
try:
items = self.proxmox.nodes(node).storage(storage_id).content.get(content='iso')
for it in items:
result.append({
'volid': it.get('volid'),
'storage': storage_id,
'size': it.get('size'),
'format': it.get('format'),
'name': (it.get('volid') or '').split('/')[-1],
})
except Exception as e:
logger.debug(f"Не удалось получить ISO из {storage_id}: {e}")
except Exception as e:
logger.error(f"Ошибка получения списка ISO для ноды {node}: {e}")
return result
def get_lxc_storage_templates(self, node: Optional[str] = None) -> List[Dict]:
"""
Получить список LXC CT-шаблонов из хранилищ Proxmox.
Ищет файлы типа vzdump/vztmpl на всех нодах (или на указанной).
Возвращает список объектов с полями: volid, storage, node, name, size.
"""
if not self.proxmox:
return []
result: List[Dict] = []
try:
if node:
nodes_list = [{'node': node}]
else:
nodes_list = self.get_nodes()
seen_volids: set = set()
for n in nodes_list:
node_name = n.get('node')
try:
storages = self.proxmox.nodes(node_name).storage.get()
for s in storages:
content = s.get('content', '')
if 'vztmpl' not in content:
continue
storage_id = s.get('storage')
try:
items = self.proxmox.nodes(node_name).storage(storage_id).content.get(content='vztmpl')
for it in items:
volid = it.get('volid', '')
if volid in seen_volids:
continue
seen_volids.add(volid)
filename = volid.split('/')[-1] if '/' in volid else volid
result.append({
'volid': volid,
'storage': storage_id,
'node': node_name,
'name': filename,
'size': it.get('size', 0),
})
except Exception as e:
logger.debug(f"Не удалось получить CT-шаблоны из {storage_id} на {node_name}: {e}")
except Exception as e:
logger.error(f"Ошибка получения хранилищ ноды {node_name}: {e}")
except Exception as e:
logger.error(f"Ошибка получения CT-шаблонов {self.host}: {e}")
return result
def download_url(self, node: str, storage: str, url: str, content: str,
filename: str, checksum: Optional[str] = None,
checksum_algorithm: Optional[str] = None) -> Optional[str]:
"""
Скачать файл (ISO / vztmpl / образ для импорта) по URL прямо на ноду
средствами Proxmox (aria2). Использует endpoint
POST /nodes/{node}/storage/{storage}/download-url.
Args:
node: имя ноды
storage: целевое хранилище
url: прямой URL к образу (.img/.qcow2/.iso/.tar.zst ...)
content: тип контента ('iso', 'vztmpl', 'import')
filename: имя файла в хранилище
checksum: контрольная сумма (опционально)
checksum_algorithm: алгоритм (md5/sha1/sha224/sha256/sha384/sha512)
Returns:
UPID задачи или None
"""
if not self.proxmox:
return None
try:
params = {
'url': url,
'content': content,
'filename': filename,
}
if checksum and checksum_algorithm:
params['checksum'] = checksum
params['checksum-algorithm'] = checksum_algorithm
# 'download-url' содержит дефис — обращаемся через call-форму proxmoxer
result = self.proxmox.nodes(node).storage(storage)('download-url').post(**params)
logger.info(f"Запущена загрузка {filename} ({content}) на {node}:{storage}, UPID: {result}")
return result
except Exception as e:
logger.error(f"Ошибка загрузки {url} на {node}:{storage}: {e}")
raise
def volume_exists(self, node: str, storage: str, volid: str) -> bool:
"""Проверить, есть ли уже том volid в хранилище (для идемпотентной загрузки)."""
if not self.proxmox:
return False
try:
items = self.proxmox.nodes(node).storage(storage).content.get()
return any(it.get('volid') == volid for it in (items or []))
except Exception as e:
logger.debug(f"volume_exists check failed {storage}/{volid}: {e}")
return False
def get_download_target_storages(self, node: str, content: str) -> List[Dict]:
"""
Получить активные хранилища ноды, поддерживающие указанный тип контента
(для выбора цели загрузки образов).
Args:
node: имя ноды
content: 'iso' | 'vztmpl' | 'images' | 'import'
"""
if not self.proxmox:
return []
result: List[Dict] = []
try:
for s in self.proxmox.nodes(node).storage.get():
if s.get('active', 1) == 0 or s.get('enabled', 1) == 0:
continue
if content in (s.get('content', '') or '').split(','):
result.append(s)
except Exception as e:
logger.error(f"Ошибка получения хранилищ ({content}) ноды {node}: {e}")
return result
def get_node_storages(self, node: str, content: str = None) -> List[Dict]:
"""Get storages available on a specific node, optionally filtered by content type"""
if not self.proxmox:
return []
try:
params = {}
if content:
params['content'] = content
storages = self.proxmox.nodes(node).storage.get(**params)
return storages
except Exception as e:
logger.error(f"Error getting storages for node {node}: {e}")
return []
def vzdump_guest(self, node: str, vmid: int, storage: str,
compress: str = 'zstd', mode: str = 'snapshot') -> Dict:
"""
Создать резервную копию VM/LXC через vzdump.
Args:
node: Имя ноды
vmid: ID ВМ или контейнера
storage: Хранилище для бэкапа (должно поддерживать content=backup)
compress: Алгоритм сжатия (zstd, lzo, gzip, 0)
mode: Режим снятия бэкапа (snapshot, suspend, stop)
Returns:
Dict с UPID задачи
"""
if not self.proxmox:
return {'success': False, 'error': 'Not connected'}
try:
upid = self.proxmox.nodes(node).vzdump.post(
vmid=vmid,
storage=storage,
compress=compress,
mode=mode,
remove=0, # не удалять старые бэкапы автоматически
)
logger.info(f"vzdump started for vmid {vmid} on {node}, storage {storage}, UPID: {upid}")
return {'success': True, 'upid': upid, 'vmid': vmid}
except Exception as e:
logger.error(f"Ошибка vzdump vmid {vmid} на {node}: {e}")
return {'success': False, 'error': str(e), 'vmid': vmid}
def get_backup_storages(self, node: str) -> List[Dict]:
"""
Получить список хранилищ, поддерживающих content=backup на ноде.
Returns:
Список Dict с полями: storage, type, content, avail, total
"""
if not self.proxmox:
return []
try:
storages = self.proxmox.nodes(node).storage.get(content='backup', enabled=1)
return storages
except Exception as e:
logger.error(f"Ошибка получения backup-хранилищ для {node}: {e}")
return []