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:
@@ -16,6 +16,11 @@ bp = Blueprint("certs", __name__)
|
||||
|
||||
@bp.route("/list", methods=["GET"])
|
||||
def list_certs_bp():
|
||||
"""GET /api/certs/list — list all managed ACME certificates.
|
||||
|
||||
Returns:
|
||||
Response containing the list of certificates or an error message.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/acme/list"))
|
||||
except RuntimeError as exc:
|
||||
@@ -25,6 +30,14 @@ def list_certs_bp():
|
||||
|
||||
@bp.route("/<domain>", methods=["GET"])
|
||||
def cert_details(domain: str):
|
||||
"""GET /api/certs/<domain> — get details for a specific certificate.
|
||||
|
||||
Args:
|
||||
domain: Domain name to look up.
|
||||
|
||||
Returns:
|
||||
Response containing certificate info or an error message.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/acme/info", {"domain": domain}))
|
||||
except NotFound as exc:
|
||||
@@ -37,6 +50,13 @@ def cert_details(domain: str):
|
||||
|
||||
@bp.route("/validate", methods=["POST"])
|
||||
def validate():
|
||||
"""POST /api/certs/validate — run pre-flight checks for certificate issuance.
|
||||
|
||||
Expects JSON body with ``{``domain``}``.
|
||||
|
||||
Returns:
|
||||
Response containing validation results or an error message.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
domain = body.get("domain", "").strip()
|
||||
if not domain:
|
||||
@@ -54,6 +74,13 @@ def validate():
|
||||
|
||||
@bp.route("/issue/start", methods=["POST"])
|
||||
def issue_start():
|
||||
"""POST /api/certs/issue/start — create a new certificate issuance request.
|
||||
|
||||
Expects JSON body with ``{``domain``}``; optional ``email`` and ``webroot``.
|
||||
|
||||
Returns:
|
||||
Response containing an issuance request ID or an error message.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
domain = body.get("domain", "").strip()
|
||||
if not domain:
|
||||
@@ -81,6 +108,14 @@ def issue_start():
|
||||
|
||||
@bp.route("/issue/<request_id>", methods=["GET"])
|
||||
def issue_status(request_id: str):
|
||||
"""GET /api/certs/issue/<request_id> — poll status of a certificate issuance request.
|
||||
|
||||
Args:
|
||||
request_id: Issuance request identifier returned by issue_start.
|
||||
|
||||
Returns:
|
||||
Response containing issuance status or an error message.
|
||||
"""
|
||||
try:
|
||||
result = get("/acme/issue/status", {"id": request_id})
|
||||
return _ok(result)
|
||||
@@ -94,6 +129,14 @@ def issue_status(request_id: str):
|
||||
|
||||
@bp.route("/<domain>/renew", methods=["POST"])
|
||||
def renew_bp(domain: str):
|
||||
"""POST /api/certs/<domain>/renew — renew an existing certificate.
|
||||
|
||||
Args:
|
||||
domain: Domain name whose certificate should be renewed.
|
||||
|
||||
Returns:
|
||||
Response confirming renewal or an error message.
|
||||
"""
|
||||
try:
|
||||
logger.info("Certificate renewal requested for '%s' via API", domain)
|
||||
post("/acme/renew", {"domain": domain})
|
||||
@@ -109,6 +152,14 @@ def renew_bp(domain: str):
|
||||
|
||||
@bp.route("/<domain>", methods=["DELETE"])
|
||||
def remove_bp(domain: str):
|
||||
"""DELETE /api/certs/<domain> — remove a certificate from ACME management.
|
||||
|
||||
Args:
|
||||
domain: Domain name whose certificate should be removed.
|
||||
|
||||
Returns:
|
||||
Response confirming removal or an error message.
|
||||
"""
|
||||
try:
|
||||
delete("/acme/remove", {"domain": domain})
|
||||
logger.info("Certificate removed for '%s' via API", domain)
|
||||
@@ -123,6 +174,13 @@ def remove_bp(domain: str):
|
||||
|
||||
@bp.route("/email", methods=["POST"])
|
||||
def set_email_bp():
|
||||
"""POST /api/certs/email — set the ACME account email address.
|
||||
|
||||
Expects JSON body with ``{``email``}``.
|
||||
|
||||
Returns:
|
||||
Response confirming the email was set or an error message.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
email = body.get("email", "").strip()
|
||||
if not email:
|
||||
|
||||
@@ -21,6 +21,11 @@ bp = Blueprint("dhcp", __name__)
|
||||
|
||||
@bp.route("/config", methods=["GET"])
|
||||
def get_config_bp():
|
||||
"""GET /api/dhcp/config — Retrieve the current dnsmasq configuration.
|
||||
|
||||
Returns:
|
||||
JSON response with the config or an error.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/dnsmasq/config"))
|
||||
except RuntimeError as exc:
|
||||
@@ -30,6 +35,14 @@ def get_config_bp():
|
||||
|
||||
@bp.route("/config", methods=["POST"])
|
||||
def post_config():
|
||||
"""POST /api/dhcp/config — Save a full replacement dnsmasq configuration.
|
||||
|
||||
Args:
|
||||
request: JSON body containing the complete config object.
|
||||
|
||||
Returns:
|
||||
JSON response with success status or an error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not isinstance(body, dict):
|
||||
return _error("Request body must be a JSON object", 400)
|
||||
@@ -46,6 +59,14 @@ def post_config():
|
||||
|
||||
@bp.route("/config", methods=["PATCH"])
|
||||
def patch_config():
|
||||
"""PATCH /api/dhcp/config — Partially update the dnsmasq configuration.
|
||||
|
||||
Args:
|
||||
request: JSON body containing the fields to update.
|
||||
|
||||
Returns:
|
||||
JSON response with success status or an error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not isinstance(body, dict):
|
||||
return _error("Request body must be a JSON object", 400)
|
||||
@@ -62,6 +83,8 @@ def patch_config():
|
||||
|
||||
@bp.route("/apply", methods=["POST"])
|
||||
def apply_bp():
|
||||
"""POST /api/dhcp/apply — Apply the current dnsmasq configuration to the running service."""
|
||||
|
||||
try:
|
||||
post("/dnsmasq/apply")
|
||||
logger.info("dnsmasq config applied via API")
|
||||
@@ -78,6 +101,8 @@ def apply_bp():
|
||||
|
||||
@bp.route("/status", methods=["GET"])
|
||||
def status_bp():
|
||||
"""GET /api/dhcp/status — Retrieve dnsmasq service status."""
|
||||
|
||||
try:
|
||||
return _ok(get("/dnsmasq/status"))
|
||||
except RuntimeError as exc:
|
||||
@@ -92,6 +117,14 @@ def status_bp():
|
||||
|
||||
@bp.route("/ranges", methods=["POST"])
|
||||
def add_range_bp():
|
||||
"""POST /api/dhcp/ranges — Add a DHCP address range for an interface.
|
||||
|
||||
Args:
|
||||
request: JSON body with `interface`, `start`, `end`, and optional `lease_time`.
|
||||
|
||||
Returns:
|
||||
JSON response with success status or an error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
iface = body.get("interface", "").strip() or None
|
||||
start = body.get("start", "").strip()
|
||||
@@ -121,6 +154,14 @@ def add_range_bp():
|
||||
|
||||
@bp.route("/ranges", methods=["DELETE"])
|
||||
def remove_range_bp():
|
||||
"""DELETE /api/dhcp/ranges — Remove a DHCP address range.
|
||||
|
||||
Args:
|
||||
request: JSON body with `interface`, `start`, and `end`.
|
||||
|
||||
Returns:
|
||||
JSON response with success status or an error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
iface = body.get("interface", "").strip() or ""
|
||||
start = body.get("start", "").strip()
|
||||
@@ -148,6 +189,8 @@ def remove_range_bp():
|
||||
|
||||
@bp.route("/leases", methods=["GET"])
|
||||
def leases_bp():
|
||||
"""GET /api/dhcp/leases — Retrieve the current DHCP lease table."""
|
||||
|
||||
try:
|
||||
return _ok(get("/dnsmasq/leases"))
|
||||
except RuntimeError as exc:
|
||||
@@ -162,6 +205,14 @@ def leases_bp():
|
||||
|
||||
@bp.route("/static-lease", methods=["POST"])
|
||||
def add_static_lease_bp():
|
||||
"""POST /api/dhcp/static-lease — Add a static DHCP lease by MAC address.
|
||||
|
||||
Args:
|
||||
request: JSON body with `mac`, `ip`, and optional `hostname`.
|
||||
|
||||
Returns:
|
||||
JSON response with lease details or an error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
mac = body.get("mac", "").strip()
|
||||
ip = body.get("ip", "").strip()
|
||||
@@ -182,6 +233,14 @@ def add_static_lease_bp():
|
||||
|
||||
@bp.route("/static-lease/<mac>", methods=["DELETE"])
|
||||
def remove_static_lease_bp(mac):
|
||||
"""DELETE /api/dhcp/static-lease/<mac> — Remove a static DHCP lease by MAC address.
|
||||
|
||||
Args:
|
||||
mac: MAC address of the static lease to remove.
|
||||
|
||||
Returns:
|
||||
JSON response with success status or an error.
|
||||
"""
|
||||
try:
|
||||
delete("/dnsmasq/static-lease/remove", {"mac": mac})
|
||||
logger.info("Static lease removed via API: %s", mac)
|
||||
@@ -201,6 +260,14 @@ def remove_static_lease_bp(mac):
|
||||
|
||||
@bp.route("/dns-record", methods=["POST"])
|
||||
def add_dns_record_bp():
|
||||
"""POST /api/dhcp/dns-record — Add a DNS record.
|
||||
|
||||
Args:
|
||||
request: JSON body with `name`, `address`, and optional `hostname`.
|
||||
|
||||
Returns:
|
||||
JSON response with record details or an error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
name = body.get("name", "").strip()
|
||||
address = body.get("address", "").strip()
|
||||
@@ -224,6 +291,14 @@ def add_dns_record_bp():
|
||||
|
||||
@bp.route("/dns-record/<name>", methods=["DELETE"])
|
||||
def remove_dns_record_bp(name):
|
||||
"""DELETE /api/dhcp/dns-record/<name> — Remove a DNS record by name.
|
||||
|
||||
Args:
|
||||
name: Name of the DNS record to remove.
|
||||
|
||||
Returns:
|
||||
JSON response with success status or an error.
|
||||
"""
|
||||
try:
|
||||
delete("/dnsmasq/dns-record/remove", {"name": name})
|
||||
logger.info("DNS record removed via API: %s", name)
|
||||
|
||||
@@ -21,6 +21,16 @@ bp = Blueprint("firewall", __name__)
|
||||
|
||||
@bp.route("/config", methods=["GET"])
|
||||
def config_list():
|
||||
"""Retrieve the current firewall declarative configuration.
|
||||
|
||||
Returns JSON containing the full firewall config from the daemon.
|
||||
|
||||
Endpoint:
|
||||
GET /api/firewall/config
|
||||
|
||||
Returns:
|
||||
JSON response with the config data or an error message.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/firewall/config"))
|
||||
except RuntimeError as exc:
|
||||
@@ -30,6 +40,20 @@ def config_list():
|
||||
|
||||
@bp.route("/config", methods=["POST"])
|
||||
def config_save():
|
||||
"""Save a new firewall declarative configuration.
|
||||
|
||||
Validates that the request body contains a ``zones`` dict, forwards
|
||||
to the daemon, and returns the pending state including unmanaged zones.
|
||||
|
||||
Endpoint:
|
||||
POST /api/firewall/config
|
||||
|
||||
Args:
|
||||
body: JSON with ``zones`` dict mapping zone names to zone configs.
|
||||
|
||||
Returns:
|
||||
JSON with ``config_saved`` flag and pending apply information.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if "zones" not in body:
|
||||
return _error("'zones' key is required", 400)
|
||||
@@ -64,6 +88,20 @@ def config_save():
|
||||
|
||||
@bp.route("/config", methods=["PATCH"])
|
||||
def patch_config():
|
||||
"""Partially update the firewall declarative configuration.
|
||||
|
||||
Accepts a JSON body and forwards it as a patch to the daemon config
|
||||
endpoint, returning the updated pending state.
|
||||
|
||||
Endpoint:
|
||||
PATCH /api/firewall/config
|
||||
|
||||
Args:
|
||||
body: JSON object with configuration fields to patch.
|
||||
|
||||
Returns:
|
||||
JSON with ``config_saved`` flag and pending apply information.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not isinstance(body, dict):
|
||||
return _error("Request body must be a JSON object", 400)
|
||||
@@ -95,6 +133,17 @@ def patch_config():
|
||||
|
||||
@bp.route("/config/apply", methods=["POST"])
|
||||
def config_apply_bp():
|
||||
"""Apply any pending firewall configuration changes.
|
||||
|
||||
Triggers the daemon to apply saved declarative config to the live
|
||||
firewalld instance.
|
||||
|
||||
Endpoint:
|
||||
POST /api/firewall/config/apply
|
||||
|
||||
Returns:
|
||||
JSON with ``applied_zones`` list or an error message.
|
||||
"""
|
||||
try:
|
||||
result = post("/firewall/config/apply")
|
||||
logger.info("Firewall config applied: %s", result.get("applied_zones", []))
|
||||
@@ -106,6 +155,17 @@ def config_apply_bp():
|
||||
|
||||
@bp.route("/config/pending", methods=["GET"])
|
||||
def config_pending_bp():
|
||||
"""Check the pending firewall configuration state.
|
||||
|
||||
Returns information about unsaved changes, whether an apply is
|
||||
needed, and any unmanaged zones detected on the system.
|
||||
|
||||
Endpoint:
|
||||
GET /api/firewall/config/pending
|
||||
|
||||
Returns:
|
||||
JSON with pending changes and apply status.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/firewall/config/pending"))
|
||||
except RuntimeError as exc:
|
||||
@@ -120,6 +180,14 @@ def config_pending_bp():
|
||||
|
||||
@bp.route("/zones", methods=["GET"])
|
||||
def list_zones():
|
||||
"""List all active and available firewall zones.
|
||||
|
||||
Endpoint:
|
||||
GET /api/firewall/zones
|
||||
|
||||
Returns:
|
||||
JSON with ``active`` zones dict and ``available`` zones list.
|
||||
"""
|
||||
try:
|
||||
data = get("/firewall/zones")
|
||||
return _ok(
|
||||
@@ -132,6 +200,17 @@ def list_zones():
|
||||
|
||||
@bp.route("/zones/<name>", methods=["GET"])
|
||||
def zone_details(name: str):
|
||||
"""Retrieve details for a specific firewall zone.
|
||||
|
||||
Endpoint:
|
||||
GET /api/firewall/zones/<name>
|
||||
|
||||
Args:
|
||||
name: Name of the zone to look up.
|
||||
|
||||
Returns:
|
||||
JSON with zone configuration details or 404 error.
|
||||
"""
|
||||
try:
|
||||
info = get("/firewall/zones/info", {"zone": name})
|
||||
return _ok(info)
|
||||
@@ -145,6 +224,17 @@ def zone_details(name: str):
|
||||
|
||||
@bp.route("/zones", methods=["POST"])
|
||||
def create_zone_bp():
|
||||
"""Create a new firewall zone.
|
||||
|
||||
Endpoint:
|
||||
POST /api/firewall/zones
|
||||
|
||||
Args:
|
||||
body: JSON with ``name`` (required) and optional ``target`` string.
|
||||
|
||||
Returns:
|
||||
JSON confirmation or error if the zone already exists.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone_name = body.get("name", "").strip()
|
||||
target = body.get("target", "default").strip() or "default"
|
||||
@@ -164,6 +254,17 @@ def create_zone_bp():
|
||||
|
||||
@bp.route("/zones/<name>", methods=["DELETE"])
|
||||
def delete_zone_bp(name: str):
|
||||
"""Delete a firewall zone by name.
|
||||
|
||||
Endpoint:
|
||||
DELETE /api/firewall/zones/<name>
|
||||
|
||||
Args:
|
||||
name: Name of the zone to delete.
|
||||
|
||||
Returns:
|
||||
JSON confirmation or 404 if the zone does not exist.
|
||||
"""
|
||||
try:
|
||||
delete("/firewall/zones/delete", {"zone": name})
|
||||
logger.info("Zone '%s' deleted via API", name)
|
||||
@@ -183,6 +284,20 @@ def delete_zone_bp(name: str):
|
||||
|
||||
@bp.route("/zones/<name>/interfaces", methods=["POST"])
|
||||
def set_zone_interfaces_bp(name: str):
|
||||
"""Set the network interfaces assigned to a firewall zone.
|
||||
|
||||
Replaces all existing interfaces for the zone with the provided list.
|
||||
|
||||
Endpoint:
|
||||
POST /api/firewall/zones/<name>/interfaces
|
||||
|
||||
Args:
|
||||
name: Zone name.
|
||||
body: JSON with ``interfaces`` list of interface names.
|
||||
|
||||
Returns:
|
||||
JSON confirmation with zone and updated interfaces list.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
interfaces = body.get("interfaces", [])
|
||||
if not isinstance(interfaces, list):
|
||||
@@ -209,6 +324,20 @@ def set_zone_interfaces_bp(name: str):
|
||||
|
||||
@bp.route("/zones/<name>/services", methods=["POST"])
|
||||
def set_zone_services_bp(name: str):
|
||||
"""Set the allowed services for a firewall zone.
|
||||
|
||||
Replaces all existing services for the zone with the provided list.
|
||||
|
||||
Endpoint:
|
||||
POST /api/firewall/zones/<name>/services
|
||||
|
||||
Args:
|
||||
name: Zone name.
|
||||
body: JSON with ``services`` list of service names.
|
||||
|
||||
Returns:
|
||||
JSON confirmation with zone and updated services list.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
services = body.get("services", [])
|
||||
if not isinstance(services, list):
|
||||
@@ -234,6 +363,14 @@ def set_zone_services_bp(name: str):
|
||||
|
||||
@bp.route("/services", methods=["GET"])
|
||||
def list_services():
|
||||
"""List all available firewall services.
|
||||
|
||||
Endpoint:
|
||||
GET /api/firewall/services
|
||||
|
||||
Returns:
|
||||
JSON with the list of available service names.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/firewall/services"))
|
||||
except RuntimeError as exc:
|
||||
@@ -243,6 +380,14 @@ def list_services():
|
||||
|
||||
@bp.route("/interfaces", methods=["GET"])
|
||||
def list_interfaces():
|
||||
"""List all available network interfaces.
|
||||
|
||||
Endpoint:
|
||||
GET /api/firewall/interfaces
|
||||
|
||||
Returns:
|
||||
JSON with the list of available interface names.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/firewall/interfaces"))
|
||||
except RuntimeError as exc:
|
||||
@@ -257,6 +402,17 @@ def list_interfaces():
|
||||
|
||||
@bp.route("/rich-rules", methods=["POST"])
|
||||
def add_rich_rule_bp():
|
||||
"""Add a rich rule to a firewall zone.
|
||||
|
||||
Endpoint:
|
||||
POST /api/firewall/rich-rules
|
||||
|
||||
Args:
|
||||
body: JSON with ``zone`` (zone name) and ``rule`` (XML rule string).
|
||||
|
||||
Returns:
|
||||
JSON with zone, generated rule ID, and rule string.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone = body.get("zone", "").strip()
|
||||
rule = body.get("rule", "").strip()
|
||||
@@ -276,6 +432,17 @@ def add_rich_rule_bp():
|
||||
|
||||
@bp.route("/rich-rules/<zone>", methods=["GET"])
|
||||
def list_rich_rules(zone: str):
|
||||
"""List rich rules for a specific firewall zone.
|
||||
|
||||
Endpoint:
|
||||
GET /api/firewall/rich-rules/<zone>
|
||||
|
||||
Args:
|
||||
zone: Zone name to list rules for.
|
||||
|
||||
Returns:
|
||||
JSON with list of rich rule entries for the zone.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/firewall/rich-rules", {"zone": zone}))
|
||||
except RuntimeError as exc:
|
||||
@@ -285,6 +452,18 @@ def list_rich_rules(zone: str):
|
||||
|
||||
@bp.route("/rich-rules/<zone>/<rule_id>", methods=["DELETE"])
|
||||
def remove_rich_rule_bp(zone: str, rule_id: str):
|
||||
"""Remove a rich rule from a firewall zone by ID.
|
||||
|
||||
Endpoint:
|
||||
DELETE /api/firewall/rich-rules/<zone>/<rule_id>
|
||||
|
||||
Args:
|
||||
zone: Zone name.
|
||||
rule_id: Rule identifier.
|
||||
|
||||
Returns:
|
||||
JSON confirmation or 404 if the rule does not exist.
|
||||
"""
|
||||
try:
|
||||
delete("/firewall/rich-rules/remove", {"zone": zone, "id": rule_id})
|
||||
logger.info("Rich rule '%s' removed from zone '%s'", rule_id, zone)
|
||||
@@ -304,6 +483,17 @@ def remove_rich_rule_bp(zone: str, rule_id: str):
|
||||
|
||||
@bp.route("/masquerade", methods=["POST"])
|
||||
def set_masquerade_bp():
|
||||
"""Enable or disable masquerade (NAT) on a firewall zone.
|
||||
|
||||
Endpoint:
|
||||
POST /api/firewall/masquerade
|
||||
|
||||
Args:
|
||||
body: JSON with ``zone`` (zone name) and ``enable`` (boolean).
|
||||
|
||||
Returns:
|
||||
JSON confirmation with zone and masquerade status.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone = body.get("zone", "").strip()
|
||||
enable = body.get("enable")
|
||||
@@ -332,6 +522,18 @@ def set_masquerade_bp():
|
||||
|
||||
@bp.route("/forward-port", methods=["POST"])
|
||||
def add_forward_port_bp():
|
||||
"""Add a port forwarding rule to a firewall zone.
|
||||
|
||||
Endpoint:
|
||||
POST /api/firewall/forward-port
|
||||
|
||||
Args:
|
||||
body: JSON with ``zone`` (zone name), ``port`` (int), ``proto``
|
||||
(tcp/udp), optional ``toaddr`` and ``toport``.
|
||||
|
||||
Returns:
|
||||
JSON confirmation with zone, generated ID, port, and protocol.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone = body.get("zone", "").strip()
|
||||
port = body.get("port")
|
||||
@@ -373,6 +575,19 @@ def add_forward_port_bp():
|
||||
|
||||
@bp.route("/forward-port/<zone>/<int:port>/<proto>", methods=["DELETE"])
|
||||
def remove_forward_port_bp(zone: str, port: int, proto: str):
|
||||
"""Remove a port forwarding rule from a firewall zone.
|
||||
|
||||
Endpoint:
|
||||
DELETE /api/firewall/forward-port/<zone>/<port>/<proto>
|
||||
|
||||
Args:
|
||||
zone: Zone name.
|
||||
port: Port number.
|
||||
proto: Protocol (tcp/udp).
|
||||
|
||||
Returns:
|
||||
JSON confirmation or 404 if the rule does not exist.
|
||||
"""
|
||||
try:
|
||||
delete(
|
||||
"/firewall/forward-port/remove",
|
||||
|
||||
@@ -19,12 +19,28 @@ _LOG_LINE_TEMPLATE = """\
|
||||
|
||||
|
||||
def _render_log_lines(text: str) -> str:
|
||||
"""Render raw log text into styled HTML log-line divs.
|
||||
|
||||
Args:
|
||||
text: Raw log content with newline-separated lines.
|
||||
|
||||
Returns:
|
||||
HTML string with color-coded log-line elements.
|
||||
"""
|
||||
lines = text.rstrip("\n").split("\n") if text.strip() else []
|
||||
return render_template_string(_LOG_LINE_TEMPLATE, lines=lines)
|
||||
|
||||
|
||||
@bp.route("/journal")
|
||||
def journal():
|
||||
"""GET /api/logs/journal — Return systemd journal log lines.
|
||||
|
||||
Fetches the daemon's journal log content via vacuum-walld and
|
||||
renders it as styled HTML log-line elements.
|
||||
|
||||
Returns:
|
||||
HTML string containing rendered journal log lines.
|
||||
"""
|
||||
try:
|
||||
text = get("/logs/journal")
|
||||
return _render_log_lines(text)
|
||||
@@ -34,6 +50,14 @@ def journal():
|
||||
|
||||
@bp.route("/nginx/access")
|
||||
def nginx_access():
|
||||
"""GET /api/logs/nginx/access — Return nginx access log lines.
|
||||
|
||||
Fetches the nginx access log content via vacuum-walld and
|
||||
renders it as styled HTML log-line elements.
|
||||
|
||||
Returns:
|
||||
HTML string containing rendered access log lines.
|
||||
"""
|
||||
try:
|
||||
text = get("/logs/nginx/access")
|
||||
return _render_log_lines(text)
|
||||
@@ -43,6 +67,14 @@ def nginx_access():
|
||||
|
||||
@bp.route("/nginx/error")
|
||||
def nginx_error():
|
||||
"""GET /api/logs/nginx/error — Return nginx error log lines.
|
||||
|
||||
Fetches the nginx error log content via vacuum-walld and
|
||||
renders it as styled HTML log-line elements.
|
||||
|
||||
Returns:
|
||||
HTML string containing rendered error log lines.
|
||||
"""
|
||||
try:
|
||||
text = get("/logs/nginx/error")
|
||||
return _render_log_lines(text)
|
||||
@@ -52,6 +84,14 @@ def nginx_error():
|
||||
|
||||
@bp.route("/dnsmasq")
|
||||
def dnsmasq():
|
||||
"""GET /api/logs/dnsmasq — Return dnsmasq log lines.
|
||||
|
||||
Fetches the dnsmasq log content via vacuum-walld and
|
||||
renders it as styled HTML log-line elements.
|
||||
|
||||
Returns:
|
||||
HTML string containing rendered dnsmasq log lines.
|
||||
"""
|
||||
try:
|
||||
text = get("/logs/dnsmasq")
|
||||
return _render_log_lines(text)
|
||||
@@ -61,6 +101,14 @@ def dnsmasq():
|
||||
|
||||
@bp.route("/app")
|
||||
def app_log():
|
||||
"""GET /api/logs/app — Return application log lines.
|
||||
|
||||
Fetches the application log content via vacuum-walld and
|
||||
renders it as styled HTML log-line elements.
|
||||
|
||||
Returns:
|
||||
HTML string containing rendered application log lines.
|
||||
"""
|
||||
try:
|
||||
text = get("/logs/app")
|
||||
return _render_log_lines(text)
|
||||
|
||||
@@ -16,6 +16,16 @@ bp = Blueprint("proxy", __name__)
|
||||
|
||||
@bp.route("/ssl-apply", methods=["POST"])
|
||||
def ssl_apply_bp():
|
||||
"""Apply SSL snippet config.
|
||||
|
||||
POST /api/proxy/ssl-apply
|
||||
|
||||
Returns:
|
||||
``{"ok": true}`` on success.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If nginx SSL snippet write fails.
|
||||
"""
|
||||
try:
|
||||
post("/nginx/ssl-apply")
|
||||
logger.info("SSL snippet written via API")
|
||||
@@ -27,6 +37,13 @@ def ssl_apply_bp():
|
||||
|
||||
@bp.route("/config", methods=["GET"])
|
||||
def get_config_bp():
|
||||
"""Get the current nginx proxy configuration.
|
||||
|
||||
GET /api/proxy/config
|
||||
|
||||
Returns:
|
||||
Current config dict from the daemon.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/nginx/config"))
|
||||
except RuntimeError as exc:
|
||||
@@ -36,6 +53,16 @@ def get_config_bp():
|
||||
|
||||
@bp.route("/config", methods=["POST"])
|
||||
def post_config():
|
||||
"""Save the nginx proxy configuration.
|
||||
|
||||
POST /api/proxy/config
|
||||
|
||||
Body:
|
||||
Any JSON object to merge into the config.
|
||||
|
||||
Returns:
|
||||
``{"ok": true}`` on success.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not isinstance(body, dict):
|
||||
return _error("Request body must be a JSON object", 400)
|
||||
@@ -53,6 +80,16 @@ def post_config():
|
||||
|
||||
@bp.route("/config", methods=["PATCH"])
|
||||
def patch_config():
|
||||
"""Partially update the nginx proxy configuration.
|
||||
|
||||
PATCH /api/proxy/config
|
||||
|
||||
Body:
|
||||
JSON object with fields to patch.
|
||||
|
||||
Returns:
|
||||
``{"ok": true}`` on success.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not isinstance(body, dict):
|
||||
return _error("Request body must be a JSON object", 400)
|
||||
@@ -70,6 +107,13 @@ def patch_config():
|
||||
|
||||
@bp.route("/domains", methods=["GET"])
|
||||
def list_domains():
|
||||
"""List all configured proxy domains.
|
||||
|
||||
GET /api/proxy/domains
|
||||
|
||||
Returns:
|
||||
List of domain dicts from the daemon.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/nginx/domains"))
|
||||
except RuntimeError as exc:
|
||||
@@ -79,6 +123,21 @@ def list_domains():
|
||||
|
||||
@bp.route("/domains", methods=["POST"])
|
||||
def add_domain_bp():
|
||||
"""Add a new proxy domain.
|
||||
|
||||
POST /api/proxy/domains
|
||||
|
||||
Body fields:
|
||||
domain: Domain name.
|
||||
backend_host: Upstream host.
|
||||
backend_port: Upstream port.
|
||||
backend_proto: Protocol (``http`` or ``https``, default ``http``).
|
||||
cert: Optional certificate type.
|
||||
extra_headers: Optional extra headers dict.
|
||||
|
||||
Returns:
|
||||
``{"domain": ...}`` on success.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
domain = body.get("domain", "").strip()
|
||||
backend_host = body.get("backend_host", "").strip()
|
||||
@@ -116,6 +175,16 @@ def add_domain_bp():
|
||||
|
||||
@bp.route("/domains/<domain>", methods=["PUT"])
|
||||
def update_domain_bp(domain):
|
||||
"""Update an existing proxy domain in-place.
|
||||
|
||||
PUT /api/proxy/domains/<domain>
|
||||
|
||||
Body fields:
|
||||
Fields to merge into the domain config.
|
||||
|
||||
Returns:
|
||||
``{"domain": ...}`` on success.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not body:
|
||||
return _error("Request body must be a JSON object with fields to update", 400)
|
||||
@@ -136,6 +205,13 @@ def update_domain_bp(domain):
|
||||
|
||||
@bp.route("/domains/<domain>", methods=["DELETE"])
|
||||
def remove_domain_bp(domain):
|
||||
"""Remove a proxy domain.
|
||||
|
||||
DELETE /api/proxy/domains/<domain>
|
||||
|
||||
Returns:
|
||||
``{"domain": ...}`` on success.
|
||||
"""
|
||||
try:
|
||||
delete("/nginx/domains/remove", {"domain": domain})
|
||||
logger.info("Proxy domain removed via API: %s", domain)
|
||||
@@ -150,6 +226,13 @@ def remove_domain_bp(domain):
|
||||
|
||||
@bp.route("/apply", methods=["POST"])
|
||||
def apply_bp():
|
||||
"""Generate all nginx configs and reload nginx.
|
||||
|
||||
POST /api/proxy/apply
|
||||
|
||||
Returns:
|
||||
``{"ok": true}`` on success.
|
||||
"""
|
||||
try:
|
||||
post("/nginx/apply")
|
||||
logger.info("nginx config applied via API")
|
||||
@@ -161,6 +244,13 @@ def apply_bp():
|
||||
|
||||
@bp.route("/test", methods=["POST"])
|
||||
def test_bp():
|
||||
"""Test nginx configuration without reloading.
|
||||
|
||||
POST /api/proxy/test
|
||||
|
||||
Returns:
|
||||
``{"valid": true, "output": ...}`` on success. Returns 400 if test fails.
|
||||
"""
|
||||
try:
|
||||
result = post("/nginx/test")
|
||||
if result.get("valid"):
|
||||
@@ -173,6 +263,20 @@ def test_bp():
|
||||
|
||||
@bp.route("/management", methods=["POST"])
|
||||
def management_bp():
|
||||
"""Configure the management reverse proxy for the WebUI.
|
||||
|
||||
POST /api/proxy/management
|
||||
|
||||
Body fields:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
``{"ok": true}`` on success.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
domain = body.get("domain", "").strip()
|
||||
if not domain:
|
||||
|
||||
@@ -16,6 +16,14 @@ bp = Blueprint("wireguard", __name__)
|
||||
|
||||
@bp.route("/config", methods=["GET"])
|
||||
def get_config_bp():
|
||||
"""Get the current WireGuard configuration.
|
||||
|
||||
Endpoint: GET /api/wireguard/config
|
||||
|
||||
Returns:
|
||||
JSON response with the WireGuard config on success, or an error
|
||||
response on failure.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/wireguard/config"))
|
||||
except RuntimeError as exc:
|
||||
@@ -25,6 +33,18 @@ def get_config_bp():
|
||||
|
||||
@bp.route("/config", methods=["POST"])
|
||||
def post_config():
|
||||
"""Create or fully replace the WireGuard configuration.
|
||||
|
||||
Endpoint: POST /api/wireguard/config
|
||||
|
||||
Args:
|
||||
body: JSON body with the configuration. If an ``interface`` key
|
||||
is present, the private key will be stripped before forwarding.
|
||||
|
||||
Returns:
|
||||
Success response on acceptance, 400 on validation failure, or 500
|
||||
on server error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not isinstance(body, dict):
|
||||
return _error("Request body must be a JSON object", 400)
|
||||
@@ -45,6 +65,18 @@ def post_config():
|
||||
|
||||
@bp.route("/config", methods=["PATCH"])
|
||||
def patch_config():
|
||||
"""Partially update the WireGuard configuration.
|
||||
|
||||
Endpoint: PATCH /api/wireguard/config
|
||||
|
||||
Args:
|
||||
body: JSON body with the fields to update. If an ``interface``
|
||||
key is present, the private key will be stripped before forwarding.
|
||||
|
||||
Returns:
|
||||
Success response on acceptance, 400 on validation failure, or 500
|
||||
on server error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not isinstance(body, dict):
|
||||
return _error("Request body must be a JSON object", 400)
|
||||
@@ -66,6 +98,13 @@ def patch_config():
|
||||
|
||||
@bp.route("/apply", methods=["POST"])
|
||||
def apply_bp():
|
||||
"""Apply the current WireGuard configuration to the live tunnel.
|
||||
|
||||
Endpoint: POST /api/wireguard/apply
|
||||
|
||||
Returns:
|
||||
Success response on acceptance, or 500 on server error.
|
||||
"""
|
||||
try:
|
||||
post("/wireguard/apply")
|
||||
logger.info("WireGuard tunnel applied via API")
|
||||
@@ -77,6 +116,13 @@ def apply_bp():
|
||||
|
||||
@bp.route("/up", methods=["POST"])
|
||||
def up_bp():
|
||||
"""Bring the WireGuard tunnel interface up.
|
||||
|
||||
Endpoint: POST /api/wireguard/up
|
||||
|
||||
Returns:
|
||||
Success response on acceptance, or 500 on server error.
|
||||
"""
|
||||
try:
|
||||
post("/wireguard/apply")
|
||||
logger.info("WireGuard tunnel started via API")
|
||||
@@ -88,6 +134,13 @@ def up_bp():
|
||||
|
||||
@bp.route("/down", methods=["POST"])
|
||||
def down_bp():
|
||||
"""Bring the WireGuard tunnel interface down.
|
||||
|
||||
Endpoint: POST /api/wireguard/down
|
||||
|
||||
Returns:
|
||||
Success response on acceptance, or 500 on server error.
|
||||
"""
|
||||
try:
|
||||
post("/wireguard/down")
|
||||
logger.info("WireGuard tunnel brought down via API")
|
||||
@@ -99,6 +152,14 @@ def down_bp():
|
||||
|
||||
@bp.route("/status", methods=["GET"])
|
||||
def status_bp():
|
||||
"""Get the current WireGuard tunnel status.
|
||||
|
||||
Endpoint: GET /api/wireguard/status
|
||||
|
||||
Returns:
|
||||
JSON response with the tunnel status on success, or an error
|
||||
response on failure.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/wireguard/status"))
|
||||
except RuntimeError as exc:
|
||||
@@ -108,6 +169,13 @@ def status_bp():
|
||||
|
||||
@bp.route("/initialize", methods=["POST"])
|
||||
def initialize_bp():
|
||||
"""Initialize WireGuard for first-time use.
|
||||
|
||||
Endpoint: POST /api/wireguard/initialize
|
||||
|
||||
Returns:
|
||||
Success response on acceptance, or 500 on server error.
|
||||
"""
|
||||
try:
|
||||
post("/wireguard/initialize")
|
||||
logger.info("WireGuard initialized via API")
|
||||
@@ -119,6 +187,21 @@ def initialize_bp():
|
||||
|
||||
@bp.route("/peers", methods=["POST"])
|
||||
def add_peer_bp():
|
||||
"""Add a new peer to the WireGuard configuration.
|
||||
|
||||
Endpoint: POST /api/wireguard/peers
|
||||
|
||||
Args:
|
||||
name: Peer display name (required).
|
||||
endpoint: Optional peer endpoint address.
|
||||
allowed_ips: Optional list of allowed IP CIDRs.
|
||||
persistent_keepalive: Optional keepalive interval in seconds.
|
||||
preshared_key: Optional pre-shared key in hex.
|
||||
|
||||
Returns:
|
||||
JSON response with the created peer on success, 400 on validation
|
||||
failure, or 500 on server error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
name = body.get("name", "").strip()
|
||||
if not name:
|
||||
@@ -146,6 +229,17 @@ def add_peer_bp():
|
||||
|
||||
@bp.route("/peers/<name>", methods=["DELETE"])
|
||||
def remove_peer_bp(name):
|
||||
"""Remove a peer from the WireGuard configuration.
|
||||
|
||||
Endpoint: DELETE /api/wireguard/peers/<name>
|
||||
|
||||
Args:
|
||||
name: Peer name to remove (from URL path).
|
||||
|
||||
Returns:
|
||||
Success response with peer name on removal, 404 if peer not found,
|
||||
or 500 on server error.
|
||||
"""
|
||||
try:
|
||||
delete("/wireguard/peers/remove", {"name": name})
|
||||
logger.info("WireGuard peer '%s' removed via API", name)
|
||||
@@ -160,6 +254,14 @@ def remove_peer_bp(name):
|
||||
|
||||
@bp.route("/peers", methods=["GET"])
|
||||
def peers_bp():
|
||||
"""List all configured WireGuard peers.
|
||||
|
||||
Endpoint: GET /api/wireguard/peers
|
||||
|
||||
Returns:
|
||||
JSON response with the peers list on success, or an error response
|
||||
on failure.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/wireguard/peers"))
|
||||
except RuntimeError as exc:
|
||||
@@ -169,6 +271,14 @@ def peers_bp():
|
||||
|
||||
@bp.route("/peer-status", methods=["GET"])
|
||||
def peer_status_bp():
|
||||
"""Get real-time status information for all WireGuard peers.
|
||||
|
||||
Endpoint: GET /api/wireguard/peer-status
|
||||
|
||||
Returns:
|
||||
JSON response with peer status on success, or an error response
|
||||
on failure.
|
||||
"""
|
||||
try:
|
||||
return _ok(get("/wireguard/peer-status"))
|
||||
except RuntimeError as exc:
|
||||
@@ -178,6 +288,18 @@ def peer_status_bp():
|
||||
|
||||
@bp.route("/generate-client", methods=["POST"])
|
||||
def generate_client_bp():
|
||||
"""Generate a WireGuard client configuration file for a peer.
|
||||
|
||||
Endpoint: POST /api/wireguard/generate-client
|
||||
|
||||
Args:
|
||||
name: Peer name (required).
|
||||
server_endpoint: Server endpoint address for the client config (required).
|
||||
|
||||
Returns:
|
||||
JSON response with the generated config string on success, 404 if
|
||||
peer not found, 400 on validation failure, or 500 on server error.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
name = body.get("name", "").strip()
|
||||
if not name:
|
||||
|
||||
Reference in New Issue
Block a user