summaryrefslogtreecommitdiff
path: root/plugins/modules
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-07-04 14:39:16 +1000
committeromnom62 <omnom62@outlook.com>2026-07-04 14:39:16 +1000
commitd95da0c5d3dc8e29c231eb4ac3ed5d66050da427 (patch)
treebb06733b2d89a374bb17942de35547d79b1fabce /plugins/modules
parentd997d0c7a4d5c8973186f2d9ba1581314a93866a (diff)
downloadrest.vyos-d95da0c5d3dc8e29c231eb4ac3ed5d66050da427.tar.gz
rest.vyos-d95da0c5d3dc8e29c231eb4ac3ed5d66050da427.zip
T8989: vyos_firewall_rules module
Diffstat (limited to 'plugins/modules')
-rw-r--r--plugins/modules/vyos_firewall_rules.py546
1 files changed, 546 insertions, 0 deletions
diff --git a/plugins/modules/vyos_firewall_rules.py b/plugins/modules/vyos_firewall_rules.py
new file mode 100644
index 0000000..a1c7c01
--- /dev/null
+++ b/plugins/modules/vyos_firewall_rules.py
@@ -0,0 +1,546 @@
+#!/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_rules
+short_description: Manage firewall rule sets on VyOS devices using REST API
+description:
+ - Manages named firewall rule sets on VyOS devices via the REST API.
+ - Supports both IPv4 (C(ipv4)) and IPv6 (C(ipv6)) rule sets.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+ - In VyOS 1.5+, firewall uses named rule sets under C(firewall.ipv4.name)
+ and C(firewall.ipv6.name).
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: Firewall rule set configuration.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ rule_sets:
+ description: Named rule sets for this address family.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Rule set name.
+ type: str
+ required: true
+ default_action:
+ description: Default action when no rule matches.
+ type: str
+ choices: [accept, drop, reject]
+ description:
+ description: Rule set description.
+ type: str
+ rules:
+ description: Firewall rules in this rule set.
+ 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]
+ source:
+ description: Source match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Source IP address or prefix.
+ type: str
+ group:
+ description: Source group name.
+ 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
+ group:
+ description: Destination group name.
+ type: str
+ port:
+ description: Destination port or range.
+ type: str
+ log:
+ description: Enable logging for this rule.
+ type: bool
+ icmp:
+ description: ICMP type/code to match.
+ type: dict
+ suboptions:
+ type:
+ description: ICMP type.
+ type: int
+ code:
+ description: ICMP code.
+ type: int
+ state:
+ description:
+ - Desired state of the firewall rules configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces rule sets for named rule sets in config.
+ - C(overridden) replaces all firewall rule sets.
+ - C(deleted) removes firewall rule sets.
+ - 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).
+ - Rule sets are identified by AFI and name. Deleting a rule set removes
+ all its rules.
+"""
+
+EXAMPLES = r"""
+- name: Merge firewall rules
+ vyos.rest.vyos_firewall_rules:
+ config:
+ - afi: ipv4
+ rule_sets:
+ - name: RULE-SET1
+ default_action: drop
+ rules:
+ - number: 10
+ action: accept
+ protocol: tcp
+ source:
+ address: 192.168.1.0/24
+ destination:
+ port: "80"
+ - number: 20
+ action: drop
+ state: invalid
+ - afi: ipv6
+ rule_sets:
+ - name: RULE-SET6
+ default_action: accept
+ rules:
+ - number: 10
+ action: accept
+ state: merged
+
+- name: Delete all firewall rules
+ vyos.rest.vyos_firewall_rules:
+ state: deleted
+
+- name: Gather firewall rules
+ vyos.rest.vyos_firewall_rules:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Firewall rules configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: Firewall rules 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 rules 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"]
+
+
+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
+
+ src = data.get("source", {}) or {}
+ if src:
+ rule["source"] = {}
+ if "address" in src:
+ rule["source"]["address"] = src["address"]
+ if "group" in src:
+ grp = src["group"]
+ if isinstance(grp, dict):
+ rule["source"]["group"] = list(grp.values())[0] if grp else None
+ else:
+ rule["source"]["group"] = grp
+ if "port" in src:
+ rule["source"]["port"] = src["port"]
+
+ dst = data.get("destination", {}) or {}
+ if dst:
+ rule["destination"] = {}
+ if "address" in dst:
+ rule["destination"]["address"] = dst["address"]
+ if "group" in dst:
+ grp = dst["group"]
+ if isinstance(grp, dict):
+ rule["destination"]["group"] = list(grp.values())[0] if grp else None
+ else:
+ rule["destination"]["group"] = grp
+ if "port" in dst:
+ rule["destination"]["port"] = dst["port"]
+
+ icmp = data.get("icmp", {}) or {}
+ if icmp:
+ rule["icmp"] = {}
+ if "type" in icmp:
+ rule["icmp"]["type"] = int(icmp["type"])
+ if "code" in icmp:
+ rule["icmp"]["code"] = int(icmp["code"])
+
+ return rule
+
+
+def _parse_rule_set(rs_name, data):
+ rs = {"name": rs_name}
+ data = data or {}
+ if "default-action" in data:
+ rs["default_action"] = data["default-action"]
+ if "description" in data:
+ rs["description"] = data["description"]
+ rules_raw = 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:
+ rs["rules"] = rules
+ return rs
+
+
+def get_running_config(vyos):
+ result = []
+ for afi in _AFIS:
+ raw = vyos.get_config(_BASE + [afi, "name"])
+ if not raw or not isinstance(raw, dict):
+ continue
+ # unwrap "name" key if present
+ raw = raw.get("name", raw)
+ if not raw or not isinstance(raw, dict):
+ continue
+ rule_sets = [_parse_rule_set(name, data) for name, data in sorted(raw.items())]
+ if rule_sets:
+ result.append({"afi": afi, "rule_sets": rule_sets})
+ return result
+
+
+def _rule_cmds(rs_name, afi, rule, have_rule):
+ cmds = []
+ rbase = _BASE + [afi, "name", rs_name, "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"])]))
+ if want_ep.get("group") and want_ep["group"] != have_ep.get("group"):
+ cmds.append(
+ (
+ "set",
+ rbase
+ + [
+ endpoint,
+ "group",
+ "address-group",
+ want_ep["group"],
+ ],
+ ),
+ )
+
+ icmp = rule.get("icmp") or {}
+ have_icmp = have_rule.get("icmp") or {}
+ if icmp.get("type") and icmp["type"] != have_icmp.get("type"):
+ cmds.append(("set", rbase + ["icmp", "type", str(icmp["type"])]))
+ if icmp.get("code") and icmp["code"] != have_icmp.get("code"):
+ cmds.append(("set", rbase + ["icmp", "code", str(icmp["code"])]))
+
+ return cmds
+
+
+def _rule_set_cmds(afi, rs, have_rs, state):
+ cmds = []
+ rs_name = rs["name"]
+ rsbase = _BASE + [afi, "name", rs_name]
+ have_rs = have_rs or {}
+
+ if rs.get("default_action") and rs["default_action"] != have_rs.get("default_action"):
+ cmds.append(("set", rsbase + ["default-action", rs["default_action"]]))
+ if rs.get("description") and rs["description"] != have_rs.get("description"):
+ cmds.append(("set", rsbase + ["description", rs["description"]]))
+
+ have_rules = {r["number"]: r for r in (have_rs.get("rules") or [])}
+ want_rules = {r["number"]: r for r in (rs.get("rules") or [])}
+
+ if state == "replaced":
+ for num in set(have_rules) - set(want_rules):
+ cmds.append(("delete", rsbase + ["rule", str(num)]))
+
+ for num, rule in want_rules.items():
+ cmds += _rule_cmds(rs_name, afi, rule, have_rules.get(num))
+
+ return cmds
+
+
+def build_commands(config, have_list, state):
+ cmds = []
+
+ if state == "deleted":
+ if not config:
+ if have_list:
+ cmds.append(("delete", _BASE))
+ else:
+ have_map = {
+ (e["afi"], rs["name"]): rs for e in have_list for rs in e.get("rule_sets", [])
+ }
+ for entry in config:
+ afi = entry["afi"]
+ for rs in entry.get("rule_sets") or []:
+ if (afi, rs["name"]) in have_map:
+ cmds.append(("delete", _BASE + [afi, "name", rs["name"]]))
+ return cmds
+
+ have_map = {e["afi"]: {rs["name"]: rs for rs in e.get("rule_sets", [])} for e in have_list}
+
+ if state == "overridden":
+ want_keys = {
+ (e["afi"], rs["name"]) for e in (config or []) for rs in e.get("rule_sets", [])
+ }
+ for e in have_list:
+ for rs in e.get("rule_sets", []):
+ if (e["afi"], rs["name"]) not in want_keys:
+ cmds.append(("delete", _BASE + [e["afi"], "name", rs["name"]]))
+
+ for entry in config or []:
+ afi = entry["afi"]
+ have_afi = have_map.get(afi, {})
+
+ for rs in entry.get("rule_sets") or []:
+ have_rs = have_afi.get(rs["name"])
+
+ if state == "replaced" and have_rs:
+ # delete and rebuild if different
+ want_cmds = _rule_set_cmds(afi, rs, {}, "merged")
+ have_cmds = _rule_set_cmds(
+ afi,
+ {
+ "name": rs["name"],
+ "default_action": have_rs.get("default_action"),
+ "rules": have_rs.get("rules", []),
+ },
+ {},
+ "merged",
+ )
+ if want_cmds != have_cmds:
+ cmds.append(("delete", _BASE + [afi, "name", rs["name"]]))
+ have_rs = None
+
+ cmds += _rule_set_cmds(
+ afi,
+ rs,
+ have_rs,
+ state if state not in ("replaced", "overridden") else "merged",
+ )
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", choices=["ipv4", "ipv6"], required=True),
+ rule_sets=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", 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"),
+ group=dict(type="str"),
+ port=dict(type="str"),
+ ),
+ ),
+ destination=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ group=dict(type="str"),
+ port=dict(type="str"),
+ ),
+ ),
+ icmp=dict(
+ type="dict",
+ options=dict(
+ type=dict(type="int"),
+ code=dict(type="int"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ 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()