From 8ff757f8c1bbbdd99ecce5963fa0da1a5bf7b13a Mon Sep 17 00:00:00 2001 From: Dilshod Rasulov Date: Fri, 14 Aug 2026 16:19:41 +0500 Subject: [PATCH] fix: re-download cached import images when checksum is provided volume_exists() only checked file presence, not content. A stale image from a previous download (different URL/version) would be silently reused, causing templates and VMs to be created from unpatched images even when the mirror's checksum pointed to a newer patched build. Add delete_volume() to ProxmoxClient storage mixin, and update both _do_image_download_sync and _do_image_template_sync to delete the cached volume and re-download whenever a checksum is provided, letting Proxmox verify the new download server-side. --- backend/app/api/proxmox/async_ops.py | 29 ++++++++++++++++++++------- backend/app/proxmox/mixins/storage.py | 15 ++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/backend/app/api/proxmox/async_ops.py b/backend/app/api/proxmox/async_ops.py index cbdcf85..47d99c3 100644 --- a/backend/app/api/proxmox/async_ops.py +++ b/backend/app/api/proxmox/async_ops.py @@ -886,12 +886,18 @@ def _do_image_download_sync(task_id: int, server_id: int, node: str, storage: st else: dl_filename = filename - # Идемпотентность: если файл уже есть в хранилище — не качаем повторно + # Идемпотентность: если файл уже есть в хранилище — не качаем повторно, + # но при наличии контрольной суммы удаляем кэш и перекачиваем, чтобы + # Proxmox проверил актуальность образа. existing_volid = f'{storage}:{content}/{dl_filename}' if client.volume_exists(node, storage, existing_volid): - _update_deploy_task(task_id, 'completed', f'Образ {dl_filename} уже в хранилище', 100, node=node) - logger.info(f"[IMG DL #{task_id}] {dl_filename} already present on {node}:{storage}") - return + if checksum: + logger.info(f"[IMG DL #{task_id}] Removing cached {existing_volid} to re-verify checksum") + client.delete_volume(node, storage, existing_volid) + else: + _update_deploy_task(task_id, 'completed', f'Образ {dl_filename} уже в хранилище', 100, node=node) + logger.info(f"[IMG DL #{task_id}] {dl_filename} already present on {node}:{storage}") + return _update_deploy_task(task_id, 'running', f'Старт загрузки {dl_filename}...', 15, node=node) try: @@ -1057,10 +1063,19 @@ def _do_image_template_sync(task_id: int, server_id: int, node: str, filename = _normalize_import_filename(filename) import_volid = f'{import_storage}:import/{filename}' + need_download = True if client.volume_exists(node, import_storage, import_volid): - # Образ уже скачан прошлой попыткой — пропускаем загрузку (идемпотентность) - _update_deploy_task(task_id, 'running', 'Образ уже загружен, пропуск...', 40, node=node) - else: + if checksum: + # Есть кэшированный файл, но задана контрольная сумма — удаляем + # и перекачиваем: Proxmox проверит сумму при новой загрузке. + logger.info(f"[IMG TPL #{task_id}] Removing cached {import_volid} to re-verify checksum") + client.delete_volume(node, import_storage, import_volid) + else: + # Без контрольной суммы доверяем кэшу (чистая идемпотентность) + need_download = False + _update_deploy_task(task_id, 'running', 'Образ уже загружен, пропуск...', 40, node=node) + + if need_download: _update_deploy_task(task_id, 'running', f'Загрузка образа {filename}...', 10, node=node) try: dl_upid = client.download_url(node, import_storage, url, 'import', filename, diff --git a/backend/app/proxmox/mixins/storage.py b/backend/app/proxmox/mixins/storage.py index d6a9883..fedcf38 100644 --- a/backend/app/proxmox/mixins/storage.py +++ b/backend/app/proxmox/mixins/storage.py @@ -170,6 +170,21 @@ class StorageMixin: logger.debug(f"volume_exists check failed {storage}/{volid}: {e}") return False + def delete_volume(self, node: str, storage: str, volid: str) -> bool: + """Удалить том из хранилища. volid вида 'storage:content/filename'. + Возвращает True при успехе, False при ошибке.""" + if not self.proxmox: + return False + # proxmoxer принимает volume без префикса хранилища + volume = volid.split(':', 1)[1] if ':' in volid else volid + try: + self.proxmox.nodes(node).storage(storage).content(volume).delete() + logger.info(f"Deleted volume {volid} from {node}:{storage}") + return True + except Exception as e: + logger.warning(f"delete_volume failed {node}:{storage}/{volume}: {e}") + return False + def get_download_target_storages(self, node: str, content: str) -> List[Dict]: """ Получить активные хранилища ноды, поддерживающие указанный тип контента