Files
virtuality/tests/conftest.py
T
Claude 34684634af Повседневные операции с машиной были добавлены: диск, клон, пауза, заметки, нагрузка
Модуль web/features/vmops.py (router подключён двумя строками в app.py):

- Увеличение диска на вкладке «Диски и загрузка»: размер из qemu-img info,
  только рост, проверка свободного места; выключенная машина — qemu-img resize,
  работающая — virsh blockresize. Диски вне /var/lib/virtuality не трогаются.
- Клонирование: страница /vm/{name}/clone с проверками (машина выключена,
  хватает места, имя свободно), фоновая операция virt-clone с явными --file
  в хранилище образов, прогресс по росту файлов копии, pool-refresh после.
- Пауза, продолжение, спящий режим (managedsave как операция) и удаление
  сохранённого состояния — /vm/{name}/ops/{action}; пункты меню на обзоре
  и странице машины, кнопка «Продолжить» для приостановленной машины.
- Заметки: название и описание через virsh desc (--new-desc=ТЕКСТ одним
  аргументом, чтобы текст вида «--edit» не стал опцией); название видно
  под именем машины на обзоре (virsh list --all --title, парсер в presenters).
- Живая нагрузка: /live/stats по virsh domstats для всех активных машин
  одним вызовом, CPU % по разнице cpu.time между опросами; шкалы в строках
  обзора и карточка «Нагрузка сейчас» на странице машины (panel.js, 5 с).

Тесты для всех парсеров и маршрутов (tests/test_vmops.py), демо-данные
в dev_server.py (три машины в разных состояниях, живые шкалы), значки
notebook-pen и expand в спрайте.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
2026-09-25 15:00:41 +00:00

161 строка
6.7 KiB
Python

