Старт панели переведён с устаревшего on_event на lifespan, шаблоны вызываются как TemplateResponse(request, name): ушла сотня предупреждений FastAPI

Этот коммит содержится в:
2026-09-27 16:02:10 +09:00
родитель fc76f3f1fa
Коммит b7c87867fb
5 изменённых файлов: 56 добавлений и 38 удалений
+1 -1
Просмотреть файл
@@ -222,5 +222,5 @@ if __name__ == "__main__":
seed(Path(tempfile.mkdtemp(prefix="virtuality-dev-"))) seed(Path(tempfile.mkdtemp(prefix="virtuality-dev-")))
# The demo operations are seeded as "running": keep them instead of closing them as orphaned on startup. # The demo operations are seeded as "running": keep them instead of closing them as orphaned on startup.
app.app.router.on_startup.clear() app.run_startup_tasks = lambda: None
uvicorn.run(app.app, host="127.0.0.1", port=int(sys.argv[1]) if len(sys.argv) > 1 else 8765, log_level="warning") uvicorn.run(app.app, host="127.0.0.1", port=int(sys.argv[1]) if len(sys.argv) > 1 else 8765, log_level="warning")
+11
Просмотреть файл
@@ -139,6 +139,17 @@ def test_orphaned_operations_are_closed_on_startup(data_dirs):
assert core.active_operations_for("web01") == [] assert core.active_operations_for("web01") == []
def test_panel_startup_closes_orphaned_operations(data_dirs):
"""Стартовые задачи панели запускает lifespan (FastAPI on_event устарел)."""
import core
from fastapi.testclient import TestClient
running = core.new_operation("snapshot_create", "Снимок web01", vm_name="web01")
core.update_operation(running, status="running")
with TestClient(app.app, base_url="http://panel.test"):
assert core.read_operation(running["id"])["interrupted"] is True
def test_interrupt_leaves_operations_of_other_live_processes(data_dirs): def test_interrupt_leaves_operations_of_other_live_processes(data_dirs):
"""Копии по расписанию идут в отдельной службе: перезапуск панели не должен объявлять их прерванными.""" """Копии по расписанию идут в отдельной службе: перезапуск панели не должен объявлять их прерванными."""
import subprocess import subprocess
+41 -34
Просмотреть файл
@@ -18,6 +18,7 @@ from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import quote, urlsplit from urllib.parse import quote, urlsplit
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Form, UploadFile, File, WebSocket, WebSocketDisconnect from fastapi import FastAPI, Request, Form, UploadFile, File, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
@@ -45,7 +46,22 @@ OPERATIONS_DIR = Path("/var/log/virtuality/operations")
DEFAULT_BRIDGE = "br0" DEFAULT_BRIDGE = "br0"
NOVNC_DIR = next((p for p in [Path("/usr/share/novnc"), Path("/usr/share/novnc/app")] if p.exists()), None) NOVNC_DIR = next((p for p in [Path("/usr/share/novnc"), Path("/usr/share/novnc/app")] if p.exists()), None)
app = FastAPI(title="Virtuality Panel") def run_startup_tasks() -> None:
"""После перезапуска панели: закрыть задачи, оборванные перезапуском, и убрать недокачанные загрузки."""
import core
from features import downloads
core.interrupt_orphaned_operations()
downloads.close_stale_downloads()
@asynccontextmanager
async def lifespan(_app: FastAPI):
run_startup_tasks()
yield
app = FastAPI(title="Virtuality Panel", lifespan=lifespan)
templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
static_dir = BASE_DIR / "static" static_dir = BASE_DIR / "static"
static_dir.mkdir(exist_ok=True) static_dir.mkdir(exist_ok=True)
@@ -1260,7 +1276,7 @@ def virt_boot_arg(boot_order: str, is_arm: bool) -> str:
def vm_form_context(request: Request, error: str | None = None, form: dict[str, Any] | None = None, status_code: int = 200): def vm_form_context(request: Request, error: str | None = None, form: dict[str, Any] | None = None, status_code: int = 200):
profile = host_profile.load_host_profile() profile = host_profile.load_host_profile()
default_mode = profile.get("recommended_network", "nat") default_mode = profile.get("recommended_network", "nat")
return templates.TemplateResponse("vm_create.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "disk_images": list_disk_image_files(), "os_types": catalog.OS_TYPES, "boot_options": vm_boot_order_options(), "error": error, "profile": profile, "form": form or {"memory": 4096, "vcpus": 2, "disk_size": 40, "source_type": "iso", "boot_order": "auto", "network_mode": default_mode, "bridge": DEFAULT_BRIDGE}}, status_code=status_code) return templates.TemplateResponse(request, "vm_create.html", {"app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "disk_images": list_disk_image_files(), "os_types": catalog.OS_TYPES, "boot_options": vm_boot_order_options(), "error": error, "profile": profile, "form": form or {"memory": 4096, "vcpus": 2, "disk_size": 40, "source_type": "iso", "boot_order": "auto", "network_mode": default_mode, "bridge": DEFAULT_BRIDGE}}, status_code=status_code)
# Virtuality noVNC console patch # Virtuality noVNC console patch
@@ -1296,20 +1312,20 @@ async def proxy_websocket_to_vnc(websocket: WebSocket, writer: asyncio.StreamWri
def login_page(request: Request): def login_page(request: Request):
if get_current_user(request): if get_current_user(request):
return RedirectResponse(url="/", status_code=303) return RedirectResponse(url="/", status_code=303)
return templates.TemplateResponse("login.html", {"request": request, "app_name": APP_NAME, "error": None, "configured": is_configured(), "auth_user": AUTH_USER}) return templates.TemplateResponse(request, "login.html", {"app_name": APP_NAME, "error": None, "configured": is_configured(), "auth_user": AUTH_USER})
@app.post("/login", response_class=HTMLResponse) @app.post("/login", response_class=HTMLResponse)
def login_submit(request: Request, username: str = Form(...), password: str = Form(...)): def login_submit(request: Request, username: str = Form(...), password: str = Form(...)):
if not is_configured(): if not is_configured():
return templates.TemplateResponse("login.html", {"request": request, "app_name": APP_NAME, "error": "Панель ещё не настроена. Запусти установщик веб-панели повторно.", "configured": False, "auth_user": AUTH_USER}, status_code=500) return templates.TemplateResponse(request, "login.html", {"app_name": APP_NAME, "error": "Панель ещё не настроена. Запусти установщик веб-панели повторно.", "configured": False, "auth_user": AUTH_USER}, status_code=500)
key = client_key(request) key = client_key(request)
retry_after = login_throttle.retry_after(key) retry_after = login_throttle.retry_after(key)
if retry_after: if retry_after:
return templates.TemplateResponse("login.html", {"request": request, "app_name": APP_NAME, "error": f"Слишком много неудачных попыток входа. Повтори через {retry_after} сек.", "configured": True, "auth_user": AUTH_USER}, status_code=429, headers={"Retry-After": str(retry_after)}) return templates.TemplateResponse(request, "login.html", {"app_name": APP_NAME, "error": f"Слишком много неудачных попыток входа. Повтори через {retry_after} сек.", "configured": True, "auth_user": AUTH_USER}, status_code=429, headers={"Retry-After": str(retry_after)})
if not verify_linux_password(username, password): if not verify_linux_password(username, password):
login_throttle.record_failure(key) login_throttle.record_failure(key)
return templates.TemplateResponse("login.html", {"request": request, "app_name": APP_NAME, "error": "Неверный логин или пароль Linux-пользователя", "configured": True, "auth_user": AUTH_USER}, status_code=401) return templates.TemplateResponse(request, "login.html", {"app_name": APP_NAME, "error": "Неверный логин или пароль Linux-пользователя", "configured": True, "auth_user": AUTH_USER}, status_code=401)
login_throttle.record_success(key) login_throttle.record_success(key)
token = serializer.dumps({"user": AUTH_USER}) token = serializer.dumps({"user": AUTH_USER})
response = RedirectResponse(url="/", status_code=303) response = RedirectResponse(url="/", status_code=303)
@@ -1333,7 +1349,7 @@ def dashboard(request: Request):
return RedirectResponse(url="/setup", status_code=303) return RedirectResponse(url="/setup", status_code=303)
services = {"libvirtd": service_state("libvirtd.service"), "virtlogd": service_state("virtlogd.service"), "web": service_state("virtuality-web.service")} services = {"libvirtd": service_state("libvirtd.service"), "virtlogd": service_state("virtlogd.service"), "web": service_state("virtuality-web.service")}
service_rows = [{"key": key, "name": presenters.SERVICE_LABELS.get(key, key), "state": state, **presenters.service_state(state)} for key, state in services.items()] service_rows = [{"key": key, "name": presenters.SERVICE_LABELS.get(key, key), "state": state, **presenters.service_state(state)} for key, state in services.items()]
return templates.TemplateResponse("dashboard.html", {"request": request, "app_name": APP_NAME, "system": system_summary(), "services": services, "service_rows": service_rows, "services_ok": all(row["tone"] == "success" for row in service_rows if row["key"] in ("libvirtd", "web")), "vms": parse_virsh_list(), "pools": parse_pool_list(), "network": network_summary(), "user": AUTH_USER, "profile": host_profile.load_host_profile(), "operations": list_operations(5), "operation_css": operation_css, "host": presenters.host_stats(STORAGE_DIR), "greeting": presenters.greeting()}) return templates.TemplateResponse(request, "dashboard.html", {"app_name": APP_NAME, "system": system_summary(), "services": services, "service_rows": service_rows, "services_ok": all(row["tone"] == "success" for row in service_rows if row["key"] in ("libvirtd", "web")), "vms": parse_virsh_list(), "pools": parse_pool_list(), "network": network_summary(), "user": AUTH_USER, "profile": host_profile.load_host_profile(), "operations": list_operations(5), "operation_css": operation_css, "host": presenters.host_stats(STORAGE_DIR), "greeting": presenters.greeting()})
@app.get("/help", response_class=HTMLResponse) @app.get("/help", response_class=HTMLResponse)
@@ -1341,7 +1357,7 @@ def help_page(request: Request):
auth_redirect = require_auth(request) auth_redirect = require_auth(request)
if auth_redirect: if auth_redirect:
return auth_redirect return auth_redirect
return templates.TemplateResponse("help.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "host_ip": system_summary()["ip"]}) return templates.TemplateResponse(request, "help.html", {"app_name": APP_NAME, "user": AUTH_USER, "host_ip": system_summary()["ip"]})
@app.get("/host", response_class=HTMLResponse) @app.get("/host", response_class=HTMLResponse)
@@ -1350,7 +1366,7 @@ def host_page(request: Request):
if auth_redirect: if auth_redirect:
return auth_redirect return auth_redirect
profile = host_profile.load_host_profile() profile = host_profile.load_host_profile()
return templates.TemplateResponse("host.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "profile": profile, "profile_json": json.dumps(profile, ensure_ascii=False, indent=2)}) return templates.TemplateResponse(request, "host.html", {"app_name": APP_NAME, "user": AUTH_USER, "profile": profile, "profile_json": json.dumps(profile, ensure_ascii=False, indent=2)})
@app.post("/host/refresh") @app.post("/host/refresh")
@@ -1368,7 +1384,7 @@ def iso_page(request: Request, error: str | None = None):
auth_redirect = require_auth(request) auth_redirect = require_auth(request)
if auth_redirect: if auth_redirect:
return auth_redirect return auth_redirect
return templates.TemplateResponse("iso.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": error}) return templates.TemplateResponse(request, "iso.html", {"app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": error})
@app.post("/iso/upload", response_class=HTMLResponse) @app.post("/iso/upload", response_class=HTMLResponse)
@@ -1378,11 +1394,11 @@ def iso_upload(request: Request, iso_file: UploadFile = File(...)):
return auth_redirect return auth_redirect
safe_name = safe_iso_filename(iso_file.filename or "") safe_name = safe_iso_filename(iso_file.filename or "")
if not safe_name: if not safe_name:
return templates.TemplateResponse("iso.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": "Можно загружать только .iso файлы с безопасным именем."}, status_code=400) return templates.TemplateResponse(request, "iso.html", {"app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": "Можно загружать только .iso файлы с безопасным именем."}, status_code=400)
ISO_DIR.mkdir(parents=True, exist_ok=True) ISO_DIR.mkdir(parents=True, exist_ok=True)
target = ISO_DIR / safe_name target = ISO_DIR / safe_name
if target.exists(): if target.exists():
return templates.TemplateResponse("iso.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": f"ISO уже существует: {safe_name}"}, status_code=400) return templates.TemplateResponse(request, "iso.html", {"app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": f"ISO уже существует: {safe_name}"}, status_code=400)
tmp_target = ISO_DIR / f".{safe_name}.uploading" tmp_target = ISO_DIR / f".{safe_name}.uploading"
try: try:
with tmp_target.open("wb") as out: with tmp_target.open("wb") as out:
@@ -1391,7 +1407,7 @@ def iso_upload(request: Request, iso_file: UploadFile = File(...)):
refresh_iso_pool() refresh_iso_pool()
except Exception as exc: except Exception as exc:
tmp_target.unlink(missing_ok=True) tmp_target.unlink(missing_ok=True)
return templates.TemplateResponse("iso.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": f"Ошибка загрузки ISO: {exc}. Проверь свободное место, права на /var/lib/virtuality/iso и временный каталог /var/lib/virtuality/tmp."}, status_code=500) return templates.TemplateResponse(request, "iso.html", {"app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "error": f"Ошибка загрузки ISO: {exc}. Проверь свободное место, права на /var/lib/virtuality/iso и временный каталог /var/lib/virtuality/tmp."}, status_code=500)
return RedirectResponse(url="/iso", status_code=303) return RedirectResponse(url="/iso", status_code=303)
@@ -1424,7 +1440,7 @@ def disk_images_page(request: Request, error: str | None = None):
auth_redirect = require_auth(request) auth_redirect = require_auth(request)
if auth_redirect: if auth_redirect:
return auth_redirect return auth_redirect
return templates.TemplateResponse("disk_images.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "images": list_disk_image_files(), "error": error}) return templates.TemplateResponse(request, "disk_images.html", {"app_name": APP_NAME, "user": AUTH_USER, "images": list_disk_image_files(), "error": error})
@app.post("/disk-images/upload", response_class=HTMLResponse) @app.post("/disk-images/upload", response_class=HTMLResponse)
@@ -1434,7 +1450,7 @@ def disk_image_upload(request: Request, image_file: UploadFile = File(...)):
return auth_redirect return auth_redirect
safe_name = safe_disk_upload_filename(image_file.filename or "") safe_name = safe_disk_upload_filename(image_file.filename or "")
if not safe_name: if not safe_name:
return templates.TemplateResponse("disk_images.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "images": list_disk_image_files(), "error": "Можно загружать только .img, .raw, .qcow2, .img.xz, .zip, .tar.gz или .tgz файлы."}, status_code=400) return templates.TemplateResponse(request, "disk_images.html", {"app_name": APP_NAME, "user": AUTH_USER, "images": list_disk_image_files(), "error": "Можно загружать только .img, .raw, .qcow2, .img.xz, .zip, .tar.gz или .tgz файлы."}, status_code=400)
DISK_IMAGES_DIR.mkdir(parents=True, exist_ok=True) DISK_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
tmp_target = DISK_IMAGES_DIR / f".{safe_name}.uploading" tmp_target = DISK_IMAGES_DIR / f".{safe_name}.uploading"
saved_paths: list[Path] = [] saved_paths: list[Path] = []
@@ -1455,7 +1471,7 @@ def disk_image_upload(request: Request, image_file: UploadFile = File(...)):
tmp_target.unlink(missing_ok=True) tmp_target.unlink(missing_ok=True)
for path in saved_paths: for path in saved_paths:
path.unlink(missing_ok=True) path.unlink(missing_ok=True)
return templates.TemplateResponse("disk_images.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "images": list_disk_image_files(), "error": f"Ошибка загрузки/распаковки образа: {exc}"}, status_code=500) return templates.TemplateResponse(request, "disk_images.html", {"app_name": APP_NAME, "user": AUTH_USER, "images": list_disk_image_files(), "error": f"Ошибка загрузки/распаковки образа: {exc}"}, status_code=500)
operations = [] operations = []
for path in saved_paths: for path in saved_paths:
@@ -1507,8 +1523,7 @@ def safe_network_template(request: Request, error: str | None = None, status_cod
"nft_rules": "", "nft_rules": "",
} }
error = error or f"Ошибка чтения сетевого состояния: {exc}" error = error or f"Ошибка чтения сетевого состояния: {exc}"
return templates.TemplateResponse("network.html", { return templates.TemplateResponse(request, "network.html", {
"request": request,
"app_name": APP_NAME, "app_name": APP_NAME,
"user": AUTH_USER, "user": AUTH_USER,
"vms": parse_virsh_list(), "vms": parse_virsh_list(),
@@ -1535,7 +1550,7 @@ def network_vm_ip_save(request: Request, vm_name: str = Form(...), manual_ip: st
return RedirectResponse(url="/network", status_code=303) return RedirectResponse(url="/network", status_code=303)
manual_ip = (manual_ip or "").strip() manual_ip = (manual_ip or "").strip()
if manual_ip and not re.fullmatch(r"(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}", manual_ip): if manual_ip and not re.fullmatch(r"(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}", manual_ip):
return templates.TemplateResponse("network.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "vms": parse_virsh_list(), "ctx": network_core.network_context(), "error": "Некорректный ручной IP VM"}, status_code=400) return templates.TemplateResponse(request, "network.html", {"app_name": APP_NAME, "user": AUTH_USER, "vms": parse_virsh_list(), "ctx": network_core.network_context(), "error": "Некорректный ручной IP VM"}, status_code=400)
path = Path("/var/lib/virtuality/network/vm_ips.json") path = Path("/var/lib/virtuality/network/vm_ips.json")
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
try: try:
@@ -1616,8 +1631,7 @@ def network_diagnose(request: Request, vm_name: str = Form(...), external_port:
diagnostics = network_core.diagnose_public_access(vm_name, external_port, guest_port, protocol) diagnostics = network_core.diagnose_public_access(vm_name, external_port, guest_port, protocol)
except NetworkError as exc: except NetworkError as exc:
error = str(exc) error = str(exc)
return templates.TemplateResponse("network.html", { return templates.TemplateResponse(request, "network.html", {
"request": request,
"app_name": APP_NAME, "app_name": APP_NAME,
"user": AUTH_USER, "user": AUTH_USER,
"vms": parse_virsh_list(), "vms": parse_virsh_list(),
@@ -1657,7 +1671,7 @@ def update_page(request: Request):
} }
current = str(info.get("current_version", "")) current = str(info.get("current_version", ""))
current_release = next((item for item in update_core.load_manifest().get("versions", []) if str(item.get("version")) == current), None) current_release = next((item for item in update_core.load_manifest().get("versions", []) if str(item.get("version")) == current), None)
return templates.TemplateResponse("update.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "info": info, "error": error, "current_release": current_release}) return templates.TemplateResponse(request, "update.html", {"app_name": APP_NAME, "user": AUTH_USER, "info": info, "error": error, "current_release": current_release})
@app.get("/update/status") @app.get("/update/status")
@@ -1702,7 +1716,7 @@ def operations_page(request: Request):
auth_redirect = require_auth(request) auth_redirect = require_auth(request)
if auth_redirect: if auth_redirect:
return auth_redirect return auth_redirect
return templates.TemplateResponse("operations.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "operations": list_operations(50), "operation_css": operation_css}) return templates.TemplateResponse(request, "operations.html", {"app_name": APP_NAME, "user": AUTH_USER, "operations": list_operations(50), "operation_css": operation_css})
@app.get("/operations/{operation_id}", response_class=HTMLResponse) @app.get("/operations/{operation_id}", response_class=HTMLResponse)
@@ -1713,7 +1727,7 @@ def operation_detail_page(request: Request, operation_id: str):
operation = read_operation(operation_id) operation = read_operation(operation_id)
if not operation: if not operation:
return RedirectResponse(url="/operations", status_code=303) return RedirectResponse(url="/operations", status_code=303)
return templates.TemplateResponse("operation_detail.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "operation": operation, "operation_css": operation_css}) return templates.TemplateResponse(request, "operation_detail.html", {"app_name": APP_NAME, "user": AUTH_USER, "operation": operation, "operation_css": operation_css})
@app.get("/api/operations") @app.get("/api/operations")
@@ -1739,8 +1753,7 @@ def logs_page(request: Request, source: str = "web", lines: int = 220):
if auth_redirect: if auth_redirect:
return auth_redirect return auth_redirect
selected = read_log_source(source, lines) selected = read_log_source(source, lines)
return templates.TemplateResponse("logs.html", { return templates.TemplateResponse(request, "logs.html", {
"request": request,
"app_name": APP_NAME, "app_name": APP_NAME,
"user": AUTH_USER, "user": AUTH_USER,
"sources": LOG_SOURCES, "sources": LOG_SOURCES,
@@ -1905,7 +1918,7 @@ def vm_console_page(request: Request, name: str):
return auth_redirect return auth_redirect
if not valid_vm_name(name) or not vm_exists(name): if not valid_vm_name(name) or not vm_exists(name):
return RedirectResponse(url="/", status_code=303) return RedirectResponse(url="/", status_code=303)
return templates.TemplateResponse("console.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "vm": vm_details(name), "console": console_info(name)}) return templates.TemplateResponse(request, "console.html", {"app_name": APP_NAME, "user": AUTH_USER, "vm": vm_details(name), "console": console_info(name)})
@app.websocket("/console/ws/{token}") @app.websocket("/console/ws/{token}")
@@ -1955,7 +1968,7 @@ def vm_detail_page(request: Request, name: str):
return auth_redirect return auth_redirect
if not valid_vm_name(name) or not vm_exists(name): if not valid_vm_name(name) or not vm_exists(name):
return RedirectResponse(url="/", status_code=303) return RedirectResponse(url="/", status_code=303)
return templates.TemplateResponse("vm_detail.html", {"request": request, "app_name": APP_NAME, "user": AUTH_USER, "vm": vm_details(name), "host_ip": system_summary()["ip"], "boot_options": vm_boot_order_options(), "current_boot_order": current_vm_boot_order(name), "boot_message": request.query_params.get("boot_message", ""), "boot_error": request.query_params.get("boot_error", ""), "resource_settings": vm_resource_settings(name), "resource_message": request.query_params.get("resource_message", ""), "resource_error": request.query_params.get("resource_error", ""), "isos": list_iso_files(), "current_iso": current_vm_iso(name), "iso_message": request.query_params.get("iso_message", ""), "iso_error": request.query_params.get("iso_error", "")}) return templates.TemplateResponse(request, "vm_detail.html", {"app_name": APP_NAME, "user": AUTH_USER, "vm": vm_details(name), "host_ip": system_summary()["ip"], "boot_options": vm_boot_order_options(), "current_boot_order": current_vm_boot_order(name), "boot_message": request.query_params.get("boot_message", ""), "boot_error": request.query_params.get("boot_error", ""), "resource_settings": vm_resource_settings(name), "resource_message": request.query_params.get("resource_message", ""), "resource_error": request.query_params.get("resource_error", ""), "isos": list_iso_files(), "current_iso": current_vm_iso(name), "iso_message": request.query_params.get("iso_message", ""), "iso_error": request.query_params.get("iso_error", "")})
@app.post("/vm/{name}/resources") @app.post("/vm/{name}/resources")
@@ -2085,12 +2098,6 @@ def api_health(request: Request):
# Feature modules register template globals into app.templates, so they are imported once it exists. # Feature modules register template globals into app.templates, so they are imported once it exists.
from features import backup_schedule, backups, downloads, settings, setup, snapshots, vmops # noqa: E402 from features import backup_schedule, backups, downloads, settings, setup, snapshots, vmops # noqa: E402
@app.on_event("startup")
def close_orphaned_operations() -> None:
import core
core.interrupt_orphaned_operations()
app.include_router(setup.router) app.include_router(setup.router)
app.include_router(settings.router) app.include_router(settings.router)
+1 -1
Просмотреть файл
@@ -112,7 +112,7 @@ def render(request: Request, template: str, context: dict[str, Any] | None = Non
data = {"request": request, "app_name": APP_NAME, "user": AUTH_USER} data = {"request": request, "app_name": APP_NAME, "user": AUTH_USER}
data.update(context or {}) data.update(context or {})
data.update(extra) data.update(extra)
return templates.TemplateResponse(template, data, status_code=status_code) return templates.TemplateResponse(request, template, data, status_code=status_code)
# ---------------------------------------------------------------- auth # ---------------------------------------------------------------- auth
+2 -2
Просмотреть файл
@@ -783,9 +783,9 @@ def api_cancel_operation(request: Request, operation_id: str):
return {"ok": True, "operation": read_operation(operation_id) or operation} return {"ok": True, "operation": read_operation(operation_id) or operation}
@router.on_event("startup")
def close_stale_downloads() -> None: def close_stale_downloads() -> None:
"""После перезапуска панели потоков загрузки нет: закрываем такие операции и убираем их .part.""" """После перезапуска панели потоков загрузки нет: закрываем такие операции и убираем их .part.
Вызывается из app.run_startup_tasks() при старте панели."""
reap_stale_downloads() reap_stale_downloads()