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:
@@ -37,6 +37,15 @@ _ISSUANCE_TTL = 300 # seconds to keep completed requests
|
||||
|
||||
@dataclass
|
||||
class IssueStep:
|
||||
"""Single step in a certificate issuance workflow.
|
||||
|
||||
Attributes:
|
||||
name: Machine-readable step identifier (e.g. "issue").
|
||||
label: Human-readable description shown to the user.
|
||||
status: Current state: "pending", "running", "done", or "error".
|
||||
message: Optional detail or error message for the step.
|
||||
"""
|
||||
|
||||
name: str
|
||||
label: str
|
||||
status: str = "pending"
|
||||
@@ -45,6 +54,19 @@ class IssueStep:
|
||||
|
||||
@dataclass
|
||||
class IssueRequest:
|
||||
"""Tracked certificate issuance request.
|
||||
|
||||
Attributes:
|
||||
request_id: Unique hex identifier for polling.
|
||||
domain: Target domain for the certificate.
|
||||
email: Optional ACME contact email.
|
||||
webroot: Optional custom webroot path.
|
||||
steps: Ordered list of issuance steps.
|
||||
status: Overall status: "running", "completed", or "failed".
|
||||
created_at: Unix timestamp when request was created.
|
||||
expires_at: Unix timestamp when entry expires from store.
|
||||
"""
|
||||
|
||||
request_id: str
|
||||
domain: str
|
||||
email: str | None = None
|
||||
@@ -55,6 +77,7 @@ class IssueRequest:
|
||||
expires_at: float | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Serialize request to a JSON-compatible dictionary."""
|
||||
return {
|
||||
"request_id": self.request_id,
|
||||
"domain": self.domain,
|
||||
@@ -78,6 +101,14 @@ class IssueRequest:
|
||||
|
||||
|
||||
def _run_acme(args: list[str]) -> str:
|
||||
"""Execute an acme.sh command and return combined output.
|
||||
|
||||
Returns:
|
||||
Standard output (plus stderr).
|
||||
|
||||
Raises:
|
||||
RuntimeError: On timeout or non-zero exit.
|
||||
"""
|
||||
from lib.state import _find_acme
|
||||
|
||||
acme_bin = _find_acme()
|
||||
@@ -102,12 +133,14 @@ def _run_acme(args: list[str]) -> str:
|
||||
|
||||
|
||||
def _find_acme_bin() -> str:
|
||||
"""Return the path to the acme.sh binary."""
|
||||
from lib.state import _find_acme
|
||||
|
||||
return _find_acme()
|
||||
|
||||
|
||||
def _get_acme_email() -> str:
|
||||
"""Read registered contact email from ACME account config."""
|
||||
try:
|
||||
acme_home = Path(os.environ.get("ACME_HOME", str(_ACME_HOME)))
|
||||
account_conf = acme_home / "account.conf"
|
||||
@@ -122,12 +155,14 @@ def _get_acme_email() -> str:
|
||||
|
||||
|
||||
def _get_state() -> dict[str, Any] | None:
|
||||
"""Return the raw ACME entry from the shared state store."""
|
||||
from lib.state import state as state_store
|
||||
|
||||
return state_store.get("acme")
|
||||
|
||||
|
||||
def _get_acme_state() -> dict[str, Any]:
|
||||
"""Return the ACME state or empty dict when missing."""
|
||||
ac = _get_state()
|
||||
if ac is None:
|
||||
return {}
|
||||
@@ -213,6 +248,7 @@ def _check_dns_resolves(domain: str) -> tuple[bool, str]:
|
||||
|
||||
|
||||
def _check_acme_installed() -> tuple[bool, str]:
|
||||
"""Verify acme.sh binary is installed and executable."""
|
||||
try:
|
||||
_find_acme_bin()
|
||||
return True, "acme.sh found"
|
||||
@@ -221,6 +257,7 @@ def _check_acme_installed() -> tuple[bool, str]:
|
||||
|
||||
|
||||
def _check_email_configured() -> tuple[bool, str]:
|
||||
"""Check whether an ACME contact email has been configured."""
|
||||
email = _get_acme_email() or ""
|
||||
if email:
|
||||
return True, f"Contact email configured: {email}"
|
||||
@@ -228,12 +265,14 @@ def _check_email_configured() -> tuple[bool, str]:
|
||||
|
||||
|
||||
def _check_webroot() -> tuple[bool, str]:
|
||||
"""Verify the ACME webroot directory exists and is writable."""
|
||||
if _WEBROOT.is_dir() and os.access(str(_WEBROOT), os.W_OK):
|
||||
return True, "ACME webroot ready"
|
||||
return False, "ACME webroot not ready or not writable"
|
||||
|
||||
|
||||
def _check_challenge_config() -> tuple[bool, str]:
|
||||
"""Check for the ACME HTTP-01 challenge nginx config file."""
|
||||
from lib.nginx import SITES_DIR
|
||||
|
||||
site_conf = SITES_DIR / "_acme-challenge.conf" if SITES_DIR else None
|
||||
@@ -298,12 +337,19 @@ def _validate(domain: str) -> dict[str, Any]:
|
||||
|
||||
@registry.register("GET", "/acme/list")
|
||||
def list_certs(_request: Any, _body: Any) -> list[dict]:
|
||||
"""GET /acme/list — return managed certificates."""
|
||||
ac = _get_acme_state()
|
||||
return ac.get("certs", [])
|
||||
|
||||
|
||||
@registry.register("GET", "/acme/info")
|
||||
def get_cert_info(_request: Any, body: dict[str, Any] | None) -> dict:
|
||||
"""GET /acme/info — return details for a single domain certificate.
|
||||
|
||||
Raises:
|
||||
ValueError: When domain is missing.
|
||||
NotFoundError: When no certificate exists for domain.
|
||||
"""
|
||||
if not body or "domain" not in body:
|
||||
raise ValueError("'domain' is required")
|
||||
domain = body["domain"]
|
||||
@@ -316,6 +362,11 @@ def get_cert_info(_request: Any, body: dict[str, Any] | None) -> dict:
|
||||
|
||||
@registry.register("POST", "/acme/validate")
|
||||
def validate_cert(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""POST /acme/validate — run pre-flight checks for a domain.
|
||||
|
||||
Raises:
|
||||
ValueError: When domain is missing.
|
||||
"""
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
domain = body.get("domain", "").strip()
|
||||
@@ -326,6 +377,14 @@ def validate_cert(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
||||
@registry.register("POST", "/acme/issue")
|
||||
async def issue_cert(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""POST /acme/issue — create a new certificate issuance request.
|
||||
|
||||
Deduplicates in-progress requests. Spawns background task for actual issuance.
|
||||
|
||||
Raises:
|
||||
ValueError: When domain is missing.
|
||||
RuntimeError: When pre-flight checks fail.
|
||||
"""
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
domain = body.get("domain", "").strip()
|
||||
@@ -381,6 +440,12 @@ async def issue_cert(_request: Any, body: dict[str, Any] | None) -> dict[str, An
|
||||
|
||||
@registry.register("GET", "/acme/issue/status")
|
||||
def get_issuance_status(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""GET /acme/issue/status — poll status of an issuance request.
|
||||
|
||||
Raises:
|
||||
ValueError: When id is missing.
|
||||
NotFoundError: When request_id is unknown.
|
||||
"""
|
||||
request_id = (body or {}).get("id", "").strip()
|
||||
if not request_id:
|
||||
raise ValueError("'id' is required")
|
||||
@@ -444,6 +509,14 @@ async def _run_issue(req: IssueRequest) -> None:
|
||||
|
||||
@registry.register("POST", "/acme/renew")
|
||||
def renew_cert(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""POST /acme/renew — renew a certificate for the given domain.
|
||||
|
||||
Args:
|
||||
force: Force renewal regardless of expiry.
|
||||
|
||||
Raises:
|
||||
ValueError: When domain is missing.
|
||||
"""
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
domain = body.get("domain", "").strip()
|
||||
@@ -462,6 +535,11 @@ def renew_cert(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
||||
@registry.register("DELETE", "/acme/remove")
|
||||
def remove_cert(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""DELETE /acme/remove — remove a certificate from ACME management.
|
||||
|
||||
Raises:
|
||||
ValueError: When domain is missing.
|
||||
"""
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
domain = body.get("domain", "").strip()
|
||||
@@ -475,6 +553,11 @@ def remove_cert(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
||||
@registry.register("POST", "/acme/email")
|
||||
def set_email(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""POST /acme/email — set the ACME contact email via account registration.
|
||||
|
||||
Raises:
|
||||
ValueError: When email is missing.
|
||||
"""
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
email = body.get("email", "").strip()
|
||||
@@ -488,6 +571,7 @@ def set_email(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
||||
@registry.register("GET", "/acme/email")
|
||||
def get_email(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
"""GET /acme/email — return the currently configured ACME contact email."""
|
||||
ac = _get_acme_state()
|
||||
if ac:
|
||||
return {"email": ac.get("email", "")}
|
||||
@@ -506,6 +590,11 @@ def get_email(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
|
||||
@registry.register("GET", "/acme/paths")
|
||||
def get_cert_paths(_request: Any, body: dict[str, Any] | None) -> dict[str, str]:
|
||||
"""GET /acme/paths — return filesystem paths for a domain's certificate files.
|
||||
|
||||
Raises:
|
||||
ValueError: When domain is missing.
|
||||
"""
|
||||
if not body or "domain" not in body:
|
||||
raise ValueError("'domain' is required")
|
||||
domain = body["domain"]
|
||||
|
||||
Reference in New Issue
Block a user