Страница «Настройки» была добавлена: доступ, пароль, сеть машин, хранилище, время, обновления, сервер
- web/features/settings.py + settings.html: всё, что спрашивал мастер, меняется в одном месте - перезагрузка/выключение с ожиданием возврата панели, архив настроек и отчёт для поддержки - сообщения после редиректов показываются на любой странице (?message=, ?error=) - virtuality-ctl set: настройки обновлений применяются без перезапуска панели - tests/test_settings.py Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
@@ -44,7 +44,7 @@ def seed(root: Path) -> None:
|
||||
nodectl.WIZARD_FILE = dirs["config"] / "wizard.json"
|
||||
stage = os.environ.get("VIRTUALITY_DEV_SETUP", "")
|
||||
if stage:
|
||||
# VIRTUALITY_DEV_SETUP=installing|done shows the setup wizard in the demo.
|
||||
# VIRTUALITY_DEV_SETUP=installing|done shows the setup wizard in the demo; finished = wizard completed (Settings page).
|
||||
steps = [("panel", "Панель управления", "done"), ("network-wait", "Подключение к интернету", "done"), ("virtualization", "KVM, QEMU и libvirt", "running" if stage == "installing" else "done"), ("tools", "Инструменты диагностики", "pending" if stage == "installing" else "done"), ("finish", "Завершение", "pending" if stage == "installing" else "done")]
|
||||
nodectl.SETUP_STATE.write_text(json.dumps({"stage": "installing" if stage == "installing" else "done", "message": "Устанавливаем qemu-system-x86 (14 из 61)…", "steps": [{"id": i, "title": t, "status": s} for i, t, s in steps]}))
|
||||
nodectl.FIRSTBOOT_LOG = dirs["config"] / "firstboot.log"
|
||||
@@ -54,6 +54,11 @@ def seed(root: Path) -> None:
|
||||
nodectl.network_facts = lambda: {"interface": "enp3s0", "gateway": "192.168.1.1", "address": "192.168.1.10", "prefix": 24, "mac": "aa:bb:cc:dd:ee:ff", "wireless": False, "private": True, "is_vps": False, "virt": "none", "bridge_present": False, "on_bridge": False, "recommended": "bridge", "revert_armed": False, "bridge_possible": True}
|
||||
nodectl.hardware_summary = lambda: {"hostname": "home-server", "cpu_model": "Intel Core i5-12400", "cpu_count": 6, "mem_total": 32 * 1024 ** 3, "disk_total": 480 * 1024 ** 3, "disk_free": 401 * 1024 ** 3, "kvm": True}
|
||||
nodectl.set_timezone = lambda tz: (True, tz)
|
||||
nodectl.timezones = lambda: {"current": "Europe/Moscow", "popular": nodectl.POPULAR_TIMEZONES, "all": nodectl.POPULAR_TIMEZONES}
|
||||
if stage == "finished":
|
||||
nodectl.mark_wizard_done()
|
||||
nodectl.NODE_ENV = dirs["config"] / "web.env"
|
||||
nodectl.NODE_ENV.write_text("VIRTUALITY_WEB_PORT=8088\nVIRTUALITY_TLS=1\nVIRTUALITY_AUTO_UPDATE=1\nVIRTUALITY_UPDATE_CHANNEL=stable\n")
|
||||
|
||||
for name, size in (("ubuntu-26.04-live-server-amd64.iso", 3), ("debian-13.7.0-amd64-netinst.iso", 1)):
|
||||
with (dirs["iso"] / name).open("wb") as handle:
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import nodectl
|
||||
from test_setup import prepare
|
||||
|
||||
|
||||
def ready(data_dirs, monkeypatch, ctl=True):
|
||||
prepare(data_dirs, monkeypatch)
|
||||
nodectl.mark_wizard_done()
|
||||
monkeypatch.setattr(nodectl, "ctl_available", lambda: ctl)
|
||||
nodectl.NODE_ENV = data_dirs["config"] / "web.env"
|
||||
nodectl.NODE_ENV.write_text("VIRTUALITY_WEB_PORT=8088\nVIRTUALITY_TLS=0\nVIRTUALITY_AUTO_UPDATE=1\nVIRTUALITY_UPDATE_CHANNEL=stable\n")
|
||||
|
||||
|
||||
def test_settings_page_renders(logged_in, data_dirs, monkeypatch):
|
||||
ready(data_dirs, monkeypatch)
|
||||
html = logged_in.get("/settings").text
|
||||
for text in ("Доступ к панели", "Сеть машин", "Хранилище", "Часовой пояс", "Обновления", "Перезагрузить", "http://panel.test:8088"):
|
||||
assert text in html, text
|
||||
|
||||
|
||||
def test_settings_page_without_ctl_disables_actions(logged_in, data_dirs, monkeypatch):
|
||||
ready(data_dirs, monkeypatch, ctl=False)
|
||||
html = logged_in.get("/settings").text
|
||||
assert "virtuality-ctl не найдена" in html and "disabled" in html
|
||||
|
||||
|
||||
def test_settings_requires_login(client, data_dirs, monkeypatch):
|
||||
ready(data_dirs, monkeypatch)
|
||||
assert client.get("/settings", follow_redirects=False).status_code == 303
|
||||
|
||||
|
||||
def test_access_change_applies_and_shows_new_url(logged_in, data_dirs, monkeypatch):
|
||||
ready(data_dirs, monkeypatch)
|
||||
calls = []
|
||||
monkeypatch.setattr(nodectl, "ctl_detached", lambda *args, **kw: calls.append(args) or {"ok": True, "stdout": "", "stderr": "", "code": 0, "cmd": ""})
|
||||
response = logged_in.post("/settings/access", data={"tls": "1", "port": "8088"})
|
||||
assert response.status_code == 200 and "https://panel.test:8443" in response.text
|
||||
assert calls == [("set", "VIRTUALITY_TLS=1", "VIRTUALITY_WEB_PORT=8088")]
|
||||
assert logged_in.post("/settings/access", data={"tls": "0", "port": "22"}).status_code == 400
|
||||
unchanged = logged_in.post("/settings/access", data={"tls": "0", "port": "8088"}, follow_redirects=False)
|
||||
assert unchanged.status_code == 303 and "message=" in unchanged.headers["location"]
|
||||
|
||||
|
||||
def test_password_change(logged_in, data_dirs, monkeypatch):
|
||||
ready(data_dirs, monkeypatch)
|
||||
seen = {}
|
||||
monkeypatch.setattr(nodectl, "ctl", lambda *args, **kw: seen.update(args=args, stdin=kw.get("stdin")) or {"ok": True, "stdout": "", "stderr": "", "code": 0, "cmd": ""})
|
||||
assert logged_in.post("/settings/password", data={"password": "abc", "password2": "abd"}).status_code == 400
|
||||
assert logged_in.post("/settings/password", data={"password": "abc", "password2": "abc"}).status_code == 400
|
||||
response = logged_in.post("/settings/password", data={"password": "secret1", "password2": "secret1"}, follow_redirects=False)
|
||||
assert response.status_code == 303 and seen["args"] == ("passwd", "tester") and seen["stdin"] == "secret1\n"
|
||||
|
||||
|
||||
def test_timezone_and_updates(logged_in, data_dirs, monkeypatch):
|
||||
ready(data_dirs, monkeypatch)
|
||||
calls = []
|
||||
monkeypatch.setattr(nodectl, "ctl", lambda *args, **kw: calls.append(args) or {"ok": True, "stdout": "", "stderr": "", "code": 0, "cmd": ""})
|
||||
monkeypatch.setattr(nodectl, "ctl_detached", lambda *args, **kw: calls.append(args) or {"ok": True, "stdout": "", "stderr": "", "code": 0, "cmd": ""})
|
||||
assert logged_in.post("/settings/timezone", data={"timezone": "Europe/Moscow"}, follow_redirects=False).status_code == 303
|
||||
assert logged_in.post("/settings/timezone", data={"timezone": "../etc"}).status_code == 400
|
||||
assert logged_in.post("/settings/updates", data={"channel": "main"}, follow_redirects=False).status_code == 303
|
||||
assert calls == [("timezone", "Europe/Moscow"), ("set", "VIRTUALITY_AUTO_UPDATE=0", "VIRTUALITY_UPDATE_CHANNEL=main")]
|
||||
|
||||
|
||||
def test_network_bridge_and_revert(logged_in, data_dirs, monkeypatch):
|
||||
ready(data_dirs, monkeypatch)
|
||||
calls = []
|
||||
monkeypatch.setattr(nodectl, "enable_bridge", lambda iface, mode="dhcp": calls.append(("bridge", iface, mode)) or (True, "ok"))
|
||||
monkeypatch.setattr(nodectl, "revert_bridge", lambda: calls.append(("revert",)) or (True, "Возвращаем прежнюю сеть"))
|
||||
response = logged_in.post("/settings/network", data={"mode": "bridge"})
|
||||
assert response.status_code == 200 and "Перенастраиваем сеть" in response.text and 'data-next-url="/settings"' in response.text
|
||||
assert logged_in.post("/settings/network/revert", follow_redirects=False).headers["location"].startswith("/settings?message=")
|
||||
assert calls == [("bridge", "enp3s0", "dhcp"), ("revert",)]
|
||||
|
||||
|
||||
def test_storage_power_and_downloads(logged_in, data_dirs, monkeypatch, tmp_path):
|
||||
ready(data_dirs, monkeypatch)
|
||||
monkeypatch.setattr(nodectl, "use_disk_for_storage", lambda path: (path == "/dev/sdb", "Диск подключён" if path == "/dev/sdb" else "Нельзя"))
|
||||
assert "message=" in logged_in.post("/settings/storage", data={"disk": "/dev/sdb"}, follow_redirects=False).headers["location"]
|
||||
assert "error=" in logged_in.post("/settings/storage", data={"disk": "/dev/sda"}, follow_redirects=False).headers["location"]
|
||||
monkeypatch.setattr(nodectl, "ctl_detached", lambda *args, **kw: {"ok": True, "stdout": "", "stderr": "", "code": 0, "cmd": ""})
|
||||
assert "Перезагружаем сервер" in logged_in.post("/settings/power", data={"action": "reboot"}).text
|
||||
assert logged_in.post("/settings/power", data={"action": "halt"}).status_code == 400
|
||||
archive = tmp_path / "config-1.tar.gz"
|
||||
archive.write_bytes(b"\x1f\x8b" + b"0" * 16)
|
||||
monkeypatch.setattr(nodectl, "ctl", lambda *args, **kw: {"ok": True, "stdout": str(archive), "stderr": "", "code": 0, "cmd": ""})
|
||||
response = logged_in.post("/settings/backup-config")
|
||||
assert response.status_code == 200 and response.headers["content-disposition"].endswith('"config-1.tar.gz"') and response.content.startswith(b"\x1f\x8b")
|
||||
assert logged_in.post("/settings/support-bundle").status_code == 200
|
||||
@@ -67,7 +67,7 @@ def test_wizard_steps_render_and_store_choices(logged_in, data_dirs, monkeypatch
|
||||
def test_finish_marks_done_without_ctl(logged_in, data_dirs, monkeypatch):
|
||||
prepare(data_dirs, monkeypatch)
|
||||
response = logged_in.post("/setup/finish", follow_redirects=False)
|
||||
assert response.status_code == 303 and response.headers["location"].startswith("/?setup_message=")
|
||||
assert response.status_code == 303 and response.headers["location"].startswith("/?message=")
|
||||
assert nodectl.wizard_done()
|
||||
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user