refactor: overhaul daemon server, client, and handlers

This commit is contained in:
2026-06-16 03:35:58 +00:00
parent c5813d68b3
commit 4fc0fb3f72
10 changed files with 683 additions and 195 deletions
+72 -52
View File
@@ -5,14 +5,19 @@ Communicates with vacuum-walld over a Unix socket using requests-unixsocket.
import json
import logging
import re
import urllib.parse
from typing import Any
import requests
import requests_unixsocket
from daemon.iface import PathLike
logger = logging.getLogger(__name__)
_param_re = re.compile(r"<(\w+)>")
class NotFound(Exception):
"""Raised when the daemon returns HTTP 404."""
@@ -68,9 +73,41 @@ def set_socket_path(path: str) -> None:
_DEFAULT_SOCKET = path
def _format_path(path: str, params: dict[str, Any] | None) -> str:
"""Replace ``<param>`` path segments with URL-encoded values from *params*.
Args:
path: URL path that may contain ``<key>`` placeholders.
params: Dict of parameter values to substitute.
Returns:
Path with all ``<key>`` segments replaced by their URL-encoded
values. Unmatched placeholders are left unchanged.
"""
if params is None:
return path
def _replace(m: re.Match[str]) -> str:
key = m.group(1)
if key in params:
return urllib.parse.quote(str(params[key]), safe="")
return m.group(0)
return _param_re.sub(_replace, path)
def _resolve_path(method_or_ep: PathLike, path: str | None = None) -> tuple[str, str]:
"""Resolve method/path from an Endpoint tuple or two separate arguments."""
if isinstance(method_or_ep, tuple):
return (method_or_ep[0], method_or_ep[1])
if path is None:
raise ValueError("path is required when method is a string")
return (method_or_ep, path)
def request(
method: str,
path: str,
method: PathLike,
path: str | None = None,
json_body: dict[str, Any] | None = None,
query_params: dict[str, Any] | None = None,
socket_path: str | None = None,
@@ -78,27 +115,40 @@ def request(
) -> dict[str, Any]:
"""Make a request to the daemon and return the parsed response body.
*method* can be an :class:`Endpoint` tuple from :mod:`daemon.iface`,
in which case *path* should be omitted.
For GET requests, query_params are sent as URL query parameters instead
of a JSON body. For other methods, json_body is sent as JSON.
Raises RuntimeError on non-2xx responses or connection errors.
Raises NotFound on HTTP 404. Raises BadRequest on HTTP 400.
"""
resolved_method, resolved_path = _resolve_path(method, path)
# Substitute <param> segments from body/query params so the daemon
# receives a concrete path instead of a template.
# Merge body and query params for <param> substitution. query_params
# takes precedence on key conflicts, so callers should avoid passing
# the same key in both dicts.
combined = {**(json_body or {}), **(query_params or {})}
formatted_path = _format_path(resolved_path, combined)
sp = socket_path or _get_socket_path()
url = f"http+unix://{urllib.parse.quote(sp, safe='')}{path}"
url = f"http+unix://{urllib.parse.quote(sp, safe='')}{formatted_path}"
sess = requests_unixsocket.Session()
try:
kwargs: dict[str, Any] = {
"timeout": timeout,
}
if method == "GET":
if resolved_method == "GET":
if query_params:
kwargs["params"] = query_params
else:
if json_body is not None:
kwargs["json"] = json_body
resp = sess.request(
method,
resolved_method,
url,
**kwargs,
)
@@ -112,7 +162,9 @@ def request(
except requests.ConnectionError as exc:
raise RuntimeError(f"Cannot connect to daemon at {sp}: {exc}") from exc
except requests.Timeout as exc:
raise RuntimeError(f"Daemon request timed out: {method} {path}") from exc
raise RuntimeError(
f"Daemon request timed out: {resolved_method} {resolved_path}"
) from exc
except requests.HTTPError as exc:
try:
data = resp.json()
@@ -129,72 +181,40 @@ def request(
return data.get("data")
def get(path: str, params: dict[str, Any] | None = None, **kwargs: Any) -> Any:
def get(path: PathLike, params: dict[str, Any] | None = None, **kwargs: Any) -> Any:
"""Send a GET request to the daemon.
Query parameters are passed as URL params rather than a JSON body.
Additional keyword arguments are forwarded to request().
Args:
path: URL path to request on the daemon.
params: Optional query parameters to append to the URL.
**kwargs: Extra arguments forwarded to request().
Returns:
The parsed JSON response data from the daemon.
*path* can be a :class:`Endpoint` tuple from :mod:`daemon.iface`
(e.g., ``GET_FIREWALL_ZONES``), or a plain string path.
"""
return request("GET", path, query_params=params, **kwargs)
return request(path, query_params=params, **kwargs)
def post(path: str, body: dict[str, Any] | None = None, **kwargs: Any) -> Any:
def post(path: PathLike, body: dict[str, Any] | None = None, **kwargs: Any) -> Any:
"""Send a POST request to the daemon.
The body is transmitted as a JSON payload. Extra keyword arguments
are forwarded to request().
Args:
path: URL path to request on the daemon.
body: Optional JSON-serializable payload.
**kwargs: Extra arguments forwarded to request().
Returns:
The parsed JSON response data from the daemon.
are forwarded to request(). *path* can be an :class:`Endpoint` tuple.
"""
return request("POST", path, json_body=body, **kwargs)
return request(path, json_body=body, **kwargs)
def patch(path: str, body: dict[str, Any] | None = None, **kwargs: Any) -> Any:
def patch(path: PathLike, body: dict[str, Any] | None = None, **kwargs: Any) -> Any:
"""Send a PATCH request to the daemon.
The body is transmitted as a JSON payload. Extra keyword arguments
are forwarded to request().
Args:
path: URL path to request on the daemon.
body: Optional JSON-serializable payload.
**kwargs: Extra arguments forwarded to request().
Returns:
The parsed JSON response data from the daemon.
are forwarded to request(). *path* can be an :class:`Endpoint` tuple.
"""
return request("PATCH", path, json_body=body, **kwargs)
return request(path, json_body=body, **kwargs)
def delete(path: str, body: dict[str, Any] | None = None, **kwargs: Any) -> Any:
def delete(path: PathLike, body: dict[str, Any] | None = None, **kwargs: Any) -> Any:
"""Send a DELETE request to the daemon.
The body is transmitted as a JSON payload. Extra keyword arguments
are forwarded to request().
Args:
path: URL path to request on the daemon.
body: Optional JSON-serializable payload.
**kwargs: Extra arguments forwarded to request().
Returns:
The parsed JSON response data from the daemon.
are forwarded to request(). *path* can be an :class:`Endpoint` tuple.
"""
return request("DELETE", path, json_body=body, **kwargs)
return request(path, json_body=body, **kwargs)
def batch(ops: list[dict[str, Any]], **kwargs: Any) -> dict[str, Any]:
@@ -203,4 +223,4 @@ def batch(ops: list[dict[str, Any]], **kwargs: Any) -> dict[str, Any]:
Each op is a dict with 'id', 'method', 'path', and optionally 'body'.
Returns a dict mapping each id to its result.
"""
return post("/batch", {"ops": ops}, **kwargs)
return request("POST", "/batch", json_body={"ops": ops}, **kwargs)