fix(delete): считать уже отсутствующий в Proxmox ресурс успешно удалённым
This commit is contained in:
@@ -7,6 +7,7 @@ import string
|
||||
import time
|
||||
|
||||
from proxmoxer import ProxmoxAPI
|
||||
from proxmoxer.core import ResourceException
|
||||
|
||||
from .config import settings
|
||||
|
||||
@@ -26,7 +27,7 @@ def _client() -> ProxmoxAPI:
|
||||
|
||||
|
||||
def gen_password(length: int = 14) -> str:
|
||||
"""Генерирует пароль из букв и цифр (для VM-cloud-init, если пользователь не задал)."""
|
||||
"""Генерирует пароль из букв и цифр."""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(random.choice(alphabet) for _ in range(length))
|
||||
|
||||
@@ -64,25 +65,9 @@ def create_lxc(
|
||||
storage: str = "local-lvm",
|
||||
node: str = None,
|
||||
) -> str:
|
||||
"""Создаёт LXC из архивного шаблона и запускает его.
|
||||
|
||||
Шаги (каждый — отдельный запрос к Proxmox API):
|
||||
1. Создаёт LXC через ``ostemplate`` (без запуска).
|
||||
2. Задаёт root-пароль через ``/passwd``.
|
||||
3. Создаёт rootfs указанного размера через ``/resize``.
|
||||
4. Запускает контейнер через ``/status/start``.
|
||||
|
||||
``password`` приходит из формы (клиент задаёт сам) и сохраняется в БД
|
||||
для одноразового показа в карточке инстанса.
|
||||
|
||||
LXC создаётся ТОЛЬКО из архивного шаблона (vztmpl на storage).
|
||||
Клонирование существующего LXC по VMID не поддерживается.
|
||||
"""
|
||||
"""Создаёт LXC из архива: ostemplate → passwd → resize → start."""
|
||||
node = node or settings.pve_node
|
||||
px = _client()
|
||||
|
||||
# 1. Создаём LXC через ostemplate. start=0 — не запускаем, чтобы успеть
|
||||
# задать пароль и диск нужного размера до старта.
|
||||
upid = px.nodes(node).lxc.post(
|
||||
vmid=new_vmid,
|
||||
hostname=name,
|
||||
@@ -96,23 +81,14 @@ def create_lxc(
|
||||
start=0,
|
||||
)
|
||||
_wait_task(px, node, upid)
|
||||
|
||||
# 2. Задаём пароль через отдельный endpoint /passwd.
|
||||
# Поле password в POST /lxc на некоторых версиях Proxmox игнорируется,
|
||||
# поэтому дублируем через /passwd — это идемпотентная операция.
|
||||
px.nodes(node).lxc(new_vmid).passwd.post(password=password)
|
||||
|
||||
# 3. Создаём rootfs указанного размера (storage:size_G).
|
||||
px.nodes(node).lxc(new_vmid).resize.put(
|
||||
disk="rootfs",
|
||||
size=f"{storage}:{disk_gb}",
|
||||
)
|
||||
|
||||
# 4. Запускаем контейнер.
|
||||
upid = px.nodes(node).lxc(new_vmid).status.start.post()
|
||||
if upid:
|
||||
_wait_task(px, node, upid)
|
||||
|
||||
return password
|
||||
|
||||
|
||||
@@ -137,8 +113,7 @@ _DISK_KEYS = ("scsi0", "virtio0", "ide0", "sata0")
|
||||
def _disk_size_from_config(cfg: dict) -> int:
|
||||
"""Извлекает размер диска VM в ГБ."""
|
||||
for key in _DISK_KEYS:
|
||||
value = cfg.get(key, "")
|
||||
match = re.search(r"size=(\d+)G", value)
|
||||
match = re.search(r"size=(\d+)G", cfg.get(key, ""))
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return 10
|
||||
@@ -163,7 +138,7 @@ def list_vm_templates(node: str = None) -> list:
|
||||
|
||||
|
||||
def list_lxc_templates(node: str = None) -> list:
|
||||
"""Возвращает архивные LXC-шаблоны (vztmpl) из доступных хранилищ."""
|
||||
"""Возвращает архивные LXC-шаблоны (vztmpl)."""
|
||||
node = node or settings.pve_node
|
||||
px = _client()
|
||||
result = []
|
||||
@@ -276,6 +251,16 @@ def guest_action(guest_type: str, vmid: int, action: str, node: str = None) -> N
|
||||
getattr(endpoint.status, action).post()
|
||||
|
||||
|
||||
def _is_missing_guest_error(exc: Exception) -> bool:
|
||||
"""Проверяет, что Proxmox сообщает об отсутствии VM/LXC."""
|
||||
message = str(exc).lower()
|
||||
return (
|
||||
"does not exist" in message
|
||||
or "not found" in message
|
||||
or "configuration file" in message and "does not exist" in message
|
||||
)
|
||||
|
||||
|
||||
def _wait_guest_stopped(px: ProxmoxAPI, guest_type: str, vmid: int, node: str, timeout: int = 180) -> None:
|
||||
"""Ожидает, пока VM или LXC перейдёт в состояние stopped."""
|
||||
endpoint = px.nodes(node).qemu(vmid) if guest_type == "vm" else px.nodes(node).lxc(vmid)
|
||||
@@ -289,23 +274,44 @@ def _wait_guest_stopped(px: ProxmoxAPI, guest_type: str, vmid: int, node: str, t
|
||||
|
||||
|
||||
def delete_guest(guest_type: str, vmid: int, node: str = None) -> None:
|
||||
"""Останавливает работающий инстанс и удаляет его."""
|
||||
"""Удаляет VM/LXC.
|
||||
|
||||
Если ресурс уже удалён вручную в Proxmox, операция считается успешной.
|
||||
Это позволяет backend удалить устаревшую запись из собственной БД.
|
||||
"""
|
||||
node = node or settings.pve_node
|
||||
px = _client()
|
||||
endpoint = px.nodes(node).qemu(vmid) if guest_type == "vm" else px.nodes(node).lxc(vmid)
|
||||
status = endpoint.status.current.get().get("status")
|
||||
|
||||
try:
|
||||
status = endpoint.status.current.get().get("status")
|
||||
except ResourceException as exc:
|
||||
if _is_missing_guest_error(exc):
|
||||
logger.info("%s %s уже отсутствует в Proxmox — считаем удалённым", guest_type, vmid)
|
||||
return
|
||||
raise
|
||||
|
||||
if status == "running":
|
||||
try:
|
||||
shutdown_upid = endpoint.status.shutdown.post(timeout=60)
|
||||
if shutdown_upid:
|
||||
_wait_task(px, node, shutdown_upid, timeout=120)
|
||||
except ResourceException as exc:
|
||||
if not _is_missing_guest_error(exc):
|
||||
logger.warning("Мягкая остановка %s %s не удалась: %s", guest_type, vmid, exc)
|
||||
endpoint.status.stop.post()
|
||||
except Exception as exc:
|
||||
logger.warning("Мягкая остановка %s %s не удалась: %s", guest_type, vmid, exc)
|
||||
endpoint.status.stop.post()
|
||||
_wait_guest_stopped(px, guest_type, vmid, node)
|
||||
|
||||
upid = endpoint.delete()
|
||||
try:
|
||||
upid = endpoint.delete()
|
||||
except ResourceException as exc:
|
||||
if _is_missing_guest_error(exc):
|
||||
logger.info("%s %s исчез во время удаления — считаем удалённым", guest_type, vmid)
|
||||
return
|
||||
raise
|
||||
if upid:
|
||||
_wait_task(px, node, upid)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user