summaryrefslogtreecommitdiff
path: root/plugins/modules/vyos_firewall_interfaces.py
diff options
context:
space:
mode:
authoromnom62 <75066712+omnom62@users.noreply.github.com>2026-08-22 01:36:21 +1000
committerGitHub <noreply@github.com>2026-08-21 10:36:21 -0500
commit7a6b5e4f3a7a021cfa75faa7bf833741dfc09cff (patch)
tree82dad9c7ca58b7c88ac25f2ef7b04413e259a260 /plugins/modules/vyos_firewall_interfaces.py
parentcb738721c15ac01f49f66144dae80eb478331f77 (diff)
downloadrest.vyos-7a6b5e4f3a7a021cfa75faa7bf833741dfc09cff.tar.gz
rest.vyos-7a6b5e4f3a7a021cfa75faa7bf833741dfc09cff.zip
T8989: wave3 user, bgp_global, bgp_address_family, facts, firewall_global
* T8989: vyos_user module * T8989: Wave 3 vyos_user module with integration and unit tests * T8989: Wave 3 vyos_bgp_global module with integration and unit tests * T8989: Wave 3 vyos_bgp_address_family module with integration and unit tests * T8989: Add overridden integration tests for vyos_bgp_global and vyos_bgp_address_family * T8989: vyos_facts * T8989: Add vyos_facts integration and unit tests with fixtures * T8989: vyos_firewall_global module * T8989: vyos_firewall_global module with integration and unit tests * T8989: vyos_firewall_rules module * T8989: vyos_firewall_rules module with integration and unit tests * T8989: vyos_firewall_interfaces module * T8989: vyos_firewall_interfaces module * T8989: vyos_firewall_interfaces UAT & SIT * T8989: vyos_firewall_interfaces UAT & SIT
Diffstat (limited to 'plugins/modules/vyos_firewall_interfaces.py')
-rw-r--r--plugins/modules/vyos_firewall_interfaces.py473
1 files changed, 473 insertions, 0 deletions
diff --git a/plugins/modules/vyos_firewall_interfaces.py b/plugins/modules/vyos_firewall_interfaces.py
new file mode 100644
index 0000000..8769f54
--- /dev/null
+++ b/plugins/modules/vyos_firewall_interfaces.py
@@ -0,0 +1,473 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_firewall_interfaces
+short_description: Manage firewall hook filters on VyOS devices using REST API
+description:
+ - Manages firewall hook filter configuration on VyOS devices via the REST API.
+ - In VyOS 1.5+, firewall hook filters (input/output/forward) replace the
+ per-interface firewall assignments used in VyOS 1.4.
+ - Hook filters apply globally to all traffic traversing that hook point.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: Firewall hook filter configuration.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ hooks:
+ description: Hook filter configurations for this address family.
+ type: list
+ elements: dict
+ suboptions:
+ hook:
+ description: Netfilter hook point.
+ type: str
+ choices: [input, output, forward]
+ required: true
+ default_action:
+ description: Default action when no rule matches.
+ type: str
+ choices: [accept, drop, reject]
+ description:
+ description: Filter description.
+ type: str
+ rules:
+ description: Rules in this hook filter.
+ type: list
+ elements: dict
+ suboptions:
+ number:
+ description: Rule number.
+ type: int
+ required: true
+ action:
+ description: Rule action.
+ type: str
+ choices: [accept, drop, reject, return, queue, continue]
+ description:
+ description: Rule description.
+ type: str
+ disable:
+ description: Disable this rule.
+ type: bool
+ protocol:
+ description: Protocol to match.
+ type: str
+ state:
+ description: Connection state to match.
+ type: str
+ choices: [established, invalid, new, related]
+ log:
+ description: Enable logging.
+ type: bool
+ source:
+ description: Source match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Source IP address or prefix.
+ type: str
+ port:
+ description: Source port or range.
+ type: str
+ destination:
+ description: Destination match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Destination IP address or prefix.
+ type: str
+ port:
+ description: Destination port or range.
+ type: str
+ state:
+ description:
+ - Desired state of the firewall hook filter configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces hook filter config for named hooks in config.
+ - C(overridden) replaces all firewall hook filter config.
+ - C(deleted) removes firewall hook filter config.
+ - C(gathered) returns current configuration as structured data.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+ - In VyOS 1.5+, hook filters apply globally rather than per-interface.
+ Use named rule sets (M(vyos.rest.vyos_firewall_rules)) for more granular
+ per-traffic control.
+"""
+
+EXAMPLES = r"""
+- name: Merge firewall hook filter configuration
+ vyos.rest.vyos_firewall_interfaces:
+ config:
+ - afi: ipv4
+ hooks:
+ - hook: input
+ default_action: accept
+ rules:
+ - number: 10
+ action: accept
+ state: established
+ - number: 20
+ action: drop
+ state: invalid
+ - hook: forward
+ default_action: accept
+ - hook: output
+ default_action: accept
+ - afi: ipv6
+ hooks:
+ - hook: input
+ default_action: accept
+ state: merged
+
+- name: Delete all firewall hook filter configuration
+ vyos.rest.vyos_firewall_interfaces:
+ state: deleted
+
+- name: Gather firewall hook filter configuration
+ vyos.rest.vyos_firewall_interfaces:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Firewall hook filter configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: Firewall hook filter configuration after this module ran.
+ returned: when changed
+ type: list
+commands:
+ description: List of API command tuples sent to the device.
+ returned: always
+ type: list
+gathered:
+ description: Current firewall hook filter configuration as structured data.
+ returned: when state is gathered
+ type: list
+saved:
+ description: Whether the config was saved after changes.
+ returned: when changes are applied
+ type: bool
+response:
+ description: Raw API response.
+ returned: when changes are applied
+ type: dict
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+
+
+_BASE = ["firewall"]
+_AFIS = ["ipv4", "ipv6"]
+_HOOKS = ["input", "output", "forward"]
+
+
+def _parse_rule(rule_num, data):
+ rule = {"number": int(rule_num)}
+ data = data or {}
+ if "action" in data:
+ rule["action"] = data["action"]
+ if "description" in data:
+ rule["description"] = data["description"]
+ if "disable" in data:
+ rule["disable"] = True
+ if "protocol" in data:
+ rule["protocol"] = data["protocol"]
+ if "state" in data:
+ rule["state"] = data["state"]
+ if "log" in data:
+ rule["log"] = True
+
+ for endpoint in ["source", "destination"]:
+ ep = data.get(endpoint, {}) or {}
+ if ep:
+ rule[endpoint] = {}
+ if "address" in ep:
+ rule[endpoint]["address"] = ep["address"]
+ if "port" in ep:
+ rule[endpoint]["port"] = ep["port"]
+
+ return rule
+
+
+def _parse_hook_filter(hook, data):
+ entry = {"hook": hook}
+ data = data or {}
+ filter_data = data.get("filter", {}) or {}
+ if "default-action" in filter_data:
+ entry["default_action"] = filter_data["default-action"]
+ if "description" in filter_data:
+ entry["description"] = filter_data["description"]
+ rules_raw = filter_data.get("rule", {}) or {}
+ if rules_raw and isinstance(rules_raw, dict):
+ rules = [
+ _parse_rule(num, rdata)
+ for num, rdata in sorted(
+ rules_raw.items(),
+ key=lambda x: int(x[0]),
+ )
+ ]
+ if rules:
+ entry["rules"] = rules
+ return entry
+
+
+def get_running_config(vyos):
+ result = []
+ for afi in _AFIS:
+ raw = vyos.get_config(_BASE + [afi])
+ if not raw or not isinstance(raw, dict):
+ continue
+ hooks = []
+ for hook in _HOOKS:
+ if hook in raw:
+ parsed = _parse_hook_filter(hook, raw[hook])
+ if len(parsed) > 1: # more than just hook key
+ hooks.append(parsed)
+ if hooks:
+ result.append({"afi": afi, "hooks": hooks})
+ return result
+
+
+def _rule_cmds(afi, hook, rule, have_rule):
+ cmds = []
+ rbase = _BASE + [afi, hook, "filter", "rule", str(rule["number"])]
+ have_rule = have_rule or {}
+
+ if rule.get("action") and rule["action"] != have_rule.get("action"):
+ cmds.append(("set", rbase + ["action", rule["action"]]))
+ if rule.get("description") and rule["description"] != have_rule.get("description"):
+ cmds.append(("set", rbase + ["description", rule["description"]]))
+ if rule.get("disable") and not have_rule.get("disable"):
+ cmds.append(("set", rbase + ["disable"]))
+ if rule.get("protocol") and rule["protocol"] != have_rule.get("protocol"):
+ cmds.append(("set", rbase + ["protocol", rule["protocol"]]))
+ if rule.get("state") and rule["state"] != have_rule.get("state"):
+ cmds.append(("set", rbase + ["state", rule["state"]]))
+ if rule.get("log") and not have_rule.get("log"):
+ cmds.append(("set", rbase + ["log"]))
+
+ for endpoint in ["source", "destination"]:
+ want_ep = rule.get(endpoint) or {}
+ have_ep = have_rule.get(endpoint) or {}
+ if want_ep.get("address") and want_ep["address"] != have_ep.get("address"):
+ cmds.append(("set", rbase + [endpoint, "address", want_ep["address"]]))
+ if want_ep.get("port") and want_ep["port"] != have_ep.get("port"):
+ cmds.append(("set", rbase + [endpoint, "port", str(want_ep["port"])]))
+
+ return cmds
+
+
+def _hook_cmds(afi, hook_entry, have_hook, state):
+ cmds = []
+ hook = hook_entry["hook"]
+ hbase = _BASE + [afi, hook, "filter"]
+ have_hook = have_hook or {}
+
+ if hook_entry.get("default_action") and hook_entry["default_action"] != have_hook.get(
+ "default_action",
+ ):
+ cmds.append(("set", hbase + ["default-action", hook_entry["default_action"]]))
+ if hook_entry.get("description") and hook_entry["description"] != have_hook.get("description"):
+ cmds.append(("set", hbase + ["description", hook_entry["description"]]))
+
+ have_rules = {r["number"]: r for r in (have_hook.get("rules") or [])}
+ want_rules = {r["number"]: r for r in (hook_entry.get("rules") or [])}
+
+ if state == "replaced":
+ for num in set(have_rules) - set(want_rules):
+ cmds.append(("delete", hbase + ["rule", str(num)]))
+
+ for num, rule in want_rules.items():
+ cmds += _rule_cmds(afi, hook, rule, have_rules.get(num))
+
+ return cmds
+
+
+def build_commands(config, have_list, state):
+ cmds = []
+
+ if state == "deleted":
+ if not config:
+ if have_list:
+ for entry in have_list:
+ afi = entry["afi"]
+ for hook_entry in entry.get("hooks", []):
+ cmds.append(("delete", _BASE + [afi, hook_entry["hook"], "filter"]))
+ else:
+ have_map = {(e["afi"], h["hook"]): h for e in have_list for h in e.get("hooks", [])}
+ for entry in config:
+ afi = entry["afi"]
+ for hook_entry in entry.get("hooks") or []:
+ if (afi, hook_entry["hook"]) in have_map:
+ cmds.append(("delete", _BASE + [afi, hook_entry["hook"], "filter"]))
+ return cmds
+
+ have_map = {e["afi"]: {h["hook"]: h for h in e.get("hooks", [])} for e in have_list}
+
+ if state == "overridden":
+ want_keys = {(e["afi"], h["hook"]) for e in (config or []) for h in e.get("hooks", [])}
+ for e in have_list:
+ for h in e.get("hooks", []):
+ if (e["afi"], h["hook"]) not in want_keys:
+ cmds.append(("delete", _BASE + [e["afi"], h["hook"], "filter"]))
+
+ for entry in config or []:
+ afi = entry["afi"]
+ have_afi = have_map.get(afi, {})
+
+ for hook_entry in entry.get("hooks") or []:
+ hook = hook_entry["hook"]
+ have_hook = have_afi.get(hook)
+
+ if state == "replaced" and have_hook:
+ want_cmds = _hook_cmds(afi, hook_entry, {}, "merged")
+ have_hook_entry = {
+ "hook": hook,
+ "default_action": have_hook.get("default_action"),
+ "rules": have_hook.get("rules", []),
+ }
+ have_cmds = _hook_cmds(afi, have_hook_entry, {}, "merged")
+ if want_cmds != have_cmds:
+ cmds.append(("delete", _BASE + [afi, hook, "filter"]))
+ have_hook = None
+
+ effective_state = state if state not in ("replaced", "overridden") else "merged"
+ cmds += _hook_cmds(afi, hook_entry, have_hook, effective_state)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", choices=["ipv4", "ipv6"], required=True),
+ hooks=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ hook=dict(
+ type="str",
+ choices=["input", "output", "forward"],
+ required=True,
+ ),
+ default_action=dict(
+ type="str",
+ choices=["accept", "drop", "reject"],
+ ),
+ description=dict(type="str"),
+ rules=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ number=dict(type="int", required=True),
+ action=dict(
+ type="str",
+ choices=[
+ "accept",
+ "drop",
+ "reject",
+ "return",
+ "queue",
+ "continue",
+ ],
+ ),
+ description=dict(type="str"),
+ disable=dict(type="bool"),
+ protocol=dict(type="str"),
+ state=dict(
+ type="str",
+ choices=[
+ "established",
+ "invalid",
+ "new",
+ "related",
+ ],
+ ),
+ log=dict(type="bool"),
+ source=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ port=dict(type="str"),
+ ),
+ ),
+ destination=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ port=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or []
+
+ have = get_running_config(vyos)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+
+ commands = build_commands(config, have, state)
+
+ if module.check_mode:
+ module.exit_json(changed=bool(commands), commands=commands, before=have)
+
+ if commands:
+ response = vyos.apply_commands(commands)
+ saved = vyos.save_config()
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=get_running_config(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()