refactor: update all API blueprints (certs, dhcp, firewall, logs, network, proxy, wireguard)

This commit is contained in:
2026-06-16 03:36:51 +00:00
parent 4fc0fb3f72
commit 708b8b5d15
7 changed files with 199 additions and 149 deletions
+18 -8
View File
@@ -8,6 +8,16 @@ import logging
from flask import Blueprint, request from flask import Blueprint, request
from daemon.client import BadRequest, NotFound, delete, get, post from daemon.client import BadRequest, NotFound, delete, get, post
from daemon.iface import (
DELETE_ACME_REMOVE,
GET_ACME_INFO,
GET_ACME_ISSUE_STATUS,
GET_ACME_LIST,
POST_ACME_EMAIL,
POST_ACME_ISSUE,
POST_ACME_RENEW,
POST_ACME_VALIDATE,
)
from webui.api.common import _error, _ok from webui.api.common import _error, _ok
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -22,7 +32,7 @@ def list_certs_bp():
Response containing the list of certificates or an error message. Response containing the list of certificates or an error message.
""" """
try: try:
return _ok(get("/acme/list")) return _ok(get(GET_ACME_LIST))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to list certificates: %s", exc) logger.error("Failed to list certificates: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -39,7 +49,7 @@ def cert_details(domain: str):
Response containing certificate info or an error message. Response containing certificate info or an error message.
""" """
try: try:
return _ok(get("/acme/info", {"domain": domain})) return _ok(get(GET_ACME_INFO, {"domain": domain}))
except NotFound as exc: except NotFound as exc:
logger.info("Cert for '%s' not found: %s", domain, exc) logger.info("Cert for '%s' not found: %s", domain, exc)
return _error(str(exc), 404) return _error(str(exc), 404)
@@ -62,7 +72,7 @@ def validate():
if not domain: if not domain:
return _error("'domain' is required", 400) return _error("'domain' is required", 400)
try: try:
result = post("/acme/validate", {"domain": domain}) result = post(POST_ACME_VALIDATE, {"domain": domain})
return _ok(result) return _ok(result)
except BadRequest as exc: except BadRequest as exc:
logger.info("Validation rejected: %s", exc) logger.info("Validation rejected: %s", exc)
@@ -90,7 +100,7 @@ def issue_start():
try: try:
logger.info("Certificate issuance requested for '%s' via API", domain) logger.info("Certificate issuance requested for '%s' via API", domain)
result = post( result = post(
"/acme/issue", {"domain": domain, "webroot": webroot, "email": email} POST_ACME_ISSUE, {"domain": domain, "webroot": webroot, "email": email}
) )
logger.info( logger.info(
"Certificate issuance started for '%s' (id=%s)", "Certificate issuance started for '%s' (id=%s)",
@@ -117,7 +127,7 @@ def issue_status(request_id: str):
Response containing issuance status or an error message. Response containing issuance status or an error message.
""" """
try: try:
result = get("/acme/issue/status", {"id": request_id}) result = get(GET_ACME_ISSUE_STATUS, {"id": request_id})
return _ok(result) return _ok(result)
except NotFound as exc: except NotFound as exc:
logger.info("Issuance request '%s' not found: %s", request_id, exc) logger.info("Issuance request '%s' not found: %s", request_id, exc)
@@ -139,7 +149,7 @@ def renew_bp(domain: str):
""" """
try: try:
logger.info("Certificate renewal requested for '%s' via API", domain) logger.info("Certificate renewal requested for '%s' via API", domain)
post("/acme/renew", {"domain": domain}) post(POST_ACME_RENEW, {"domain": domain})
logger.info("Certificate renewed for '%s'", domain) logger.info("Certificate renewed for '%s'", domain)
return _ok(None) return _ok(None)
except BadRequest as exc: except BadRequest as exc:
@@ -161,7 +171,7 @@ def remove_bp(domain: str):
Response confirming removal or an error message. Response confirming removal or an error message.
""" """
try: try:
delete("/acme/remove", {"domain": domain}) delete(DELETE_ACME_REMOVE, {"domain": domain})
logger.info("Certificate removed for '%s' via API", domain) logger.info("Certificate removed for '%s' via API", domain)
return _ok(None) return _ok(None)
except NotFound as exc: except NotFound as exc:
@@ -186,7 +196,7 @@ def set_email_bp():
if not email: if not email:
return _error("'email' is required", 400) return _error("'email' is required", 400)
try: try:
post("/acme/email", {"email": email}) post(POST_ACME_EMAIL, {"email": email})
logger.info("ACME email set via API: %s", email) logger.info("ACME email set via API: %s", email)
return _ok({"email": email}) return _ok({"email": email})
except BadRequest as exc: except BadRequest as exc:
+29 -12
View File
@@ -8,6 +8,20 @@ import logging
from flask import Blueprint, request from flask import Blueprint, request
from daemon.client import BadRequest, NotFound, delete, get, patch, post from daemon.client import BadRequest, NotFound, delete, get, patch, post
from daemon.iface import (
DELETE_DNSMASQ_DNS_RECORD_REMOVE,
DELETE_DNSMASQ_RANGES_REMOVE,
DELETE_DNSMASQ_STATIC_LEASE_REMOVE,
GET_DNSMASQ_CONFIG,
GET_DNSMASQ_LEASES,
GET_DNSMASQ_STATUS,
PATCH_DNSMASQ_CONFIG,
POST_DNSMASQ_APPLY,
POST_DNSMASQ_CONFIG,
POST_DNSMASQ_DNS_RECORD_ADD,
POST_DNSMASQ_RANGES_ADD,
POST_DNSMASQ_STATIC_LEASE_ADD,
)
from webui.api.common import _error, _ok from webui.api.common import _error, _ok
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -27,7 +41,7 @@ def get_config_bp():
JSON response with the config or an error. JSON response with the config or an error.
""" """
try: try:
return _ok(get("/dnsmasq/config")) return _ok(get(GET_DNSMASQ_CONFIG))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to read DHCP config: %s", exc) logger.error("Failed to read DHCP config: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -47,7 +61,7 @@ def post_config():
if not isinstance(body, dict): if not isinstance(body, dict):
return _error("Request body must be a JSON object", 400) return _error("Request body must be a JSON object", 400)
try: try:
post("/dnsmasq/config", body) post(POST_DNSMASQ_CONFIG, body)
return _ok(None) return _ok(None)
except BadRequest as exc: except BadRequest as exc:
logger.info("DHCP config save rejected: %s", exc) logger.info("DHCP config save rejected: %s", exc)
@@ -71,7 +85,7 @@ def patch_config():
if not isinstance(body, dict): if not isinstance(body, dict):
return _error("Request body must be a JSON object", 400) return _error("Request body must be a JSON object", 400)
try: try:
patch("/dnsmasq/config", body) patch(PATCH_DNSMASQ_CONFIG, body)
return _ok(None) return _ok(None)
except BadRequest as exc: except BadRequest as exc:
logger.info("DHCP config patch rejected: %s", exc) logger.info("DHCP config patch rejected: %s", exc)
@@ -86,7 +100,7 @@ def apply_bp():
"""POST /api/dhcp/apply — Apply the current dnsmasq configuration to the running service.""" """POST /api/dhcp/apply — Apply the current dnsmasq configuration to the running service."""
try: try:
post("/dnsmasq/apply") post(POST_DNSMASQ_APPLY)
logger.info("dnsmasq config applied via API") logger.info("dnsmasq config applied via API")
return _ok(None) return _ok(None)
except RuntimeError as exc: except RuntimeError as exc:
@@ -104,7 +118,7 @@ def status_bp():
"""GET /api/dhcp/status — Retrieve dnsmasq service status.""" """GET /api/dhcp/status — Retrieve dnsmasq service status."""
try: try:
return _ok(get("/dnsmasq/status")) return _ok(get(GET_DNSMASQ_STATUS))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to get DHCP status: %s", exc) logger.error("Failed to get DHCP status: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -134,7 +148,7 @@ def add_range_bp():
return _error("'start' and 'end' are required", 400) return _error("'start' and 'end' are required", 400)
try: try:
post( post(
"/dnsmasq/ranges/add", POST_DNSMASQ_RANGES_ADD,
{ {
"interface": iface or "", "interface": iface or "",
"start": start, "start": start,
@@ -170,7 +184,8 @@ def remove_range_bp():
return _error("'start' and 'end' are required", 400) return _error("'start' and 'end' are required", 400)
try: try:
delete( delete(
"/dnsmasq/ranges/remove", {"interface": iface, "start": start, "end": end} DELETE_DNSMASQ_RANGES_REMOVE,
{"interface": iface, "start": start, "end": end},
) )
logger.info("DHCP range removed via API: %s-%s", start, end) logger.info("DHCP range removed via API: %s-%s", start, end)
return _ok(None) return _ok(None)
@@ -192,7 +207,7 @@ def leases_bp():
"""GET /api/dhcp/leases — Retrieve the current DHCP lease table.""" """GET /api/dhcp/leases — Retrieve the current DHCP lease table."""
try: try:
return _ok(get("/dnsmasq/leases")) return _ok(get(GET_DNSMASQ_LEASES))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to read lease table: %s", exc) logger.error("Failed to read lease table: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -220,7 +235,9 @@ def add_static_lease_bp():
if not mac or not ip: if not mac or not ip:
return _error("'mac' and 'ip' are required", 400) return _error("'mac' and 'ip' are required", 400)
try: try:
post("/dnsmasq/static-lease/add", {"mac": mac, "ip": ip, "hostname": hostname}) post(
POST_DNSMASQ_STATIC_LEASE_ADD, {"mac": mac, "ip": ip, "hostname": hostname}
)
logger.info("Static lease added via API: %s -> %s", mac, ip) logger.info("Static lease added via API: %s -> %s", mac, ip)
return _ok({"mac": mac, "ip": ip, "hostname": hostname}) return _ok({"mac": mac, "ip": ip, "hostname": hostname})
except BadRequest as exc: except BadRequest as exc:
@@ -242,7 +259,7 @@ def remove_static_lease_bp(mac):
JSON response with success status or an error. JSON response with success status or an error.
""" """
try: try:
delete("/dnsmasq/static-lease/remove", {"mac": mac}) delete(DELETE_DNSMASQ_STATIC_LEASE_REMOVE, {"mac": mac})
logger.info("Static lease removed via API: %s", mac) logger.info("Static lease removed via API: %s", mac)
return _ok(None) return _ok(None)
except NotFound as exc: except NotFound as exc:
@@ -276,7 +293,7 @@ def add_dns_record_bp():
return _error("'name' and 'address' are required", 400) return _error("'name' and 'address' are required", 400)
try: try:
post( post(
"/dnsmasq/dns-record/add", POST_DNSMASQ_DNS_RECORD_ADD,
{"name": name, "address": address, "hostname": hostname}, {"name": name, "address": address, "hostname": hostname},
) )
logger.info("DNS record added via API: %s -> %s", name, address) logger.info("DNS record added via API: %s -> %s", name, address)
@@ -300,7 +317,7 @@ def remove_dns_record_bp(name):
JSON response with success status or an error. JSON response with success status or an error.
""" """
try: try:
delete("/dnsmasq/dns-record/remove", {"name": name}) delete(DELETE_DNSMASQ_DNS_RECORD_REMOVE, {"name": name})
logger.info("DNS record removed via API: %s", name) logger.info("DNS record removed via API: %s", name)
return _ok(None) return _ok(None)
except NotFound as exc: except NotFound as exc:
+42 -21
View File
@@ -8,6 +8,27 @@ import logging
from flask import Blueprint, request from flask import Blueprint, request
from daemon.client import BadRequest, NotFound, delete, get, patch, post from daemon.client import BadRequest, NotFound, delete, get, patch, post
from daemon.iface import (
DELETE_FIREWALL_FORWARD_PORT_REMOVE,
DELETE_FIREWALL_RICH_RULES_REMOVE,
DELETE_FIREWALL_ZONES_DELETE,
GET_FIREWALL_CONFIG,
GET_FIREWALL_CONFIG_PENDING,
GET_FIREWALL_INTERFACES,
GET_FIREWALL_RICH_RULES,
GET_FIREWALL_SERVICES,
GET_FIREWALL_ZONES,
GET_FIREWALL_ZONES_INFO,
PATCH_FIREWALL_CONFIG,
POST_FIREWALL_CONFIG,
POST_FIREWALL_CONFIG_APPLY,
POST_FIREWALL_FORWARD_PORT_ADD,
POST_FIREWALL_MASQUERADE,
POST_FIREWALL_RICH_RULES_ADD,
POST_FIREWALL_ZONES_CREATE,
POST_FIREWALL_ZONES_INTERFACES,
POST_FIREWALL_ZONES_SERVICES,
)
from webui.api.common import _error, _ok from webui.api.common import _error, _ok
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -32,7 +53,7 @@ def config_list():
JSON response with the config data or an error message. JSON response with the config data or an error message.
""" """
try: try:
return _ok(get("/firewall/config")) return _ok(get(GET_FIREWALL_CONFIG))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to read firewall config: %s", exc) logger.error("Failed to read firewall config: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -60,9 +81,9 @@ def config_save():
if not isinstance(body["zones"], dict): if not isinstance(body["zones"], dict):
return _error("'zones' must be a dict", 400) return _error("'zones' must be a dict", 400)
try: try:
post("/firewall/config", body) post(POST_FIREWALL_CONFIG, body)
try: try:
pending = get("/firewall/config/pending") pending = get(GET_FIREWALL_CONFIG_PENDING)
pending_data = { pending_data = {
"pending": pending.get("pending", []), "pending": pending.get("pending", []),
"needs_apply": pending.get("needs_apply", False), "needs_apply": pending.get("needs_apply", False),
@@ -106,9 +127,9 @@ def patch_config():
if not isinstance(body, dict): if not isinstance(body, dict):
return _error("Request body must be a JSON object", 400) return _error("Request body must be a JSON object", 400)
try: try:
patch("/firewall/config", body) patch(PATCH_FIREWALL_CONFIG, body)
try: try:
pending = get("/firewall/config/pending") pending = get(GET_FIREWALL_CONFIG_PENDING)
pending_data = { pending_data = {
"pending": pending.get("pending", []), "pending": pending.get("pending", []),
"needs_apply": pending.get("needs_apply", False), "needs_apply": pending.get("needs_apply", False),
@@ -145,7 +166,7 @@ def config_apply_bp():
JSON with ``applied_zones`` list or an error message. JSON with ``applied_zones`` list or an error message.
""" """
try: try:
result = post("/firewall/config/apply") result = post(POST_FIREWALL_CONFIG_APPLY)
logger.info("Firewall config applied: %s", result.get("applied_zones", [])) logger.info("Firewall config applied: %s", result.get("applied_zones", []))
return _ok(result) return _ok(result)
except RuntimeError as exc: except RuntimeError as exc:
@@ -167,7 +188,7 @@ def config_pending_bp():
JSON with pending changes and apply status. JSON with pending changes and apply status.
""" """
try: try:
return _ok(get("/firewall/config/pending")) return _ok(get(GET_FIREWALL_CONFIG_PENDING))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to check pending config: %s", exc) logger.error("Failed to check pending config: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -189,7 +210,7 @@ def list_zones():
JSON with ``active`` zones dict and ``available`` zones list. JSON with ``active`` zones dict and ``available`` zones list.
""" """
try: try:
data = get("/firewall/zones") data = get(GET_FIREWALL_ZONES)
return _ok( return _ok(
{"active": data.get("active", {}), "available": data.get("available", [])} {"active": data.get("active", {}), "available": data.get("available", [])}
) )
@@ -212,7 +233,7 @@ def zone_details(name: str):
JSON with zone configuration details or 404 error. JSON with zone configuration details or 404 error.
""" """
try: try:
info = get("/firewall/zones/info", {"zone": name}) info = get(GET_FIREWALL_ZONES_INFO, {"zone": name})
return _ok(info) return _ok(info)
except NotFound as exc: except NotFound as exc:
logger.info("Zone '%s' not found: %s", name, exc) logger.info("Zone '%s' not found: %s", name, exc)
@@ -241,7 +262,7 @@ def create_zone_bp():
if not zone_name: if not zone_name:
return _error("Zone name is required", 400) return _error("Zone name is required", 400)
try: try:
post("/firewall/zones/create", {"name": zone_name, "target": target}) post(POST_FIREWALL_ZONES_CREATE, {"name": zone_name, "target": target})
logger.info("Zone '%s' created via API", zone_name) logger.info("Zone '%s' created via API", zone_name)
return _ok(None) return _ok(None)
except BadRequest as exc: except BadRequest as exc:
@@ -266,7 +287,7 @@ def delete_zone_bp(name: str):
JSON confirmation or 404 if the zone does not exist. JSON confirmation or 404 if the zone does not exist.
""" """
try: try:
delete("/firewall/zones/delete", {"zone": name}) delete(DELETE_FIREWALL_ZONES_DELETE, {"zone": name})
logger.info("Zone '%s' deleted via API", name) logger.info("Zone '%s' deleted via API", name)
return _ok(None) return _ok(None)
except NotFound as exc: except NotFound as exc:
@@ -303,7 +324,7 @@ def set_zone_interfaces_bp(name: str):
if not isinstance(interfaces, list): if not isinstance(interfaces, list):
return _error("'interfaces' must be a list", 400) return _error("'interfaces' must be a list", 400)
try: try:
post("/firewall/zones/interfaces", {"zone": name, "interfaces": interfaces}) post(POST_FIREWALL_ZONES_INTERFACES, {"zone": name, "interfaces": interfaces})
logger.info("Zone '%s' interfaces updated: %s", name, interfaces) logger.info("Zone '%s' interfaces updated: %s", name, interfaces)
return _ok({"zone": name, "interfaces": interfaces}) return _ok({"zone": name, "interfaces": interfaces})
except BadRequest as exc: except BadRequest as exc:
@@ -343,7 +364,7 @@ def set_zone_services_bp(name: str):
if not isinstance(services, list): if not isinstance(services, list):
return _error("'services' must be a list", 400) return _error("'services' must be a list", 400)
try: try:
post("/firewall/zones/services", {"zone": name, "services": services}) post(POST_FIREWALL_ZONES_SERVICES, {"zone": name, "services": services})
return _ok({"zone": name, "services": services}) return _ok({"zone": name, "services": services})
except BadRequest as exc: except BadRequest as exc:
logger.info("Set services for zone '%s' rejected: %s", name, exc) logger.info("Set services for zone '%s' rejected: %s", name, exc)
@@ -372,7 +393,7 @@ def list_services():
JSON with the list of available service names. JSON with the list of available service names.
""" """
try: try:
return _ok(get("/firewall/services")) return _ok(get(GET_FIREWALL_SERVICES))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to list services: %s", exc) logger.error("Failed to list services: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -389,7 +410,7 @@ def list_interfaces():
JSON with the list of available interface names. JSON with the list of available interface names.
""" """
try: try:
return _ok(get("/firewall/interfaces")) return _ok(get(GET_FIREWALL_INTERFACES))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to list interfaces: %s", exc) logger.error("Failed to list interfaces: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -419,7 +440,7 @@ def add_rich_rule_bp():
if not zone or not rule: if not zone or not rule:
return _error("Both 'zone' and 'rule' are required", 400) return _error("Both 'zone' and 'rule' are required", 400)
try: try:
entry = post("/firewall/rich-rules/add", {"zone": zone, "rule": rule}) entry = post(POST_FIREWALL_RICH_RULES_ADD, {"zone": zone, "rule": rule})
logger.info("Rich rule added to zone '%s': %s", zone, rule[:80]) logger.info("Rich rule added to zone '%s': %s", zone, rule[:80])
return _ok({"zone": zone, "id": entry["id"], "rule": rule}) return _ok({"zone": zone, "id": entry["id"], "rule": rule})
except BadRequest as exc: except BadRequest as exc:
@@ -444,7 +465,7 @@ def list_rich_rules(zone: str):
JSON with list of rich rule entries for the zone. JSON with list of rich rule entries for the zone.
""" """
try: try:
return _ok(get("/firewall/rich-rules", {"zone": zone})) return _ok(get(GET_FIREWALL_RICH_RULES, {"zone": zone}))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to get rich rules for zone '%s': %s", zone, exc) logger.error("Failed to get rich rules for zone '%s': %s", zone, exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -465,7 +486,7 @@ def remove_rich_rule_bp(zone: str, rule_id: str):
JSON confirmation or 404 if the rule does not exist. JSON confirmation or 404 if the rule does not exist.
""" """
try: try:
delete("/firewall/rich-rules/remove", {"zone": zone, "id": rule_id}) delete(DELETE_FIREWALL_RICH_RULES_REMOVE, {"zone": zone, "id": rule_id})
logger.info("Rich rule '%s' removed from zone '%s'", rule_id, zone) logger.info("Rich rule '%s' removed from zone '%s'", rule_id, zone)
return _ok({"zone": zone, "id": rule_id}) return _ok({"zone": zone, "id": rule_id})
except NotFound as exc: except NotFound as exc:
@@ -500,7 +521,7 @@ def set_masquerade_bp():
if not zone or enable is None: if not zone or enable is None:
return _error("'zone' and 'enable' (bool) are required", 400) return _error("'zone' and 'enable' (bool) are required", 400)
try: try:
post("/firewall/masquerade", {"zone": zone, "enable": bool(enable)}) post(POST_FIREWALL_MASQUERADE, {"zone": zone, "enable": bool(enable)})
logger.info( logger.info(
"Masquerade %s on zone '%s' via API", "Masquerade %s on zone '%s' via API",
"enabled" if enable else "disabled", "enabled" if enable else "disabled",
@@ -555,7 +576,7 @@ def add_forward_port_bp():
toaddr_str = str(toaddr) if toaddr else None toaddr_str = str(toaddr) if toaddr else None
try: try:
entry = post( entry = post(
"/firewall/forward-port/add", POST_FIREWALL_FORWARD_PORT_ADD,
{ {
"zone": zone, "zone": zone,
"port": port_int, "port": port_int,
@@ -590,7 +611,7 @@ def remove_forward_port_bp(zone: str, port: int, proto: str):
""" """
try: try:
delete( delete(
"/firewall/forward-port/remove", DELETE_FIREWALL_FORWARD_PORT_REMOVE,
{"zone": zone, "port": port, "proto": proto}, {"zone": zone, "port": port, "proto": proto},
) )
logger.info("Forward port %s/%s removed from zone '%s'", port, proto, zone) logger.info("Forward port %s/%s removed from zone '%s'", port, proto, zone)
+32 -76
View File
@@ -1,116 +1,72 @@
"""Log viewing API blueprint. """Log viewing API blueprint.
Serves log content to the /logs page via HTMX endpoints through vacuum-walld. Wraps raw log text in the standard JSON response contract.
""" """
import logging import logging
from flask import Blueprint, render_template_string from flask import Blueprint
from daemon.client import get from daemon.client import NotFound, get
from daemon.iface import (
GET_LOGS_APP,
GET_LOGS_DNSMASQ,
GET_LOGS_JOURNAL,
GET_LOGS_NGINX_ACCESS,
GET_LOGS_NGINX_ERROR,
)
from webui.api.common import _error, _ok
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
bp = Blueprint("logs", __name__) bp = Blueprint("logs", __name__)
_LOG_LINE_TEMPLATE = """\
{% for line in lines %}
<div class="log-line{% if 'ERROR' in line %} log-error{% elif 'WARN' in line %} log-warn{% endif %}">{{ line | e }}</div>
{% endfor %}"""
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") @bp.route("/journal")
def journal(): def journal():
"""GET /api/logs/journal — Return systemd journal log lines. """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: try:
text = get("/logs/journal") return _ok(get(GET_LOGS_JOURNAL))
return _render_log_lines(text)
except RuntimeError: except RuntimeError:
return _render_log_lines("(error reading journal)\n") return _error("error reading journal", 500)
@bp.route("/nginx/access") @bp.route("/nginx/access")
def nginx_access(): def nginx_access():
"""GET /api/logs/nginx/access — Return nginx access log lines. """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: try:
text = get("/logs/nginx/access") return _ok(get(GET_LOGS_NGINX_ACCESS))
return _render_log_lines(text) except NotFound:
return _error("log file not found", 404)
except RuntimeError: except RuntimeError:
return _render_log_lines("(log file not found)\n") return _error("error reading log", 500)
@bp.route("/nginx/error") @bp.route("/nginx/error")
def nginx_error(): def nginx_error():
"""GET /api/logs/nginx/error — Return nginx error log lines. """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: try:
text = get("/logs/nginx/error") return _ok(get(GET_LOGS_NGINX_ERROR))
return _render_log_lines(text) except NotFound:
return _error("log file not found", 404)
except RuntimeError: except RuntimeError:
return _render_log_lines("(log file not found)\n") return _error("error reading log", 500)
@bp.route("/dnsmasq") @bp.route("/dnsmasq")
def dnsmasq(): def dnsmasq():
"""GET /api/logs/dnsmasq — Return dnsmasq log lines. """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: try:
text = get("/logs/dnsmasq") return _ok(get(GET_LOGS_DNSMASQ))
return _render_log_lines(text)
except RuntimeError: except RuntimeError:
return _render_log_lines("(error reading journal)\n") return _error("error reading journal", 500)
@bp.route("/app") @bp.route("/app")
def app_log(): def app_log():
"""GET /api/logs/app — Return application log lines. """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: try:
text = get("/logs/app") return _ok(get(GET_LOGS_APP))
return _render_log_lines(text) except NotFound:
return _error("log file not found", 404)
except RuntimeError: except RuntimeError:
return _render_log_lines("(log file not found)\n") return _error("error reading log", 500)
+27 -8
View File
@@ -9,6 +9,16 @@ import logging
from flask import Blueprint, request from flask import Blueprint, request
from daemon.client import NotFound, get, post from daemon.client import NotFound, get, post
from daemon.iface import (
GET_NETWORK_INFER_DHCP_RANGES,
GET_NETWORK_INFER_ZONES,
GET_NETWORK_INTERFACE_NAME,
GET_NETWORK_INTERFACES,
POST_NETWORK_APPLY,
POST_NETWORK_INTERFACE_NAME,
POST_NETWORK_INTERFACE_RELOAD,
)
from lib.common import validate_interface_name
from webui.api.common import _error, _ok from webui.api.common import _error, _ok
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -26,7 +36,7 @@ def list_interfaces():
JSON with interface config + runtime state. JSON with interface config + runtime state.
""" """
try: try:
return _ok(get("/network/interfaces")) return _ok(get(GET_NETWORK_INTERFACES))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to list network interfaces: %s", exc) logger.error("Failed to list network interfaces: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -43,7 +53,10 @@ def get_interface(name: str):
JSON with interface config and runtime state. JSON with interface config and runtime state.
""" """
try: try:
return _ok(get("/network/interfaces/" + name, {"name": name})) validate_interface_name(name)
return _ok(get(GET_NETWORK_INTERFACE_NAME, {"name": name}))
except ValueError as exc:
return _error(str(exc), 400)
except NotFound as exc: except NotFound as exc:
logger.info("Interface '%s' not found: %s", name, exc) logger.info("Interface '%s' not found: %s", name, exc)
return _error(str(exc), 404) return _error(str(exc), 404)
@@ -65,11 +78,14 @@ def save_interface(name: str):
Returns: Returns:
JSON confirmation. JSON confirmation.
""" """
body = request.get_json(silent=True) or {} body = {**(request.get_json(silent=True) or {}), "name": name}
try: try:
post("/network/interfaces/" + name, body) validate_interface_name(name)
post(POST_NETWORK_INTERFACE_NAME, body)
logger.info("Interface '%s' config saved", name) logger.info("Interface '%s' config saved", name)
return _ok({"name": name, "applied": True}) return _ok({"name": name, "applied": True})
except ValueError as exc:
return _error(str(exc), 400)
except NotFound as exc: except NotFound as exc:
return _error(str(exc), 404) return _error(str(exc), 404)
except RuntimeError as exc: except RuntimeError as exc:
@@ -88,9 +104,12 @@ def reload_interface(name: str):
JSON confirmation. JSON confirmation.
""" """
try: try:
post("/network/interfaces/" + name + "/reload", {"name": name}) validate_interface_name(name)
post(POST_NETWORK_INTERFACE_RELOAD, {"name": name})
logger.info("Interface '%s' reloaded", name) logger.info("Interface '%s' reloaded", name)
return _ok({"name": name, "reloaded": True}) return _ok({"name": name, "reloaded": True})
except ValueError as exc:
return _error(str(exc), 400)
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to reload interface '%s': %s", name, exc) logger.error("Failed to reload interface '%s': %s", name, exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -107,7 +126,7 @@ def apply_all():
JSON with number of interfaces applied. JSON with number of interfaces applied.
""" """
try: try:
result = post("/network/apply", {}) result = post(POST_NETWORK_APPLY, {})
logger.info("Network config applied: %d interfaces", result.get("applied", 0)) logger.info("Network config applied: %d interfaces", result.get("applied", 0))
return _ok(result) return _ok(result)
except RuntimeError as exc: except RuntimeError as exc:
@@ -126,7 +145,7 @@ def infer_dhcp_ranges():
JSON with per-interface suggested DHCP ranges. JSON with per-interface suggested DHCP ranges.
""" """
try: try:
return _ok(get("/network/infer-dhcp-ranges")) return _ok(get(GET_NETWORK_INFER_DHCP_RANGES))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to infer DHCP ranges: %s", exc) logger.error("Failed to infer DHCP ranges: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -143,7 +162,7 @@ def infer_zones():
JSON with per-interface suggested zone names. JSON with per-interface suggested zone names.
""" """
try: try:
return _ok(get("/network/infer-zones")) return _ok(get(GET_NETWORK_INFER_ZONES))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to infer zones: %s", exc) logger.error("Failed to infer zones: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
+24 -11
View File
@@ -8,6 +8,19 @@ import logging
from flask import Blueprint, request from flask import Blueprint, request
from daemon.client import BadRequest, NotFound, delete, get, patch, post from daemon.client import BadRequest, NotFound, delete, get, patch, post
from daemon.iface import (
DELETE_NGINX_DOMAINS_REMOVE,
GET_NGINX_CONFIG,
GET_NGINX_DOMAINS,
PATCH_NGINX_CONFIG,
POST_NGINX_APPLY,
POST_NGINX_CONFIG,
POST_NGINX_DOMAINS_ADD,
POST_NGINX_DOMAINS_UPDATE,
POST_NGINX_MANAGEMENT,
POST_NGINX_SSL_APPLY,
POST_NGINX_TEST,
)
from webui.api.common import _error, _ok from webui.api.common import _error, _ok
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -27,7 +40,7 @@ def ssl_apply_bp():
RuntimeError: If nginx SSL snippet write fails. RuntimeError: If nginx SSL snippet write fails.
""" """
try: try:
post("/nginx/ssl-apply") post(POST_NGINX_SSL_APPLY)
logger.info("SSL snippet written via API") logger.info("SSL snippet written via API")
return _ok(None) return _ok(None)
except RuntimeError as exc: except RuntimeError as exc:
@@ -45,7 +58,7 @@ def get_config_bp():
Current config dict from the daemon. Current config dict from the daemon.
""" """
try: try:
return _ok(get("/nginx/config")) return _ok(get(GET_NGINX_CONFIG))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to read proxy config: %s", exc) logger.error("Failed to read proxy config: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -67,7 +80,7 @@ def post_config():
if not isinstance(body, dict): if not isinstance(body, dict):
return _error("Request body must be a JSON object", 400) return _error("Request body must be a JSON object", 400)
try: try:
post("/nginx/config", body) post(POST_NGINX_CONFIG, body)
logger.info("Proxy config saved: %s", sorted(body.keys())) logger.info("Proxy config saved: %s", sorted(body.keys()))
return _ok(None) return _ok(None)
except BadRequest as exc: except BadRequest as exc:
@@ -94,7 +107,7 @@ def patch_config():
if not isinstance(body, dict): if not isinstance(body, dict):
return _error("Request body must be a JSON object", 400) return _error("Request body must be a JSON object", 400)
try: try:
patch("/nginx/config", body) patch(PATCH_NGINX_CONFIG, body)
logger.info("Proxy config patched: %s", sorted(body.keys())) logger.info("Proxy config patched: %s", sorted(body.keys()))
return _ok(None) return _ok(None)
except BadRequest as exc: except BadRequest as exc:
@@ -115,7 +128,7 @@ def list_domains():
List of domain dicts from the daemon. List of domain dicts from the daemon.
""" """
try: try:
return _ok(get("/nginx/domains")) return _ok(get(GET_NGINX_DOMAINS))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to list proxy domains: %s", exc) logger.error("Failed to list proxy domains: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -153,7 +166,7 @@ def add_domain_bp():
return _error("'backend_port' is required", 400) return _error("'backend_port' is required", 400)
try: try:
post( post(
"/nginx/domains/add", POST_NGINX_DOMAINS_ADD,
{ {
"domain": domain, "domain": domain,
"backend_host": backend_host, "backend_host": backend_host,
@@ -189,7 +202,7 @@ def update_domain_bp(domain):
if not body: if not body:
return _error("Request body must be a JSON object with fields to update", 400) return _error("Request body must be a JSON object with fields to update", 400)
try: try:
post("/nginx/domains/update", {"domain": domain, **body}) post(POST_NGINX_DOMAINS_UPDATE, {"domain": domain, **body})
logger.info("Proxy domain '%s' updated via API", domain) logger.info("Proxy domain '%s' updated via API", domain)
return _ok({"domain": domain}) return _ok({"domain": domain})
except BadRequest as exc: except BadRequest as exc:
@@ -213,7 +226,7 @@ def remove_domain_bp(domain):
``{"domain": ...}`` on success. ``{"domain": ...}`` on success.
""" """
try: try:
delete("/nginx/domains/remove", {"domain": domain}) delete(DELETE_NGINX_DOMAINS_REMOVE, {"domain": domain})
logger.info("Proxy domain removed via API: %s", domain) logger.info("Proxy domain removed via API: %s", domain)
return _ok({"domain": domain}) return _ok({"domain": domain})
except NotFound as exc: except NotFound as exc:
@@ -234,7 +247,7 @@ def apply_bp():
``{"ok": true}`` on success. ``{"ok": true}`` on success.
""" """
try: try:
post("/nginx/apply") post(POST_NGINX_APPLY)
logger.info("nginx config applied via API") logger.info("nginx config applied via API")
return _ok(None) return _ok(None)
except RuntimeError as exc: except RuntimeError as exc:
@@ -252,7 +265,7 @@ def test_bp():
``{"valid": true, "output": ...}`` on success. Returns 400 if test fails. ``{"valid": true, "output": ...}`` on success. Returns 400 if test fails.
""" """
try: try:
result = post("/nginx/test") result = post(POST_NGINX_TEST)
if result.get("valid"): if result.get("valid"):
return _ok({"valid": True, "output": result.get("output", "")}) return _ok({"valid": True, "output": result.get("output", "")})
return _error(result.get("output", "unknown error"), 400) return _error(result.get("output", "unknown error"), 400)
@@ -287,7 +300,7 @@ def management_bp():
auth_pass = body.get("auth_pass") auth_pass = body.get("auth_pass")
try: try:
post( post(
"/nginx/management", POST_NGINX_MANAGEMENT,
{ {
"domain": domain, "domain": domain,
"flask_host": flask_host, "flask_host": flask_host,
+27 -13
View File
@@ -8,6 +8,20 @@ import logging
from flask import Blueprint, request from flask import Blueprint, request
from daemon.client import BadRequest, NotFound, delete, get, patch, post from daemon.client import BadRequest, NotFound, delete, get, patch, post
from daemon.iface import (
DELETE_WIREGUARD_PEERS_REMOVE,
GET_WIREGUARD_CONFIG,
GET_WIREGUARD_PEER_STATUS,
GET_WIREGUARD_PEERS,
GET_WIREGUARD_STATUS,
PATCH_WIREGUARD_CONFIG,
POST_WIREGUARD_APPLY,
POST_WIREGUARD_CONFIG,
POST_WIREGUARD_DOWN,
POST_WIREGUARD_GENERATE_CLIENT,
POST_WIREGUARD_INITIALIZE,
POST_WIREGUARD_PEERS_ADD,
)
from webui.api.common import _error, _ok from webui.api.common import _error, _ok
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -25,7 +39,7 @@ def get_config_bp():
response on failure. response on failure.
""" """
try: try:
return _ok(get("/wireguard/config")) return _ok(get(GET_WIREGUARD_CONFIG))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to read WireGuard config: %s", exc) logger.error("Failed to read WireGuard config: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -53,7 +67,7 @@ def post_config():
body = dict(body) body = dict(body)
body["interface"] = dict(body["interface"]) body["interface"] = dict(body["interface"])
body["interface"].pop("private_key", None) body["interface"].pop("private_key", None)
post("/wireguard/config", body) post(POST_WIREGUARD_CONFIG, body)
return _ok(None) return _ok(None)
except BadRequest as exc: except BadRequest as exc:
logger.info("WireGuard config save rejected: %s", exc) logger.info("WireGuard config save rejected: %s", exc)
@@ -85,7 +99,7 @@ def patch_config():
body = dict(body) body = dict(body)
body["interface"] = dict(body["interface"]) body["interface"] = dict(body["interface"])
body["interface"].pop("private_key", None) body["interface"].pop("private_key", None)
patch("/wireguard/config", body) patch(PATCH_WIREGUARD_CONFIG, body)
logger.info("WireGuard config patched: %s", sorted(body.keys())) logger.info("WireGuard config patched: %s", sorted(body.keys()))
return _ok(None) return _ok(None)
except BadRequest as exc: except BadRequest as exc:
@@ -106,7 +120,7 @@ def apply_bp():
Success response on acceptance, or 500 on server error. Success response on acceptance, or 500 on server error.
""" """
try: try:
post("/wireguard/apply") post(POST_WIREGUARD_APPLY)
logger.info("WireGuard tunnel applied via API") logger.info("WireGuard tunnel applied via API")
return _ok(None) return _ok(None)
except RuntimeError as exc: except RuntimeError as exc:
@@ -124,7 +138,7 @@ def up_bp():
Success response on acceptance, or 500 on server error. Success response on acceptance, or 500 on server error.
""" """
try: try:
post("/wireguard/apply") post(POST_WIREGUARD_APPLY)
logger.info("WireGuard tunnel started via API") logger.info("WireGuard tunnel started via API")
return _ok(None) return _ok(None)
except RuntimeError as exc: except RuntimeError as exc:
@@ -142,7 +156,7 @@ def down_bp():
Success response on acceptance, or 500 on server error. Success response on acceptance, or 500 on server error.
""" """
try: try:
post("/wireguard/down") post(POST_WIREGUARD_DOWN)
logger.info("WireGuard tunnel brought down via API") logger.info("WireGuard tunnel brought down via API")
return _ok(None) return _ok(None)
except RuntimeError as exc: except RuntimeError as exc:
@@ -161,7 +175,7 @@ def status_bp():
response on failure. response on failure.
""" """
try: try:
return _ok(get("/wireguard/status")) return _ok(get(GET_WIREGUARD_STATUS))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to get WireGuard status: %s", exc) logger.error("Failed to get WireGuard status: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -177,7 +191,7 @@ def initialize_bp():
Success response on acceptance, or 500 on server error. Success response on acceptance, or 500 on server error.
""" """
try: try:
post("/wireguard/initialize") post(POST_WIREGUARD_INITIALIZE)
logger.info("WireGuard initialized via API") logger.info("WireGuard initialized via API")
return _ok(None) return _ok(None)
except RuntimeError as exc: except RuntimeError as exc:
@@ -208,7 +222,7 @@ def add_peer_bp():
return _error("'name' is required", 400) return _error("'name' is required", 400)
try: try:
peer = post( peer = post(
"/wireguard/peers/add", POST_WIREGUARD_PEERS_ADD,
{ {
"name": name, "name": name,
"endpoint": body.get("endpoint"), "endpoint": body.get("endpoint"),
@@ -241,7 +255,7 @@ def remove_peer_bp(name):
or 500 on server error. or 500 on server error.
""" """
try: try:
delete("/wireguard/peers/remove", {"name": name}) delete(DELETE_WIREGUARD_PEERS_REMOVE, {"name": name})
logger.info("WireGuard peer '%s' removed via API", name) logger.info("WireGuard peer '%s' removed via API", name)
return _ok({"name": name}) return _ok({"name": name})
except NotFound as exc: except NotFound as exc:
@@ -263,7 +277,7 @@ def peers_bp():
on failure. on failure.
""" """
try: try:
return _ok(get("/wireguard/peers")) return _ok(get(GET_WIREGUARD_PEERS))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to list WireGuard peers: %s", exc) logger.error("Failed to list WireGuard peers: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -280,7 +294,7 @@ def peer_status_bp():
on failure. on failure.
""" """
try: try:
return _ok(get("/wireguard/peer-status")) return _ok(get(GET_WIREGUARD_PEER_STATUS))
except RuntimeError as exc: except RuntimeError as exc:
logger.error("Failed to get WireGuard peer status: %s", exc) logger.error("Failed to get WireGuard peer status: %s", exc)
return _error(str(exc), 500) return _error(str(exc), 500)
@@ -309,7 +323,7 @@ def generate_client_bp():
return _error("Field 'server_endpoint' is required", 400) return _error("Field 'server_endpoint' is required", 400)
try: try:
result = post( result = post(
"/wireguard/generate-client", POST_WIREGUARD_GENERATE_CLIENT,
{ {
"name": name, "name": name,
"server_endpoint": server_endpoint, "server_endpoint": server_endpoint,