""" 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): return jsonify({"ok": True, "data": data}) 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(None) 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(None) except RuntimeError as exc: return _error(str(exc), 500) @bp.route("/apply", methods=["POST"]) def apply_bp(): try: apply_config() return _ok(None) 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) current = get_config() found = any( lease["mac"].lower() == mac.lower() for lease in current.get("dhcp", {}).get("static_leases", []) ) if not found: return _error(f"No static lease found for MAC '{mac}'", 404) try: remove_static_lease(mac) return _ok(None) 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) current = get_config() found = any( r["name"] == name for r in current.get("dns", {}).get("custom_records", []) ) if not found: return _error(f"No DNS record found for '{name}'", 404) try: remove_dns_record(name) return _ok(None) except RuntimeError as exc: return _error(str(exc), 500)