Старт панели переведён с устаревшего on_event на lifespan, шаблоны вызываются как TemplateResponse(request, name): ушла сотня предупреждений FastAPI
Этот коммит содержится в:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user