b673e87c9b
Add hoover model.js as a central reactive store per subsystem, replacing per-component data fetching with a single source of truth. - Add hoover/model.js with modelRegister, modelFetch, and WS invalidation - Refactor websocket.js to route messages to model refresh (drop per-component subscribe/unsubscribe) - Simplify component.js by removing WS subscription management - Add refresh option to apiSubmit, deprecate refactorLoad and checkAbort - Rewrite all pages to use getModel() instead of inline data fetching - Bootstrap model registrations in app.js - Add GET /api/firewall/state endpoint - Fix restart-services.sh restart order and add service health verification - Update hoover.md docs with model layer architecture
650 lines
20 KiB
Python
650 lines
20 KiB
Python
"""Firewall (firewalld) management API blueprint.
|
|
|
|
Exposed at /api/firewall/* 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 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_STATE,
|
|
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
|
|
|
|
logger = logging.getLogger(__name__)
|
|
bp = Blueprint("firewall", __name__)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Declarative config (two-step: save -> apply)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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(GET_FIREWALL_CONFIG))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to read firewall config: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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)
|
|
if not isinstance(body["zones"], dict):
|
|
return _error("'zones' must be a dict", 400)
|
|
try:
|
|
post(POST_FIREWALL_CONFIG, body)
|
|
try:
|
|
pending = get(GET_FIREWALL_CONFIG_PENDING)
|
|
pending_data = {
|
|
"pending": pending.get("pending", []),
|
|
"needs_apply": pending.get("needs_apply", False),
|
|
"unmanaged_zones": pending.get("unmanaged_zones", {}),
|
|
}
|
|
except RuntimeError as exc:
|
|
pending_data = None
|
|
logger.warning("Failed to read pending state after config save: %s", exc)
|
|
logger.info("Firewall config saved (%d zones)", len(body["zones"]))
|
|
return _ok(
|
|
{
|
|
"config_saved": True,
|
|
**(pending_data or {}),
|
|
}
|
|
)
|
|
except BadRequest as exc:
|
|
logger.info("Firewall config save rejected: %s", exc)
|
|
return _error(str(exc), 400)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to save firewall config: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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)
|
|
try:
|
|
patch(PATCH_FIREWALL_CONFIG, body)
|
|
try:
|
|
pending = get(GET_FIREWALL_CONFIG_PENDING)
|
|
pending_data = {
|
|
"pending": pending.get("pending", []),
|
|
"needs_apply": pending.get("needs_apply", False),
|
|
"unmanaged_zones": pending.get("unmanaged_zones", {}),
|
|
}
|
|
except RuntimeError as exc:
|
|
pending_data = None
|
|
logger.warning("Failed to read pending state after config patch: %s", exc)
|
|
return _ok(
|
|
{
|
|
"config_saved": True,
|
|
**(pending_data or {}),
|
|
}
|
|
)
|
|
except BadRequest as exc:
|
|
logger.info("Firewall config patch rejected: %s", exc)
|
|
return _error(str(exc), 400)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to patch firewall config: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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(POST_FIREWALL_CONFIG_APPLY)
|
|
logger.info("Firewall config applied: %s", result.get("applied_zones", []))
|
|
return _ok(result)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to apply firewall config: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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(GET_FIREWALL_CONFIG_PENDING))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to check pending config: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@bp.route("/state", methods=["GET"])
|
|
def get_state():
|
|
"""Retrieve current firewall state from the state store.
|
|
|
|
Endpoint:
|
|
GET /api/firewall/state
|
|
|
|
Returns:
|
|
JSON with firewall state data or an error message.
|
|
"""
|
|
try:
|
|
return _ok(get(GET_FIREWALL_STATE))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to get firewall state: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Zones
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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(GET_FIREWALL_ZONES)
|
|
return _ok(
|
|
{"active": data.get("active", {}), "available": data.get("available", [])}
|
|
)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to list zones: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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(GET_FIREWALL_ZONES_INFO, {"zone": name})
|
|
return _ok(info)
|
|
except NotFound as exc:
|
|
logger.info("Zone '%s' not found: %s", name, exc)
|
|
return _error(str(exc), 404)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to get zone '%s' info: %s", name, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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"
|
|
if not zone_name:
|
|
return _error("Zone name is required", 400)
|
|
try:
|
|
post(POST_FIREWALL_ZONES_CREATE, {"name": zone_name, "target": target})
|
|
logger.info("Zone '%s' created via API", zone_name)
|
|
return _ok(None)
|
|
except BadRequest as exc:
|
|
logger.info("Zone '%s' creation rejected: %s", zone_name, exc)
|
|
return _error(str(exc), 400)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to create zone '%s': %s", zone_name, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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(DELETE_FIREWALL_ZONES_DELETE, {"zone": name})
|
|
logger.info("Zone '%s' deleted via API", name)
|
|
return _ok(None)
|
|
except NotFound as exc:
|
|
logger.info("Zone '%s' not found: %s", name, exc)
|
|
return _error(str(exc), 404)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to delete zone '%s': %s", name, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Zone interfaces
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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):
|
|
return _error("'interfaces' must be a list", 400)
|
|
try:
|
|
post(POST_FIREWALL_ZONES_INTERFACES, {"zone": name, "interfaces": interfaces})
|
|
logger.info("Zone '%s' interfaces updated: %s", name, interfaces)
|
|
return _ok({"zone": name, "interfaces": interfaces})
|
|
except BadRequest as exc:
|
|
logger.info("Set interfaces for zone '%s' rejected: %s", name, exc)
|
|
return _error(str(exc), 400)
|
|
except NotFound as exc:
|
|
logger.info("Zone '%s' not found: %s", name, exc)
|
|
return _error(str(exc), 404)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to set interfaces for zone '%s': %s", name, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Zone services
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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):
|
|
return _error("'services' must be a list", 400)
|
|
try:
|
|
post(POST_FIREWALL_ZONES_SERVICES, {"zone": name, "services": services})
|
|
return _ok({"zone": name, "services": services})
|
|
except BadRequest as exc:
|
|
logger.info("Set services for zone '%s' rejected: %s", name, exc)
|
|
return _error(str(exc), 400)
|
|
except NotFound as exc:
|
|
logger.info("Zone '%s' not found: %s", name, exc)
|
|
return _error(str(exc), 404)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to set services for zone '%s': %s", name, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Available services and interfaces
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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(GET_FIREWALL_SERVICES))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to list services: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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(GET_FIREWALL_INTERFACES))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to list interfaces: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Rich rules
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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()
|
|
if not zone or not rule:
|
|
return _error("Both 'zone' and 'rule' are required", 400)
|
|
try:
|
|
entry = post(POST_FIREWALL_RICH_RULES_ADD, {"zone": zone, "rule": rule})
|
|
logger.info("Rich rule added to zone '%s': %s", zone, rule[:80])
|
|
return _ok({"zone": zone, "id": entry["id"], "rule": rule})
|
|
except BadRequest as exc:
|
|
logger.info("Add rich rule for zone '%s' rejected: %s", zone, exc)
|
|
return _error(str(exc), 400)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to add rich rule to zone '%s': %s", zone, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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(GET_FIREWALL_RICH_RULES, {"zone": zone}))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to get rich rules for zone '%s': %s", zone, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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(DELETE_FIREWALL_RICH_RULES_REMOVE, {"zone": zone, "id": rule_id})
|
|
logger.info("Rich rule '%s' removed from zone '%s'", rule_id, zone)
|
|
return _ok({"zone": zone, "id": rule_id})
|
|
except NotFound as exc:
|
|
logger.info("Rich rule '%s' not found in zone '%s': %s", rule_id, zone, exc)
|
|
return _error(str(exc), 404)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to remove rich rule from zone '%s': %s", zone, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Masquerade (NAT)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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")
|
|
if not zone or enable is None:
|
|
return _error("'zone' and 'enable' (bool) are required", 400)
|
|
try:
|
|
post(POST_FIREWALL_MASQUERADE, {"zone": zone, "enable": bool(enable)})
|
|
logger.info(
|
|
"Masquerade %s on zone '%s' via API",
|
|
"enabled" if enable else "disabled",
|
|
zone,
|
|
)
|
|
return _ok({"zone": zone, "masquerade": bool(enable)})
|
|
except BadRequest as exc:
|
|
logger.info("Set masquerade for zone '%s' rejected: %s", zone, exc)
|
|
return _error(str(exc), 400)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to set masquerade on zone '%s': %s", zone, exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Port forwarding
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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")
|
|
proto = body.get("proto", "").strip()
|
|
toaddr = body.get("toaddr")
|
|
toport = body.get("toport")
|
|
if not zone or port is None or not proto:
|
|
return _error("'zone', 'port', and 'proto' are required", 400)
|
|
try:
|
|
port_int = int(port)
|
|
except ValueError:
|
|
return _error("'port' must be an integer", 400)
|
|
toport_int = None
|
|
if toport is not None:
|
|
try:
|
|
toport_int = int(toport)
|
|
except ValueError:
|
|
return _error("'toport' must be an integer", 400)
|
|
toaddr_str = str(toaddr) if toaddr else None
|
|
try:
|
|
entry = post(
|
|
POST_FIREWALL_FORWARD_PORT_ADD,
|
|
{
|
|
"zone": zone,
|
|
"port": port_int,
|
|
"proto": proto,
|
|
"toaddr": toaddr_str,
|
|
"toport": toport_int,
|
|
},
|
|
)
|
|
return _ok({"zone": zone, "id": entry["id"], "port": port_int, "proto": proto})
|
|
except BadRequest as exc:
|
|
logger.info("Add forward port rejected: %s", exc)
|
|
return _error(str(exc), 400)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to add forward port: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@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(
|
|
DELETE_FIREWALL_FORWARD_PORT_REMOVE,
|
|
{"zone": zone, "port": port, "proto": proto},
|
|
)
|
|
logger.info("Forward port %s/%s removed from zone '%s'", port, proto, zone)
|
|
return _ok({"zone": zone, "port": port, "proto": proto})
|
|
except NotFound as exc:
|
|
logger.info(
|
|
"Forward port %s/%s not found in zone '%s': %s", port, proto, zone, exc
|
|
)
|
|
return _error(str(exc), 404)
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to remove forward port from zone '%s': %s", zone, exc)
|
|
return _error(str(exc), 500)
|