Files
vacuum-wall/webui/api/dhcp.py
T
mteehan e2f56b8cc8 Initial commit: SSL proxy / firewall appliance
Flask WebUI behind nginx reverse proxy with zone-based firewall, DHCP,
WireGuard, and ACME certificate management.
2026-05-07 22:24:24 +00:00

172 lines
4.7 KiB
Python

"""
webui/api/dhcp.py - DHCP/DNS (dnsmasq) management API blueprint.
Exposed at /api/dhcp/* and delegates all operations to lib.dnsmasq.
"""
from flask import Blueprint, jsonify, request
from lib.dnsmasq import (
add_dns_record,
add_static_lease,
apply_config,
get_config,
get_lease_table,
remove_dns_record,
remove_static_lease,
save_config,
)
bp = Blueprint("dhcp", __name__)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _error(msg, code=400):
return jsonify({"ok": False, "error": msg}), code
def _ok(data=None):
body = {"ok": True}
if data is not None:
body["data"] = data
return jsonify(body)
def _deep_merge(base, overrides):
result = dict(base)
for k, v in overrides.items():
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
result[k] = _deep_merge(result[k], v)
else:
result[k] = v
return result
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
@bp.route("/config", methods=["GET"])
def get_config_bp():
try:
return _ok(get_config())
except RuntimeError as exc:
return _error(str(exc), 500)
@bp.route("/config", methods=["POST"])
def post_config():
body = request.get_json(silent=True) or {}
if not isinstance(body, dict):
return _error("Request body must be a JSON object", 400)
try:
save_config(body)
return _ok(body)
except RuntimeError as exc:
return _error(str(exc), 500)
@bp.route("/config", methods=["PATCH"])
def patch_config():
body = request.get_json(silent=True) or {}
if not isinstance(body, dict):
return _error("Request body must be a JSON object", 400)
try:
current = get_config()
merged = _deep_merge(current, body)
save_config(merged)
return _ok(merged)
except RuntimeError as exc:
return _error(str(exc), 500)
@bp.route("/apply", methods=["POST"])
def apply_bp():
try:
apply_config()
return _ok({"message": "dnsmasq configuration applied"})
except RuntimeError as exc:
return _error(str(exc), 500)
# ---------------------------------------------------------------------------
# Leases
# ---------------------------------------------------------------------------
@bp.route("/leases", methods=["GET"])
def leases_bp():
try:
return _ok(get_lease_table())
except RuntimeError as exc:
return _error(str(exc), 500)
# ---------------------------------------------------------------------------
# Static leases
# ---------------------------------------------------------------------------
@bp.route("/static-lease", methods=["POST"])
def add_static_lease_bp():
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:
add_static_lease(mac, ip, hostname)
return _ok({"mac": mac, "ip": ip, "hostname": hostname})
except RuntimeError as exc:
return _error(str(exc), 500)
@bp.route("/static-lease", methods=["DELETE"])
def remove_static_lease_bp():
mac = request.args.get("mac", "").strip()
if not mac:
return _error("Query parameter 'mac' is required", 400)
try:
remove_static_lease(mac)
return _ok({"mac": mac})
except RuntimeError as exc:
return _error(str(exc), 500)
# ---------------------------------------------------------------------------
# DNS records
# ---------------------------------------------------------------------------
@bp.route("/dns-record", methods=["POST"])
def add_dns_record_bp():
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:
add_dns_record(name, address, hostname)
return _ok({"name": name, "address": address, "hostname": hostname})
except RuntimeError as exc:
return _error(str(exc), 500)
@bp.route("/dns-record", methods=["DELETE"])
def remove_dns_record_bp():
name = request.args.get("name", "").strip()
if not name:
return _error("Query parameter 'name' is required", 400)
try:
remove_dns_record(name)
return _ok({"name": name})
except RuntimeError as exc:
return _error(str(exc), 500)