Неудачное создание машины: панель больше не предлагает экран несуществующей машины, даёт «Создать заново» с тем же образом и удаляет недосозданный диск

Этот коммит содержится в:
2026-09-27 17:24:01 +09:00
родитель 48ac535c3a
Коммит 984eb85a5a
4 изменённых файлов: 74 добавлений и 5 удалений
+31 -3
Просмотреть файл
@@ -294,6 +294,17 @@ def progress_from_line(current: int, line: str) -> int:
return current
def remove_unfinished_disk(operation_id: str, disk_path: str) -> None:
"""Машина не появилась, а её диск (из готового образа он создаётся до virt-install) остался:
удаляем его, иначе повторное создание под тем же названием упрётся в «Диск уже существует»."""
disk = Path(disk_path)
if not disk_path or disk.parent.resolve() != IMAGES_DIR.resolve() or not disk.is_file():
return
disk.unlink(missing_ok=True)
run_cmd(["virsh", "pool-refresh", "virtuality-images"], timeout=20)
append_operation_log(operation_id, f"Недосозданный диск {disk.name} удалён.")
def run_operation_worker(operation_id: str, cmd: list[str]) -> None:
operation = read_operation(operation_id)
if not operation:
@@ -322,7 +333,13 @@ def run_operation_worker(operation_id: str, cmd: list[str]) -> None:
append_operation_log(operation_id, f"virt-install завершился с ошибкой. Exit code: {exit_code}")
if fresh.get("vm_name") and fresh.get("network_mode") == "nat":
network_core.release_nat_address(fresh["vm_name"])
update_operation(fresh, status="error", progress=100, exit_code=exit_code, message=f"virt-install завершился с ошибкой: {exit_code}", finished_at=utc_now())
vm_left = bool(fresh.get("vm_name")) and vm_exists(fresh["vm_name"])
if not vm_left:
remove_unfinished_disk(operation_id, fresh.get("disk_path") or "")
message = "Машину создать не удалось, причина в журнале ниже."
if not vm_left:
message += " На сервере от неё ничего не осталось, её можно создать заново под тем же названием."
update_operation(fresh, status="error", progress=100, exit_code=exit_code, message=message, finished_at=utc_now())
except Exception as exc:
fresh = read_operation(operation_id) or operation
append_operation_log(operation_id, f"Ошибка запуска операции: {exc}")
@@ -1737,7 +1754,18 @@ def operation_detail_page(request: Request, operation_id: str):
operation = read_operation(operation_id)
if not operation:
return RedirectResponse(url="/operations", status_code=303)
return templates.TemplateResponse(request, "operation_detail.html", {"app_name": APP_NAME, "user": AUTH_USER, "operation": operation, "operation_css": operation_css})
vm_name = str(operation.get("vm_name") or "")
vm_present = valid_vm_name(vm_name) and vm_exists(vm_name)
return templates.TemplateResponse(request, "operation_detail.html", {"app_name": APP_NAME, "user": AUTH_USER, "operation": operation, "operation_css": operation_css, "vm_present": vm_present, "retry_url": vm_create_retry_url(operation)})
def vm_create_retry_url(operation: dict[str, Any]) -> str:
"""Форма создания машины с тем же установочным образом или готовым образом, что у неудачной попытки."""
if operation.get("iso_path"):
return "/vm/create?iso=" + quote(Path(operation["iso_path"]).name)
if operation.get("disk_image_path"):
return "/vm/create?image=" + quote(Path(operation["disk_image_path"]).name)
return "/vm/create"
@app.get("/api/operations")
@@ -1924,7 +1952,7 @@ def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form
success_message = "Машина создана" + (f". {login_hint}" if login_hint else "") + (f". Адрес в сети машин: {nat_ip}" if nat_ip else "")
operation_id = str(uuid.uuid4())
operation = {"id": operation_id, "type": "vm_create", "title": f"Создание VM {name}", "status": "queued", "progress": 0, "message": "Операция поставлена в очередь", "created_at": utc_now(), "updated_at": utc_now(), "created_by": AUTH_USER, "vm_name": name, "disk_path": str(disk_path), "iso_path": iso_path, "host_profile": profile.get("profile"), "guest_arch": selected_arch, "boot_order": selected_boot_order, "network_mode": network_mode, "network": network_arg, "bridge": bridge, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "os_type": os_type, "cloud_init_user": ci_user if use_cloud_init else "", "nat_ip": nat_ip or "", "seed_file": seed_file, "success_message": success_message, "cmd": " ".join(cmd)}
operation = {"id": operation_id, "type": "vm_create", "title": f"Создание VM {name}", "status": "queued", "progress": 0, "message": "Операция поставлена в очередь", "created_at": utc_now(), "updated_at": utc_now(), "created_by": AUTH_USER, "vm_name": name, "disk_path": str(disk_path), "iso_path": iso_path, "disk_image_path": disk_image_path if source_type == "disk_image" else "", "host_profile": profile.get("profile"), "guest_arch": selected_arch, "boot_order": selected_boot_order, "network_mode": network_mode, "network": network_arg, "bridge": bridge, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "os_type": os_type, "cloud_init_user": ci_user if use_cloud_init else "", "nat_ip": nat_ip or "", "seed_file": seed_file, "success_message": success_message, "cmd": " ".join(cmd)}
start_background_operation(operation, cmd)
return RedirectResponse(url=f"/operations/{operation_id}", status_code=303)
+1 -1
Просмотреть файл
@@ -383,7 +383,7 @@
messageEl.textContent = op.message || '';
if (logEl) { const atBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 40; logEl.textContent = op.log_tail || ''; if (atBottom) logEl.scrollTop = logEl.scrollHeight; }
render();
if (done()) { clearInterval(timer); clearInterval(tick); if (!wasDone) { toast(status === 'success' ? 'Операция завершена' : 'Операция завершилась с ошибкой', status === 'success' ? 'success' : 'danger'); $$('[data-op-done]').forEach((el) => { el.hidden = false; }); } }
if (done()) { clearInterval(timer); clearInterval(tick); if (!wasDone) { toast(status === 'success' ? 'Операция завершена' : 'Операция завершилась с ошибкой', status === 'success' ? 'success' : 'danger'); $$(`[data-op-done="${status}"]`).forEach((el) => { el.hidden = false; }); if (status === 'error' && $('[data-op-done="error"][data-op-reload]')) setTimeout(() => location.reload(), 1500); } }
} catch (_) {}
};
const timer = setInterval(poll, 1500);
+8 -1
Просмотреть файл
@@ -25,10 +25,17 @@
<div class="progress lg {{ 'success' if operation.status == 'success' else 'danger' if operation.status == 'error' else '' }}" data-op="progress" style="height:10px"><span data-op="bar" style="width:{{ operation.progress|default(0) }}%"></span></div>
<p class="muted" data-op="message">{{ operation.message }}</p>
{% if operation.vm_name %}
<div class="row" data-op-done {% if operation.status != 'success' %}hidden{% endif %}>
<div class="row" data-op-done="success" {% if operation.status != 'success' %}hidden{% endif %}>
<a class="btn btn-primary" href="/vm/{{ operation.vm_name }}/console" target="_blank" rel="noopener">{{ icon('monitor') }}Открыть экран машины</a>
<a class="btn" href="/vm/{{ operation.vm_name }}">Перейти к машине</a>
</div>
<div class="row" data-op-done="error" {% if operation.type == 'vm_create' %}data-op-reload{% endif %} {% if operation.status != 'error' %}hidden{% endif %}>
{% if operation.type == 'vm_create' and not vm_present %}
<a class="btn btn-primary" href="{{ retry_url }}">{{ icon('rotate-cw') }}Создать заново</a>
{% elif vm_present %}
<a class="btn" href="/vm/{{ operation.vm_name }}">Перейти к машине</a>
{% endif %}
</div>
{% endif %}
</div>
</article>