feat(images): backend catalog + mirror model + download API/worker

Phase 1 of VM image mirror feature:
- StorageMixin.download_url (PVE download-url/aria2) + get_download_target_storages
- builtin curated cloud-image catalog (Ubuntu/Debian/Rocky/Alma, amd64+arm64)
- ImageMirror model for admin custom mirrors
- /proxmox/api/images: catalog, lxc-templates proxy, storages, download
- _do_image_download_sync worker with log-tailing progress (DeployTask)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 22:51:21 +05:00
parent 4d8cdc0143
commit 7aed5dd97e
9 changed files with 546 additions and 1 deletions

View File

@@ -80,6 +80,67 @@ class StorageMixin:
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 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: