Files
vacuum-wall/webui/api/dhcp.py
T
mteehan 2f215793e9 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.
2026-05-30 16:15:45 +00:00

312 lines
9.9 KiB
Python

"""DHCP/DNS (dnsmasq) management API blueprint.
Exposed at /api/dhcp/* and delegates all operations to vacuum-walld.
"""
import logging
from flask import Blueprint, request
from daemon.client import BadRequest, NotFound, delete, get, patch, post
from webui.api.common import _error, _ok
logger = logging.getLogger(__name__)
bp = Blueprint("dhcp", __name__)
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
@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:
logger.error("Failed to read DHCP config: %s", exc)
return _error(str(exc), 500)
@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)
try:
post("/dnsmasq/config", body)
return _ok(None)
except BadRequest as exc:
logger.info("DHCP config save rejected: %s", exc)
return _error(str(exc), 400)
except RuntimeError as exc:
logger.error("Failed to save DHCP config: %s", exc)
return _error(str(exc), 500)
@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)
try:
patch("/dnsmasq/config", body)
return _ok(None)
except BadRequest as exc:
logger.info("DHCP config patch rejected: %s", exc)
return _error(str(exc), 400)
except RuntimeError as exc:
logger.error("Failed to patch DHCP config: %s", exc)
return _error(str(exc), 500)
@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")
return _ok(None)
except RuntimeError as exc:
logger.error("Failed to apply dnsmasq config: %s", exc)
return _error(str(exc), 500)
# ---------------------------------------------------------------------------
# Status
# ---------------------------------------------------------------------------
@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:
logger.error("Failed to get DHCP status: %s", exc)
return _error(str(exc), 500)
# ---------------------------------------------------------------------------
# DHCP ranges
# ---------------------------------------------------------------------------
@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()
end = body.get("end", "").strip()
lease_time = body.get("lease_time", "12h")
if not start or not end:
return _error("'start' and 'end' are required", 400)
try:
post(
"/dnsmasq/ranges/add",
{
"interface": iface or "",
"start": start,
"end": end,
"lease_time": lease_time,
},
)
logger.info("DHCP range added via API: %s-%s", start, end)
return _ok(None)
except BadRequest as exc:
logger.info("Add DHCP range rejected: %s", exc)
return _error(str(exc), 400)
except RuntimeError as exc:
logger.error("Failed to add DHCP range: %s", exc)
return _error(str(exc), 500)
@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()
end = body.get("end", "").strip()
if not start or not end:
return _error("'start' and 'end' are required", 400)
try:
delete(
"/dnsmasq/ranges/remove", {"interface": iface, "start": start, "end": end}
)
logger.info("DHCP range removed via API: %s-%s", start, end)
return _ok(None)
except NotFound as exc:
logger.info("Remove DHCP range not found: %s", exc)
return _error(str(exc), 404)
except RuntimeError as exc:
logger.error("Failed to remove DHCP range: %s", exc)
return _error(str(exc), 500)
# ---------------------------------------------------------------------------
# Leases
# ---------------------------------------------------------------------------
@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:
logger.error("Failed to read lease table: %s", exc)
return _error(str(exc), 500)
# ---------------------------------------------------------------------------
# Static leases
# ---------------------------------------------------------------------------
@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()
hostname = body.get("hostname")
if not mac or not ip:
return _error("'mac' and 'ip' are required", 400)
try:
post("/dnsmasq/static-lease/add", {"mac": mac, "ip": ip, "hostname": hostname})
logger.info("Static lease added via API: %s -> %s", mac, ip)
return _ok({"mac": mac, "ip": ip, "hostname": hostname})
except BadRequest as exc:
logger.info("Add static lease rejected: %s", exc)
return _error(str(exc), 400)
except RuntimeError as exc:
logger.error("Failed to add static lease: %s", exc)
return _error(str(exc), 500)
@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)
return _ok(None)
except NotFound as exc:
logger.info("Static lease '%s' not found: %s", mac, exc)
return _error(str(exc), 404)
except RuntimeError as exc:
logger.error("Failed to remove static lease '%s': %s", mac, exc)
return _error(str(exc), 500)
# ---------------------------------------------------------------------------
# DNS records
# ---------------------------------------------------------------------------
@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()
hostname = body.get("hostname")
if not name or not address:
return _error("'name' and 'address' are required", 400)
try:
post(
"/dnsmasq/dns-record/add",
{"name": name, "address": address, "hostname": hostname},
)
logger.info("DNS record added via API: %s -> %s", name, address)
return _ok({"name": name, "address": address, "hostname": hostname})
except BadRequest as exc:
logger.info("Add DNS record rejected: %s", exc)
return _error(str(exc), 400)
except RuntimeError as exc:
logger.error("Failed to add DNS record: %s", exc)
return _error(str(exc), 500)
@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)
return _ok(None)
except NotFound as exc:
logger.info("DNS record '%s' not found: %s", name, exc)
return _error(str(exc), 404)
except RuntimeError as exc:
logger.error("Failed to remove DNS record '%s': %s", name, exc)
return _error(str(exc), 500)