docs: add docstrings to all API endpoints and daemon handlers
Add comprehensive docstrings to firewall, DHCP, proxy, wireguard, certs, and logs API endpoints. Document parameters, return values, and error cases for the documentation system.
This commit is contained in:
+113
-2
@@ -59,6 +59,14 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
||||
|
||||
|
||||
def get_config() -> dict[str, Any]:
|
||||
"""Load the current nginx config, initializing with defaults if needed.
|
||||
|
||||
Ensure config and sites directories exist, then return a copy of the
|
||||
JSON file. On missing file or missing keys, populate from defaults.
|
||||
|
||||
Returns:
|
||||
The complete config dict with ``domains``, ``ssl``, and ``management`` keys.
|
||||
"""
|
||||
ensure_dirs(CONFIG_DIR, SITES_DIR)
|
||||
raw = load_json(CONFIG_FILE)
|
||||
if not raw:
|
||||
@@ -69,10 +77,19 @@ def get_config() -> dict[str, Any]:
|
||||
|
||||
|
||||
def save_config(cfg: dict[str, Any]) -> None:
|
||||
"""Persist *cfg* to the nginx config file atomically."""
|
||||
save_json(CONFIG_FILE, cfg)
|
||||
|
||||
|
||||
def get_domains() -> list[dict[str, Any]]:
|
||||
"""Return a list of all configured proxy domains with status.
|
||||
|
||||
Each entry includes the domain name, backend info, SSL flag, and
|
||||
whether a site config file currently exists on disk.
|
||||
|
||||
Returns:
|
||||
List of dicts with ``domain``, ``backend``, ``online``, and ``force_ssl``.
|
||||
"""
|
||||
cfg = get_config()
|
||||
result: list[dict[str, Any]] = []
|
||||
for name, dom in cfg.get("domains", {}).items():
|
||||
@@ -101,6 +118,19 @@ def add_domain(
|
||||
cert: str | None = None,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
"""Add a new proxy domain with the given backend and optional settings.
|
||||
|
||||
Args:
|
||||
domain: Domain name to add.
|
||||
backend_host: Upstream host to proxy to.
|
||||
backend_port: Upstream port.
|
||||
backend_proto: Protocol (``http`` or ``https``).
|
||||
cert: Optional certificate type identifier.
|
||||
extra_headers: Optional dict of extra headers to forward.
|
||||
|
||||
Raises:
|
||||
ValueError: If the domain is already configured.
|
||||
"""
|
||||
cfg = get_config()
|
||||
if domain in cfg["domains"]:
|
||||
raise ValueError(f"Domain {domain!r} already configured")
|
||||
@@ -128,6 +158,7 @@ def add_domain(
|
||||
|
||||
|
||||
def remove_domain(domain: str) -> None:
|
||||
"""Remove *domain* from the config and delete its site file."""
|
||||
cfg = get_config()
|
||||
cfg["domains"].pop(domain, None)
|
||||
save_config(cfg)
|
||||
@@ -138,6 +169,15 @@ def remove_domain(domain: str) -> None:
|
||||
|
||||
|
||||
def update_domain(domain: str, **kwargs: Any) -> None:
|
||||
"""Update fields of an existing domain entry in-place.
|
||||
|
||||
Args:
|
||||
domain: Domain name to update.
|
||||
**kwargs: Key-value pairs to merge into the domain config.
|
||||
|
||||
Raises:
|
||||
KeyError: If the domain is not configured.
|
||||
"""
|
||||
cfg = get_config()
|
||||
if domain not in cfg["domains"]:
|
||||
raise KeyError(f"Domain {domain!r} not configured")
|
||||
@@ -157,6 +197,14 @@ def update_domain(domain: str, **kwargs: Any) -> None:
|
||||
|
||||
|
||||
def generate_server_conf(domain_cfg: dict[str, Any]) -> str:
|
||||
"""Render the Jinja template for a standard domain server block.
|
||||
|
||||
Args:
|
||||
domain_cfg: Domain entry dict including the ``domain`` key.
|
||||
|
||||
Returns:
|
||||
The complete nginx server-block configuration as a string.
|
||||
"""
|
||||
tmpl = ENV.get_template("nginx/server_block.conf")
|
||||
return tmpl.render(
|
||||
domain=domain_cfg["domain"],
|
||||
@@ -173,6 +221,14 @@ def generate_server_conf(domain_cfg: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def _generate_management_conf(management: dict[str, Any]) -> str:
|
||||
"""Render the Jinja template for the management WebUI server block.
|
||||
|
||||
Args:
|
||||
management: Management proxy config dict containing ``domain`` and optional ``auth``.
|
||||
|
||||
Returns:
|
||||
The complete nginx server-block configuration for the management UI.
|
||||
"""
|
||||
tmpl = ENV.get_template("nginx/server_block.conf")
|
||||
return tmpl.render(
|
||||
domain=management.get("domain"),
|
||||
@@ -196,6 +252,12 @@ def _generate_management_conf(management: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def write_site(domain: str, conf_text: str) -> None:
|
||||
"""Atomically write *conf_text* to the site config file for *domain*.
|
||||
|
||||
Args:
|
||||
domain: Domain name (becomes the ``<domain>.conf`` file).
|
||||
conf_text: Nginx server-block configuration text.
|
||||
"""
|
||||
ensure_dirs(SITES_DIR)
|
||||
path = SITES_DIR / f"{domain}.conf"
|
||||
tmp = path.with_suffix(".tmp")
|
||||
@@ -207,7 +269,7 @@ def write_site(domain: str, conf_text: str) -> None:
|
||||
|
||||
|
||||
def write_acme_challenge() -> None:
|
||||
"""Write the ACME HTTP-01 challenge catch-all nginx config.
|
||||
"""Write the catch-all nginx config for ACME HTTP-01 challenges.
|
||||
|
||||
Serves ``/.well-known/acme-challenge/`` on port 80 from the ACME
|
||||
webroot for any domain not yet covered by a dedicated server block.
|
||||
@@ -226,6 +288,12 @@ def write_acme_challenge() -> None:
|
||||
|
||||
|
||||
def write_all_sites() -> None:
|
||||
"""Regenerate all site configs from the current config state.
|
||||
|
||||
Writes server blocks for every configured domain and the management
|
||||
proxy (if any), removes orphaned site files, and ensures the ACME
|
||||
challenge config is present.
|
||||
"""
|
||||
ensure_dirs(SITES_DIR)
|
||||
cfg = get_config()
|
||||
|
||||
@@ -252,6 +320,11 @@ def write_all_sites() -> None:
|
||||
|
||||
|
||||
def write_include_file() -> None:
|
||||
"""Write the nginx include file that pulls in managed site configs.
|
||||
|
||||
The include file is installed at ``/etc/nginx/conf.d/vacuum-wall.conf``
|
||||
and must be owned by root.
|
||||
"""
|
||||
tmpl = ENV.get_template("nginx/include.conf")
|
||||
content = tmpl.render(sites_glob=str(SITES_DIR / "*.conf"))
|
||||
tmp = INCLUDE_FILE.with_suffix(".tmp")
|
||||
@@ -264,6 +337,11 @@ def write_include_file() -> None:
|
||||
|
||||
|
||||
def write_ssl_snippet() -> None:
|
||||
"""Write the shared SSL settings snippet to ``/etc/nginx/snippets/``.
|
||||
|
||||
The snippet is populated from the ``ssl`` section of the nginx config
|
||||
and installed with root ownership.
|
||||
"""
|
||||
cfg = get_config()
|
||||
ssl_cfg = cfg.get("ssl", {})
|
||||
ssl_cfg.setdefault("prefer_server_ciphers", DEFAULT_SSL["prefer_server_ciphers"])
|
||||
@@ -287,6 +365,12 @@ def write_ssl_snippet() -> None:
|
||||
|
||||
|
||||
def test_config() -> tuple[bool, str]:
|
||||
"""Run ``nginx -t`` and return the pass/fail result.
|
||||
|
||||
Returns:
|
||||
Tuple of ``(ok, message)`` where ``ok`` is ``True`` if the
|
||||
config test passed and ``message`` contains output or a summary.
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["sudo", "nginx", "-t"], capture_output=True, text=True, check=False
|
||||
)
|
||||
@@ -302,6 +386,11 @@ def test_config() -> tuple[bool, str]:
|
||||
|
||||
|
||||
def apply() -> None:
|
||||
"""Generate all configs, test them, and reload nginx.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the nginx config test fails.
|
||||
"""
|
||||
write_ssl_snippet()
|
||||
write_all_sites()
|
||||
write_include_file()
|
||||
@@ -329,6 +418,15 @@ def set_management_proxy(
|
||||
auth_user: str | None = None,
|
||||
auth_pass: str | None = None,
|
||||
) -> None:
|
||||
"""Configure the management reverse proxy for the WebUI.
|
||||
|
||||
Args:
|
||||
domain: Management domain name.
|
||||
flask_host: Upstream Flask host (default ``127.0.0.1``).
|
||||
flask_port: Upstream Flask port (default ``9090``).
|
||||
auth_user: Optional basic-auth username.
|
||||
auth_pass: Optional basic-auth password; writes htpasswd when provided with ``auth_user``.
|
||||
"""
|
||||
cfg = get_config()
|
||||
entry: dict[str, Any] = {
|
||||
"domain": domain,
|
||||
@@ -356,7 +454,12 @@ def set_management_proxy(
|
||||
|
||||
|
||||
def write_htpasswd(user: str, password: str) -> None:
|
||||
"""Append (or create) an htpasswd entry for *user*."""
|
||||
"""Append (or create) an htpasswd entry for *user*.
|
||||
|
||||
Args:
|
||||
user: Username for the htpasswd entry.
|
||||
password: Plain-text password to hash and store.
|
||||
"""
|
||||
ensure_dirs(DATA_DIR)
|
||||
hashed = _hash_password(password)
|
||||
existing: dict[str, str] = {}
|
||||
@@ -381,6 +484,14 @@ def write_htpasswd(user: str, password: str) -> None:
|
||||
|
||||
|
||||
def _hash_password(password: str) -> str:
|
||||
"""Hash *password* using Apache ``apr1`` format via passlib, with crypt fallback.
|
||||
|
||||
Args:
|
||||
password: Plain-text password to hash.
|
||||
|
||||
Returns:
|
||||
The hashed password string suitable for ``.htpasswd``.
|
||||
"""
|
||||
try:
|
||||
from passlib.hash import apache_passwd
|
||||
|
||||
|
||||
Reference in New Issue
Block a user