"""Снимки (точки восстановления) машины: парсеры и маршруты /vm/{name}/snapshots.""" from datetime import datetime from pathlib import Path from urllib.parse import unquote import pytest import conftest import core from features import snapshots FIXTURES = Path(__file__).resolve().parent / "fixtures" # ---------------------------------------------------------------- parsers def test_parse_snapshot_list_fixture(): rows = snapshots.parse_snapshot_list((FIXTURES / "snapshot-list.txt").read_text()) assert [row["name"] for row in rows] == ["before-upd", "clean"] # новые сверху assert rows[0] == {"name": "before-upd", "created_at": "2026-09-25 10:12:03", "tz": "+0000", "state": "shutoff"} assert rows[1]["state"] == "running" def test_parse_snapshot_list_without_timezone_and_garbage(): text = " Name Creation Time State\n------\n snap1 2026-01-02 03:04:05 running\nerror: something\n\n" rows = snapshots.parse_snapshot_list(text) assert rows == [{"name": "snap1", "created_at": "2026-01-02 03:04:05", "tz": "", "state": "running"}] assert snapshots.parse_snapshot_list("") == [] def test_parse_snapshot_xml(): meta = snapshots.parse_snapshot_xml(conftest.fake_snapshot_xml("clean")) assert meta["description"] == "Чистая система после установки" assert meta["state"] == "running" and meta["with_memory"] is True assert snapshots.parse_snapshot_xml(conftest.fake_snapshot_xml("before-upd"))["with_memory"] is False assert snapshots.parse_snapshot_xml("x hvm/usr/share/OVMF/OVMF_CODE.fd/var/lib/libvirt/qemu/nvram/x.fd """ info = snapshots.inspect_domain_xml(xml) assert info["unsupported"] == [{"target": "vda", "format": "raw"}] assert info["uefi"] is True assert snapshots.inspect_domain_xml("") return original(cmd, timeout, **kwargs) monkeypatch.setattr(snapshots, "run_cmd", run) response = logged_in.post("/vm/web01/snapshots/create", data={"description": "x"}, follow_redirects=False) assert "qcow2" in unquote(response.headers["location"]) assert not find(recorder, "virsh", "snapshot-create-as") def test_create_snapshot_surfaces_libvirt_error(logged_in, data_dirs, recorder, monkeypatch): original = snapshots.run_cmd def run(cmd, timeout=12, **kwargs): if cmd[:2] == ["virsh", "snapshot-create-as"]: return conftest.fake_result(ok=False, stderr="error: unsupported configuration: internal snapshots of a VM with pflash based firmware are not supported") return original(cmd, timeout, **kwargs) monkeypatch.setattr(snapshots, "run_cmd", run) response = logged_in.post("/vm/web01/snapshots/create", data={}, follow_redirects=False) location = response.headers["location"] operation = core.read_operation(location.split("op=", 1)[1]) assert operation["status"] == "error" assert operation["message"].startswith("Для машин с UEFI-прошивкой") and "pflash" in operation["message"] assert "pflash" in operation["log_tail"] html = logged_in.get(location).text assert "Для машин с UEFI-прошивкой" in html def test_create_snapshot_waits_for_active_operation(logged_in, data_dirs, recorder): core.new_operation("snapshot_create", "Снимок машины web01", vm_name="web01", snapshot="snap-x") response = logged_in.post("/vm/web01/snapshots/create", data={}, follow_redirects=False) assert "предыдущей операции" in unquote(response.headers["location"]) assert not find(recorder, "virsh", "snapshot-create-as") html = logged_in.get("/vm/web01/snapshots").text assert 'data-watch-operation=' in html and "/api/operations/" in html def test_revert_snapshot(logged_in, data_dirs, recorder): response = logged_in.post("/vm/web01/snapshots/clean/revert", follow_redirects=False) assert response.status_code == 303 assert find(recorder, "virsh", "snapshot-revert") == [["virsh", "snapshot-revert", "--domain", "web01", "--snapshotname", "clean"]] operation = core.read_operation(response.headers["location"].split("op=", 1)[1]) assert operation["status"] == "success" and "возвращена" in operation["message"] and "работает" in operation["message"] def test_revert_retries_with_force(logged_in, data_dirs, recorder, monkeypatch): original = snapshots.run_cmd def run(cmd, timeout=12, **kwargs): if cmd[:2] == ["virsh", "snapshot-revert"] and "--force" not in cmd: recorder.append(list(cmd)) return conftest.fake_result(ok=False, stderr="error: revert requires force: snapshot was created with a different configuration") return original(cmd, timeout, **kwargs) monkeypatch.setattr(snapshots, "run_cmd", run) response = logged_in.post("/vm/web01/snapshots/clean/revert", follow_redirects=False) reverts = find(recorder, "virsh", "snapshot-revert") assert len(reverts) == 2 and reverts[1][-1] == "--force" assert core.read_operation(response.headers["location"].split("op=", 1)[1])["status"] == "success" def test_revert_reports_error(logged_in, data_dirs, recorder, monkeypatch): original = snapshots.run_cmd def run(cmd, timeout=12, **kwargs): if cmd[:2] == ["virsh", "snapshot-revert"]: return conftest.fake_result(ok=False, stderr="error: Domain snapshot not found: no domain snapshot with matching name 'clean'") return original(cmd, timeout, **kwargs) monkeypatch.setattr(snapshots, "run_cmd", run) response = logged_in.post("/vm/web01/snapshots/clean/revert", follow_redirects=False) operation = core.read_operation(response.headers["location"].split("op=", 1)[1]) assert operation["status"] == "error" and operation["message"].startswith("Такого снимка уже нет") def test_delete_snapshot(logged_in, data_dirs, recorder): response = logged_in.post("/vm/web01/snapshots/before-upd/delete", follow_redirects=False) assert response.status_code == 303 assert "удалён" in unquote(response.headers["location"]) assert find(recorder, "virsh", "snapshot-delete") == [["virsh", "snapshot-delete", "--domain", "web01", "--snapshotname", "before-upd"]] def test_delete_snapshot_error(logged_in, data_dirs, recorder, monkeypatch): original = snapshots.run_cmd def run(cmd, timeout=12, **kwargs): if cmd[:2] == ["virsh", "snapshot-delete"]: return conftest.fake_result(ok=False, stderr="error: Timed out during operation: cannot acquire state change lock") return original(cmd, timeout, **kwargs) monkeypatch.setattr(snapshots, "run_cmd", run) response = logged_in.post("/vm/web01/snapshots/before-upd/delete", follow_redirects=False) assert "snapshot_error=" in response.headers["location"] and "занята" in unquote(response.headers["location"]) def test_snapshot_routes_validate_names(logged_in, data_dirs, recorder): bad = logged_in.post("/vm/web01/snapshots/-bad/revert", follow_redirects=False) assert bad.status_code == 303 and "snapshot_error=" in bad.headers["location"] assert logged_in.post("/vm/web01/snapshots/..%2Fx/delete", follow_redirects=False).status_code in (400, 404) assert logged_in.post("/vm/-bad/snapshots/create", data={}, follow_redirects=False).status_code == 400 assert not find(recorder, "virsh", "snapshot-delete") and not find(recorder, "virsh", "snapshot-revert") def test_snapshot_routes_require_login(client): for path in ("/vm/web01/snapshots/create", "/vm/web01/snapshots/clean/revert", "/vm/web01/snapshots/clean/delete"): response = client.post(path, follow_redirects=False) assert response.status_code == 303 and response.headers["location"] == "/login"