feat(lxc): support reinstall for LXC created from CT template file

Previously the reinstall endpoint required cached.template_id (clone-from-VM-
template flow), so LXC instances created via the wizard from an OS template
file (e.g. local:vztmpl/debian-13-...tar.zst) returned 'VM has no associated
template; reinstall is not available'.

Now reinstall works in two modes:
  1) clone-from-template (existing): VM/LXC with cached.template_id pointing
     to OSTemplate.vmid
  2) ostemplate-file (new, LXC only): cached.template_name like
     'storage:vztmpl/...tar.zst'. Reinstall stops, deletes, then re-creates
     the container with the same VMID using create_lxc_container, preserving
     network config, storage, rootfs size, swap, features, ssh_keys, etc.
     pulled from the previous container config + VMInstance cache.
This commit is contained in:
markmorado
2026-05-06 20:11:05 +02:00
parent daeddbc6c7
commit aba2eb3583
2 changed files with 189 additions and 50 deletions

View File

@@ -50,30 +50,47 @@ def _do_reinstall_sync(task_id: int, server_id: int, vmid: int, node: str,
VMInstance.vmid == vmid,
VMInstance.deleted_at.is_(None),
).first()
if not cached or not cached.template_id:
_update_deploy_task(task_id, 'failed', 'Шаблон не привязан', 0,
error='У VM не сохранён шаблон, переустановка недоступна')
if not cached:
_update_deploy_task(task_id, 'failed', 'VM не найдена', 0,
error='Запись об инстансе не найдена')
return
tpl = db.query(OSTemplate).filter(OSTemplate.id == cached.template_id).first()
if not tpl or not tpl.vmid:
_update_deploy_task(task_id, 'failed', 'Шаблон не найден', 0, error='Шаблон не найден или некорректен')
return
# Resolve template VMID & source node (cross-node aware)
local_template_vmid = tpl.get_vmid_for_node(node)
if local_template_vmid:
template_source_node = node
template_vmid = local_template_vmid
is_lxc = (cached.vm_type == 'lxc')
# Two reinstall modes:
# 1) clone-from-template (cached.template_id → OSTemplate.vmid). Used by VM
# templates and LXC clones from a template VMID.
# 2) ostemplate-file (LXC only, cached.template_name like "local:vztmpl/...tar.zst")
tpl = None
ostemplate_file = None
if cached.template_id:
tpl = db.query(OSTemplate).filter(OSTemplate.id == cached.template_id).first()
if not tpl or not tpl.vmid:
_update_deploy_task(task_id, 'failed', 'Шаблон не найден', 0,
error='Шаблон не найден или некорректен')
return
elif is_lxc and cached.template_name and ':' in cached.template_name:
ostemplate_file = cached.template_name
else:
template_source_node = tpl.get_source_node() or tpl.node or node
template_vmid = tpl.vmid
_update_deploy_task(task_id, 'failed', 'Шаблон не привязан', 0,
error='У инстанса не сохранён шаблон, переустановка недоступна')
return
# Resolve template VMID & source node (cross-node aware) — only for clone mode
template_source_node = node
template_vmid = None
if tpl is not None:
local_template_vmid = tpl.get_vmid_for_node(node)
if local_template_vmid:
template_source_node = node
template_vmid = local_template_vmid
else:
template_source_node = tpl.get_source_node() or tpl.node or node
template_vmid = tpl.vmid
name = cached.name
cores = cached.cores
memory = cached.memory
description = cached.description
is_lxc = (cached.vm_type == 'lxc')
memory_mb = int(memory) // (1024 * 1024) if memory else None
_update_deploy_task(task_id, 'running', 'Подключение к Proxmox...', 10, vmid=vmid, node=node)
@@ -83,6 +100,17 @@ def _do_reinstall_sync(task_id: int, server_id: int, vmid: int, node: str,
error='Не удалось подключиться к Proxmox серверу')
return
# Snapshot current container/VM config — needed for LXC ostemplate-recreate
# (network, swap, storage, features, etc. aren't all stored in VMInstance)
prev_cfg = {}
try:
if is_lxc:
prev_cfg = client.get_container_config(node, vmid) or {}
else:
prev_cfg = client.get_vm_config(node, vmid) or {}
except Exception as _ce:
logger.warning(f"[REINSTALL #{task_id}] could not fetch current config: {_ce}")
# 1) Stop
_update_deploy_task(task_id, 'running', 'Остановка инстанса...', 20, vmid=vmid, node=node)
try:
@@ -117,34 +145,133 @@ def _do_reinstall_sync(task_id: int, server_id: int, vmid: int, node: str,
break
time.sleep(1)
# 3) Clone from template
_update_deploy_task(task_id, 'running', f'Клонирование из шаблона {tpl.name}...', 50, vmid=vmid, node=node)
try:
if is_lxc:
upid = client.clone_container(
node=template_source_node, vmid=template_vmid, new_vmid=vmid,
hostname=name, full=True, target_node=node,
description=description,
)
else:
upid = client.clone_vm(
node=template_source_node, vmid=template_vmid, new_vmid=vmid,
name=name, full=True, target_node=node,
description=description,
)
except Exception as e:
_update_deploy_task(task_id, 'failed', 'Ошибка клонирования', 50, error=f'Clone failed: {e}')
return
if not upid:
_update_deploy_task(task_id, 'failed', 'Ошибка клонирования', 50,
error='Proxmox не вернул UPID задачи клонирования')
return
# 3) Re-create from template
if ostemplate_file:
# ---- LXC: re-create from CT template file with same VMID ----
_update_deploy_task(task_id, 'running',
f'Создание контейнера из шаблона {ostemplate_file.split("/")[-1]}...',
50, vmid=vmid, node=node)
_update_deploy_task(task_id, 'running', 'Ожидание завершения клонирования...', 75, vmid=vmid, node=node)
try:
client.wait_for_task(template_source_node, upid, timeout=600)
except Exception:
pass
# Extract storage/rootfs size from previous rootfs entry (e.g. "local-lvm:vm-118-disk-0,size=8G")
storage = 'local-lvm'
rootfs_size = 8
try:
rootfs_val = prev_cfg.get('rootfs', '')
if rootfs_val:
storage = rootfs_val.split(':', 1)[0]
if 'size=' in rootfs_val:
sz = rootfs_val.split('size=')[1].split(',')[0].strip()
if sz.endswith('G'):
rootfs_size = int(float(sz[:-1]))
elif sz.endswith('M'):
rootfs_size = max(1, int(sz[:-1]) // 1024)
elif sz.endswith('T'):
rootfs_size = int(float(sz[:-1])) * 1024
except Exception:
pass
# net0: prefer existing config (preserves bridge/ip/gateway/vlan/firewall)
net0 = prev_cfg.get('net0')
if not net0:
ip_part = 'ip=dhcp'
if cached.ip_address:
prefix = cached.ip_prefix or 24
ip_part = f'ip={cached.ip_address}/{prefix}'
if cached.gateway:
ip_part += f',gw={cached.gateway}'
net0 = f'name=eth0,bridge=vmbr0,{ip_part},firewall=1'
# Other params with sane fallbacks
swap_mb = 512
try:
if 'swap' in prev_cfg:
swap_mb = int(prev_cfg['swap'])
except Exception:
pass
unprivileged = True
try:
if 'unprivileged' in prev_cfg:
unprivileged = bool(int(prev_cfg['unprivileged']))
except Exception:
pass
onboot = False
try:
if 'onboot' in prev_cfg:
onboot = bool(int(prev_cfg['onboot']))
except Exception:
pass
features = prev_cfg.get('features') # None → create_lxc_container will default to nesting=1
nameserver = prev_cfg.get('nameserver') or cached.nameserver
searchdomain = prev_cfg.get('searchdomain')
try:
upid = client.create_lxc_container(
node=node,
vmid=vmid,
ostemplate=ostemplate_file,
hostname=name,
cores=cores or 1,
memory=memory_mb or 512,
swap=swap_mb,
storage=storage,
rootfs_size=rootfs_size,
net0=net0,
nameserver=nameserver,
searchdomain=searchdomain,
ssh_public_keys=cached.ssh_keys,
unprivileged=unprivileged,
start_after_create=False,
onboot=onboot,
description=description or f'Reinstalled from {ostemplate_file}',
features=features,
)
except Exception as e:
_update_deploy_task(task_id, 'failed', 'Ошибка создания контейнера', 50,
error=f'Create failed: {e}')
return
if not upid:
_update_deploy_task(task_id, 'failed', 'Ошибка создания контейнера', 50,
error='Proxmox не вернул UPID задачи создания контейнера')
return
_update_deploy_task(task_id, 'running', 'Ожидание завершения создания...', 75,
vmid=vmid, node=node)
try:
client.wait_for_task(node, upid, timeout=600)
except Exception:
pass
else:
# ---- Clone-from-template (VM or LXC with template_id) ----
_update_deploy_task(task_id, 'running', f'Клонирование из шаблона {tpl.name}...', 50, vmid=vmid, node=node)
try:
if is_lxc:
upid = client.clone_container(
node=template_source_node, vmid=template_vmid, new_vmid=vmid,
hostname=name, full=True, target_node=node,
description=description,
)
else:
upid = client.clone_vm(
node=template_source_node, vmid=template_vmid, new_vmid=vmid,
name=name, full=True, target_node=node,
description=description,
)
except Exception as e:
_update_deploy_task(task_id, 'failed', 'Ошибка клонирования', 50, error=f'Clone failed: {e}')
return
if not upid:
_update_deploy_task(task_id, 'failed', 'Ошибка клонирования', 50,
error='Proxmox не вернул UPID задачи клонирования')
return
_update_deploy_task(task_id, 'running', 'Ожидание завершения клонирования...', 75, vmid=vmid, node=node)
try:
client.wait_for_task(template_source_node, upid, timeout=600)
except Exception:
pass
# 4) Re-apply config
_update_deploy_task(task_id, 'running', 'Применение конфигурации...', 90, vmid=vmid, node=node)
@@ -228,7 +355,11 @@ def _do_reinstall_sync(task_id: int, server_id: int, vmid: int, node: str,
resource_type='lxc' if is_lxc else 'vm', resource_id=vmid,
username=username, resource_name=name,
server_id=server_id, server_name=server.name, node_name=node,
details={'template_id': tpl.id, 'template_vmid': template_vmid}, success=True,
details={
'template_id': tpl.id if tpl else None,
'template_vmid': template_vmid,
'ostemplate': ostemplate_file,
}, success=True,
)
_update_deploy_task(task_id, 'completed', 'Переустановка завершена', 100, vmid=vmid, node=node)

View File

@@ -409,16 +409,24 @@ def reinstall_vm_endpoint(
VMInstance.vmid == vmid,
VMInstance.deleted_at.is_(None),
).first()
if not cached or not cached.template_id:
raise HTTPException(status_code=400, detail="VM has no associated template; reinstall is not available")
if not cached:
raise HTTPException(status_code=404, detail="VM not found")
tpl = db.query(OSTemplate).filter(OSTemplate.id == cached.template_id).first()
if not tpl or not tpl.vmid:
raise HTTPException(status_code=400, detail="Template not found or invalid")
is_lxc = (cached.vm_type == 'lxc')
tpl = None
if cached.template_id:
tpl = db.query(OSTemplate).filter(OSTemplate.id == cached.template_id).first()
if not tpl or not tpl.vmid:
raise HTTPException(status_code=400, detail="Template not found or invalid")
elif is_lxc and cached.template_name and ':' in cached.template_name:
# LXC created from CT template file (e.g. local:vztmpl/debian-13-...tar.zst)
tpl = None
else:
raise HTTPException(status_code=400, detail="VM has no associated template; reinstall is not available")
task = DeployTask(
kind='reinstall', name=cached.name, status='pending', step='В очереди...', progress=0,
template_id=tpl.id, server_id=server_id, user_id=current_user.id,
template_id=tpl.id if tpl else None, server_id=server_id, user_id=current_user.id,
vmid=vmid, node=node,
)
db.add(task); db.commit(); db.refresh(task)