Удалить машину можно в любом состоянии: работающая выключается, пока идёт задача удаление ждёт, пробросы портов машины удаляются вместе с ней
Этот коммит содержится в:
@@ -1,7 +1,7 @@
|
||||
"""Regressions for the defects found by the 0.11 audit."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
import app
|
||||
import conftest
|
||||
@@ -69,6 +69,46 @@ def test_vm_delete_failure_is_reported(logged_in, data_dirs, monkeypatch):
|
||||
assert response.headers["location"].startswith("/vm/web01?error=")
|
||||
|
||||
|
||||
def test_vm_delete_stops_paused_machine_first(logged_in, data_dirs, monkeypatch):
|
||||
import core
|
||||
|
||||
calls = recording(monkeypatch, app)
|
||||
recording(monkeypatch, core, [(["virsh", "domstate"], conftest.fake_result("paused"))])
|
||||
logged_in.post("/vm/web01/delete", follow_redirects=False)
|
||||
assert find(calls, "virsh", "destroy", "web01") and find(calls, "virsh", "undefine", "web01")
|
||||
|
||||
|
||||
def test_vm_delete_waits_for_running_task(logged_in, data_dirs, monkeypatch):
|
||||
import core
|
||||
|
||||
calls = recording(monkeypatch, app)
|
||||
backup = core.new_operation("backup", "Резервная копия web01", vm_name="web01")
|
||||
core.update_operation(backup, status="running")
|
||||
response = logged_in.post("/vm/web01/delete", follow_redirects=False)
|
||||
assert "Резервная копия web01" in unquote(response.headers["location"])
|
||||
assert not find(calls, "virsh", "undefine") and not find(calls, "virsh", "destroy")
|
||||
|
||||
|
||||
def test_vm_delete_removes_its_port_forwards(logged_in, data_dirs, monkeypatch):
|
||||
recording(monkeypatch, app)
|
||||
rule = {"guest_ip": "192.168.100.51", "external_port_start": 2222, "external_port_end": 2222, "guest_port_start": 22, "guest_port_end": 22, "protocol": "tcp"}
|
||||
data_dirs["network"].joinpath("port_forwards.json").write_text(json.dumps([
|
||||
{"id": "1", "vm_name": "web01", **rule},
|
||||
{"id": "2", "vm_name": "db01", **rule, "external_port_start": 2223, "external_port_end": 2223, "guest_ip": "192.168.100.52"},
|
||||
]))
|
||||
response = logged_in.post("/vm/web01/delete", follow_redirects=False)
|
||||
assert "убраны пробросы портов: 1" in unquote(response.headers["location"])
|
||||
assert [item["vm_name"] for item in network_core.load_port_forwards()] == ["db01"]
|
||||
assert "2222" not in (data_dirs["nft"] / "virtuality.nft").read_text()
|
||||
|
||||
|
||||
def test_delete_is_offered_for_running_machine(logged_in):
|
||||
html = logged_in.get("/vm/web01").text # в тестах web01 работает
|
||||
assert 'action="/vm/web01/delete"' in html and "она будет сразу выключена" in html
|
||||
dashboard = logged_in.get("/").text
|
||||
assert 'action="/vm/web01/delete"' in dashboard and 'action="/vm/db01/delete"' in dashboard
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- ISO in the CD-ROM drive (app.py:1150, 1158)
|
||||
def test_iso_mount_uses_change_media_for_existing_drive(logged_in, data_dirs, monkeypatch):
|
||||
iso = data_dirs["iso"] / "debian.iso"
|
||||
|
||||
+9
-2
@@ -2094,14 +2094,21 @@ def vm_action(request: Request, name: str, action: str):
|
||||
if action in ("shutdown", "destroy", "delete"):
|
||||
backups.cancel_late_restart(name)
|
||||
if action == "delete":
|
||||
if vm_state(name) == "running":
|
||||
import core
|
||||
|
||||
busy = core.active_operations_for(name)
|
||||
if busy:
|
||||
return redirect_with_message(f"/vm/{name}", "error", f"С машиной сейчас идёт задача «{busy[0].get('title') or 'операция'}». Дождитесь её завершения и удалите машину снова.")
|
||||
if core.vm_is_running(name): # и на паузе тоже: undefine активной машины оставил бы её работать без настроек
|
||||
run_cmd(["virsh", "destroy", name], timeout=20)
|
||||
# --nvram: UEFI machines keep firmware variables that block a plain undefine.
|
||||
result = run_cmd(["virsh", "undefine", name, "--remove-all-storage", "--nvram", "--managed-save", "--snapshots-metadata", "--checkpoints-metadata"], timeout=120)
|
||||
if not result["ok"]:
|
||||
return redirect_with_message(f"/vm/{name}", "error", f"Не удалось удалить машину: {virsh_error(result)}")
|
||||
network_core.release_nat_address(name)
|
||||
return redirect_with_message("/", "message", f"Машина {name} удалена")
|
||||
removed = network_core.remove_vm_forwards(name)
|
||||
message = f"Машина {name} удалена" + (f", вместе с ней убраны пробросы портов: {removed}" if removed else "")
|
||||
return redirect_with_message("/", "message", message)
|
||||
if action not in allowed:
|
||||
return JSONResponse({"ok": False, "error": "Unsupported action"}, status_code=400)
|
||||
result = run_cmd(allowed[action], timeout=30)
|
||||
|
||||
@@ -436,6 +436,17 @@ def add_port_forward(vm_name: str, guest_ip: str, external_port: Any, guest_port
|
||||
return normalize_forward(forward)
|
||||
|
||||
|
||||
def remove_vm_forwards(vm_name: str) -> int:
|
||||
"""Машина удалена: её пробросы вели бы на адрес, который потом может достаться другой машине."""
|
||||
items = load_port_forwards()
|
||||
kept = [item for item in items if item.get('vm_name') != vm_name]
|
||||
if len(kept) == len(items):
|
||||
return 0
|
||||
save_port_forwards(kept)
|
||||
apply_port_forwards()
|
||||
return len(items) - len(kept)
|
||||
|
||||
|
||||
def delete_port_forward(forward_id: str) -> None:
|
||||
items = [item for item in load_port_forwards() if item.get('id') != forward_id]
|
||||
save_port_forwards(items)
|
||||
|
||||
@@ -101,9 +101,9 @@
|
||||
<form method="post" action="/vm/{{ vm.name }}/destroy" data-confirm="Машина будет выключена сразу, как при отключении питания. Несохранённые данные внутри неё пропадут." data-confirm-title="Выключить {{ vm.name }} принудительно?" data-confirm-button="Выключить" data-toast="Машина {{ vm.name }} выключена" data-toast-tone="warning"><button class="menu-item danger">{{ icon('zap') }}Выключить принудительно</button></form>
|
||||
{% elif vm.state != 'paused' %}
|
||||
<form method="post" action="/vm/{{ vm.name }}/start" data-toast="Машина {{ vm.name }} запускается"><button class="menu-item">{{ icon('play') }}Запустить</button></form>
|
||||
<div class="menu-sep"></div>
|
||||
<form method="post" action="/vm/{{ vm.name }}/delete" data-confirm="Машина и все её диски будут удалены без возможности восстановления." data-confirm-title="Удалить {{ vm.name }}?" data-confirm-button="Удалить навсегда" data-toast="Машина {{ vm.name }} удалена" data-toast-tone="warning"><button class="menu-item danger">{{ icon('trash-2') }}Удалить машину</button></form>
|
||||
{% endif %}
|
||||
<div class="menu-sep"></div>
|
||||
<form method="post" action="/vm/{{ vm.name }}/delete" data-confirm="{% if vm.state != 'shut off' %}Машина {{ vm.name }} работает: она будет сразу выключена, как при отключении питания, а затем удалена вместе со всеми дисками.{% else %}Машина {{ vm.name }} будет удалена вместе со всеми дисками.{% endif %} Отменить это нельзя. Резервные копии машины останутся, из них её можно восстановить." data-confirm-title="Удалить {{ vm.name }}?" data-confirm-button="Удалить навсегда" data-toast="Машина {{ vm.name }} удалена" data-toast-tone="warning"><button class="menu-item danger">{{ icon('trash-2') }}Удалить машину</button></form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -33,9 +33,8 @@
|
||||
{% if s.running %}
|
||||
<form method="post" action="/vm/{{ vm.name }}/reboot" data-toast="Машина перезагружается"><button class="menu-item">{{ icon('rotate-cw') }}<span>Перезагрузить<small>Мягкая перезагрузка системы</small></span></button></form>
|
||||
<form method="post" action="/vm/{{ vm.name }}/destroy" data-confirm="Машина будет выключена сразу, как при отключении питания. Несохранённые данные внутри неё пропадут." data-confirm-title="Выключить принудительно?" data-confirm-button="Выключить" data-toast="Машина выключена" data-toast-tone="warning"><button class="menu-item danger">{{ icon('zap') }}<span>Выключить принудительно<small>Если система зависла</small></span></button></form>
|
||||
{% else %}
|
||||
<form method="post" action="/vm/{{ vm.name }}/delete" data-confirm="Машина {{ vm.name }} и все её диски будут удалены без возможности восстановления." data-confirm-title="Удалить машину?" data-confirm-button="Удалить навсегда" data-toast="Машина {{ vm.name }} удалена" data-toast-tone="warning"><button class="menu-item danger">{{ icon('trash-2') }}<span>Удалить машину<small>Вместе с дисками</small></span></button></form>
|
||||
{% endif %}
|
||||
<form method="post" action="/vm/{{ vm.name }}/delete" data-confirm="{% if vm.state != 'shut off' %}Машина {{ vm.name }} работает: она будет сразу выключена, как при отключении питания, а затем удалена вместе со всеми дисками.{% else %}Машина {{ vm.name }} будет удалена вместе со всеми дисками.{% endif %} Отменить это нельзя. Резервные копии машины останутся, из них её можно восстановить." data-confirm-title="Удалить машину {{ vm.name }}?" data-confirm-button="Удалить навсегда" data-toast="Машина {{ vm.name }} удалена" data-toast-tone="warning"><button class="menu-item danger">{{ icon('trash-2') }}<span>Удалить машину<small>Вместе с дисками</small></span></button></form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
Ссылка в новой задаче
Block a user