import os
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
WEB_DIR = ROOT / "web"
sys.path.insert(0, str(WEB_DIR))
os.environ.setdefault("VIRTUALITY_AUTH_USER", "tester")
os.environ.setdefault("VIRTUALITY_SESSION_SECRET", "test-secret-" + "0" * 32)
DOMINFO = """Id: 1
Name: web01
UUID: 6c1b3c1e-0000-0000-0000-000000000001
OS Type: hvm
State: running
CPU(s): 2
Max memory: 2097152 KiB
Used memory: 2097152 KiB
Persistent: yes
Autostart: enable
"""
DUMPXML = """<domain type='kvm'>
<name>web01</name>
<memory unit='KiB'>2097152</memory>
<vcpu placement='static'>2</vcpu>
<os><type arch='x86_64' machine='pc-q35'>hvm</type><boot dev='hd'/></os>
<devices>
<disk type='file' device='disk'><target dev='vda' bus='virtio'/></disk>
</devices>
</domain>
"""
FIXTURES = ROOT / "tests" / "fixtures"
# Заметки машин для `virsh desc`: имя → (название, описание).
NOTES = {"web01": ("Сайт компании", "Nginx + база. Бэкап по пятницам.")}
def fake_result(stdout: str = "", ok: bool = True, stderr: str = "") -> dict:
return {"ok": ok, "code": 0 if ok else 1, "stdout": stdout, "stderr": stderr, "cmd": ""}
def fake_run_cmd(cmd, timeout=12, **_kwargs):
joined = " ".join(cmd)
if cmd[:2] == ["virsh", "list"]:
if "--title" in cmd:
return fake_result((FIXTURES / "list-title.txt").read_text())
if "--name" in cmd:
return fake_result("web01\ndb01\n" if "--all" in cmd else "web01\n")
return fake_result(" Id Name State\n-----------------------\n 1 web01 running\n - db01 shut off")
if cmd[:2] == ["virsh", "dominfo"]:
return fake_result(DOMINFO) if cmd[-1] in ("web01", "db01") else fake_result(ok=False, stderr="failed to get domain")
if cmd[:2] == ["virsh", "dumpxml"]:
return fake_result(DUMPXML)
if cmd[:2] == ["virsh", "domstate"]:
return fake_result("running")
if cmd[:2] == ["virsh", "domblklist"]:
import core # диск лежит в подменённом data_dirs хранилище — иначе панель откажется его увеличивать
return fake_result((FIXTURES / "domblklist-details.txt").read_text().replace("/var/lib/virtuality/images", str(core.IMAGES_DIR)))
if cmd[:2] == ["virsh", "domstats"]:
return fake_result((FIXTURES / "domstats.txt").read_text())
if cmd[:1] == ["virsh"] and "desc" in cmd[:3]:
if any(arg.startswith("--new-desc") for arg in cmd):
return fake_result("Domain description updated successfully")
vm = cmd[cmd.index("desc") + 1]
title, description = NOTES.get(vm, ("", ""))
return fake_result(title if "--title" in cmd else description)
if cmd[:2] == ["qemu-img", "info"]:
return fake_result((FIXTURES / "qemu-img-info.json").read_text())
if cmd[:2] == ["virsh", "domifaddr"]:
return fake_result(" Name MAC address Protocol Address\n vnet0 52:54:00:aa:bb:cc ipv4 192.168.100.51/24")
if cmd[:2] == ["virsh", "domiflist"]:
return fake_result(" Interface Type Source Model MAC\n vnet0 network virtuality-nat virtio 52:54:00:aa:bb:cc")
if cmd[:2] == ["virsh", "vncdisplay"]:
return fake_result(":0")
if cmd[:2] == ["virsh", "pool-list"]:
return fake_result(" Name State Autostart\n---\n virtuality-images active yes")
if cmd[:2] == ["virsh", "net-list"]:
return fake_result(" Name State Autostart Persistent\n---\n virtuality-nat active yes yes")
if cmd[:2] == ["systemctl", "is-active"]:
return fake_result("active")
if cmd[:1] == ["hostname"]:
return fake_result("192.168.1.10" if "-I" in cmd else "virtuality-test")
if cmd[:3] == ["ip", "route", "show"]:
return fake_result("default via 192.168.1.1 dev eth0")
if cmd[:1] in (["uptime"], ["uname"]):
return fake_result("up 1 hour" if cmd[0] == "uptime" else "6.8.0")
if "nft" in joined or "iptables" in joined or "ufw" in joined or "sysctl" in joined:
return fake_result()
return fake_result()
@pytest.fixture()
def data_dirs(tmp_path, monkeypatch):
import app
import core
import host_profile
import network_core
import update_core
dirs = {
"iso": tmp_path / "iso",
"images": tmp_path / "images",
"disk_images": tmp_path / "disk-images",
"operations": tmp_path / "operations",
"network": tmp_path / "network",
"config": tmp_path / "config",
"nft": tmp_path / "nft",
"update": tmp_path / "update",
}
for path in dirs.values():
path.mkdir()
for module in (app, core):
monkeypatch.setattr(module, "ISO_DIR", dirs["iso"])
monkeypatch.setattr(module, "IMAGES_DIR", dirs["images"])
monkeypatch.setattr(module, "DISK_IMAGES_DIR", dirs["disk_images"])
monkeypatch.setattr(module, "OPERATIONS_DIR", dirs["operations"])
monkeypatch.setattr(core, "STORAGE_DIR", tmp_path)
monkeypatch.setattr(network_core, "CONFIG_DIR", dirs["config"])
monkeypatch.setattr(network_core, "NETWORK_DIR", dirs["network"])
monkeypatch.setattr(network_core, "NFT_DIR", dirs["nft"])
monkeypatch.setattr(network_core, "PORT_FORWARDS_FILE", dirs["network"] / "port_forwards.json")
monkeypatch.setattr(network_core, "NAT_XML_FILE", dirs["network"] / "virtuality-nat.xml")
monkeypatch.setattr(network_core, "NFT_FILE", dirs["nft"] / "virtuality.nft")
monkeypatch.setattr(network_core, "enable_ip_forward", lambda: None)
monkeypatch.setattr(network_core, "disable_rp_filter", lambda: None)
monkeypatch.setattr(host_profile, "PROFILE_FILE", dirs["config"] / "host_profile.json")
monkeypatch.setattr(update_core, "STATE_DIR", dirs["update"])
monkeypatch.setattr(update_core, "STATE_FILE", dirs["update"] / "state.json")
monkeypatch.setattr(update_core, "LOG_FILE", dirs["update"] / "update.log")
monkeypatch.setattr(update_core, "SOURCE_DIR", ROOT)
for module in (app, core, network_core, host_profile):
monkeypatch.setattr(module, "run_cmd", fake_run_cmd)
return dirs
@pytest.fixture()
def client(data_dirs):
from fastapi.testclient import TestClient
import app
app.login_throttle = app.auth.LoginThrottle()
return TestClient(app.app, base_url="http://panel.test")
@pytest.fixture()
def logged_in(client, monkeypatch):
import app
monkeypatch.setattr(app.auth, "verify_password", lambda user, password: password == "correct")
response = client.post("/login", data={"username": "tester", "password": "correct"}, follow_redirects=False)
assert response.status_code == 303
return client