feat: pre-computed state store and async ACME issuance (fixes timeout mismatch)
- Add lib/state.py: in-memory state store with subsystem collectors (firewall, dnsmasq, nginx, acme, wireguard) - Refactor all handlers: read from state on GET, call refresh_state() after mutations instead of invoking subprocesses per request - daemon/server.py: add refresh_state(), /status/all, /status/refresh; populate state at startup - webui/api/certs.py: async step-by-step ACME issuance (validate, issue with request_id, poll status) replacing blocking endpoint - webui/server.py: render pages from state instead of direct lib calls - Update templates, JS for async cert issuance with polling UI - Update tests for state-based mocking; add test_state.py - Fix SIM105 lint issue (contextlib.suppress) - Add TODO.md with certificate issuance issue tracking Resolves: WebUI 30s timeout freeze during cert issuance (Problem 1)
This commit is contained in:
+45
-32
@@ -8,7 +8,7 @@ from typing import Any
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from daemon.server import NotFoundError, registry
|
||||
from daemon.server import NotFoundError, refresh_state, registry
|
||||
from lib.common import ensure_dirs, load_json, run, run_proc, save_json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -48,7 +48,11 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"ssl": {**DEFAULT_SSL},
|
||||
}
|
||||
|
||||
_NGINX_TAGS = {"nginx"}
|
||||
|
||||
def _get_state() -> dict[str, Any] | None:
|
||||
from lib.state import state as state_store
|
||||
|
||||
return state_store.get("nginx")
|
||||
|
||||
|
||||
def _get_config() -> dict[str, Any]:
|
||||
@@ -122,9 +126,7 @@ def _write_ssl_snippet() -> None:
|
||||
|
||||
|
||||
def _test_config() -> tuple[bool, str]:
|
||||
result = run_proc(
|
||||
["nginx", "-t"], sudo=True, check=False
|
||||
)
|
||||
result = run_proc(["nginx", "-t"], sudo=True, check=False)
|
||||
ok = result.returncode == 0
|
||||
output = (result.stderr or result.stdout or "").strip()
|
||||
if not output and ok:
|
||||
@@ -133,9 +135,7 @@ def _test_config() -> tuple[bool, str]:
|
||||
|
||||
|
||||
def _reload_nginx() -> None:
|
||||
result = run_proc(
|
||||
["nginx", "-s", "reload"], sudo=True, check=False
|
||||
)
|
||||
result = run_proc(["nginx", "-s", "reload"], sudo=True, check=False)
|
||||
if result.returncode != 0:
|
||||
logger.error("nginx reload failed: %s", result.stderr.strip())
|
||||
else:
|
||||
@@ -210,20 +210,35 @@ def _write_htpasswd(user: str, password: str) -> None:
|
||||
os.replace(tmp, HTPASSWD_FILE)
|
||||
|
||||
|
||||
@registry.register("GET", "/nginx/config", cache_tags=_NGINX_TAGS)
|
||||
def _get_nginx_state() -> dict[str, Any]:
|
||||
ng = _get_state()
|
||||
if ng is None:
|
||||
return {}
|
||||
return ng
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes
|
||||
|
||||
|
||||
@registry.register("GET", "/nginx/config")
|
||||
def get_config(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
ng = _get_nginx_state()
|
||||
if ng:
|
||||
return ng.get("config", {})
|
||||
return _get_config()
|
||||
|
||||
|
||||
@registry.register("POST", "/nginx/config", invalidate=_NGINX_TAGS)
|
||||
@registry.register("POST", "/nginx/config")
|
||||
def save_config_handler(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
_save_config(body)
|
||||
refresh_state(["nginx"])
|
||||
return {"config_saved": True}
|
||||
|
||||
|
||||
@registry.register("PATCH", "/nginx/config", invalidate=_NGINX_TAGS)
|
||||
@registry.register("PATCH", "/nginx/config")
|
||||
def patch_config(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -232,27 +247,19 @@ def patch_config(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
current = _get_config()
|
||||
merged = deep_merge(current, body)
|
||||
_save_config(merged)
|
||||
refresh_state(["nginx"])
|
||||
return {"config_saved": True}
|
||||
|
||||
|
||||
@registry.register("GET", "/nginx/domains", cache_tags=_NGINX_TAGS)
|
||||
@registry.register("GET", "/nginx/domains")
|
||||
def get_domains(_request: Any, _body: Any) -> list[dict[str, Any]]:
|
||||
cfg = _get_config()
|
||||
result: list[dict[str, Any]] = []
|
||||
for name, dom in cfg.get("domains", {}).items():
|
||||
site = SITES_DIR / f"{name}.conf"
|
||||
result.append(
|
||||
{
|
||||
"domain": name,
|
||||
"backend": dom.get("backend", {}),
|
||||
"online": site.exists(),
|
||||
"force_ssl": dom.get("force_ssl", True),
|
||||
}
|
||||
)
|
||||
return result
|
||||
ng = _get_nginx_state()
|
||||
if ng:
|
||||
return ng.get("domains", [])
|
||||
return []
|
||||
|
||||
|
||||
@registry.register("POST", "/nginx/domains/add", invalidate=_NGINX_TAGS)
|
||||
@registry.register("POST", "/nginx/domains/add")
|
||||
def add_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -285,10 +292,11 @@ def add_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
entry["headers"] = extra_headers
|
||||
cfg["domains"][domain] = entry
|
||||
_save_config(cfg)
|
||||
refresh_state(["nginx"])
|
||||
return {"domain": domain}
|
||||
|
||||
|
||||
@registry.register("DELETE", "/nginx/domains/remove", invalidate=_NGINX_TAGS)
|
||||
@registry.register("DELETE", "/nginx/domains/remove")
|
||||
def remove_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -303,10 +311,11 @@ def remove_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
site = SITES_DIR / f"{domain}.conf"
|
||||
if site.exists():
|
||||
site.unlink()
|
||||
refresh_state(["nginx"])
|
||||
return {"domain": domain}
|
||||
|
||||
|
||||
@registry.register("POST", "/nginx/domains/update", invalidate=_NGINX_TAGS)
|
||||
@registry.register("POST", "/nginx/domains/update")
|
||||
def update_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -324,10 +333,11 @@ def update_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
else:
|
||||
entry[key] = val
|
||||
_save_config(cfg)
|
||||
refresh_state(["nginx"])
|
||||
return {"domain": domain}
|
||||
|
||||
|
||||
@registry.register("POST", "/nginx/apply", invalidate=_NGINX_TAGS)
|
||||
@registry.register("POST", "/nginx/apply")
|
||||
def apply(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
_write_ssl_snippet()
|
||||
_write_all_sites()
|
||||
@@ -336,22 +346,24 @@ def apply(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
if not ok:
|
||||
raise RuntimeError(f"nginx config test failed: {msg}")
|
||||
_reload_nginx()
|
||||
refresh_state(["nginx"])
|
||||
return {"applied": True}
|
||||
|
||||
|
||||
@registry.register("POST", "/nginx/test", invalidate=_NGINX_TAGS)
|
||||
@registry.register("POST", "/nginx/test")
|
||||
def test(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
valid, output = _test_config()
|
||||
return {"valid": valid, "output": output}
|
||||
|
||||
|
||||
@registry.register("POST", "/nginx/ssl-apply", invalidate=_NGINX_TAGS)
|
||||
@registry.register("POST", "/nginx/ssl-apply")
|
||||
def ssl_apply(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
_write_ssl_snippet()
|
||||
refresh_state(["nginx"])
|
||||
return {"applied": True}
|
||||
|
||||
|
||||
@registry.register("POST", "/nginx/management", invalidate=_NGINX_TAGS)
|
||||
@registry.register("POST", "/nginx/management")
|
||||
def set_management_proxy(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -373,6 +385,7 @@ def set_management_proxy(_request: Any, body: dict[str, Any] | None) -> dict[str
|
||||
_save_config(cfg)
|
||||
if auth_user and auth_pass:
|
||||
_write_htpasswd(auth_user, auth_pass)
|
||||
refresh_state(["nginx"])
|
||||
return {"domain": domain}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user