diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-08-22 01:36:21 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-21 10:36:21 -0500 |
| commit | 7a6b5e4f3a7a021cfa75faa7bf833741dfc09cff (patch) | |
| tree | 82dad9c7ca58b7c88ac25f2ef7b04413e259a260 /plugins/modules/vyos_firewall_interfaces.py | |
| parent | cb738721c15ac01f49f66144dae80eb478331f77 (diff) | |
| download | rest.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.py | 473 |
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() |
