summaryrefslogtreecommitdiff
path: root/plugins/modules/vyos_firewall_global.py
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/modules/vyos_firewall_global.py')
-rw-r--r--plugins/modules/vyos_firewall_global.py402
1 files changed, 402 insertions, 0 deletions
diff --git a/plugins/modules/vyos_firewall_global.py b/plugins/modules/vyos_firewall_global.py
new file mode 100644
index 0000000..66cdabc
--- /dev/null
+++ b/plugins/modules/vyos_firewall_global.py
@@ -0,0 +1,402 @@
+#!/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_global
+short_description: Manage global firewall configuration on VyOS devices using REST API
+description:
+ - Manages global firewall group configuration on VyOS devices via the REST API.
+ - Covers address-groups, network-groups, port-groups, interface-groups,
+ and IPv6 network-groups.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: Global firewall configuration.
+ type: dict
+ suboptions:
+ group:
+ description: Firewall groups.
+ type: dict
+ suboptions:
+ address_group:
+ description: IPv4 address groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ address:
+ description: IP addresses or ranges in the group.
+ type: list
+ elements: str
+ network_group:
+ description: IPv4 network groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ network:
+ description: Network prefixes in the group.
+ type: list
+ elements: str
+ port_group:
+ description: Port groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ port:
+ description: Ports or port ranges in the group.
+ type: list
+ elements: str
+ interface_group:
+ description: Interface groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ interface:
+ description: Interfaces in the group.
+ type: list
+ elements: str
+ ipv6_network_group:
+ description: IPv6 network groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ network:
+ description: IPv6 network prefixes in the group.
+ type: list
+ elements: str
+ state:
+ description:
+ - Desired state of the firewall global configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces the entire firewall global configuration.
+ - C(deleted) removes firewall global configuration.
+ - C(gathered) returns current configuration as structured data.
+ type: str
+ choices: [merged, replaced, 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).
+"""
+
+EXAMPLES = r"""
+- name: Merge firewall global configuration
+ vyos.rest.vyos_firewall_global:
+ config:
+ group:
+ address_group:
+ - name: SERVERS
+ description: Web servers
+ address:
+ - 192.168.1.10
+ - 192.168.1.11
+ network_group:
+ - name: LAN
+ network:
+ - 192.168.0.0/16
+ port_group:
+ - name: WEB-PORTS
+ port:
+ - "80"
+ - "443"
+ interface_group:
+ - name: LAN-IFACES
+ interface:
+ - eth1
+ - eth2
+ ipv6_network_group:
+ - name: IPV6-LAN
+ network:
+ - "2001:db8::/32"
+ state: merged
+
+- name: Delete all firewall global configuration
+ vyos.rest.vyos_firewall_global:
+ state: deleted
+
+- name: Gather firewall global configuration
+ vyos.rest.vyos_firewall_global:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Firewall global configuration before this module ran.
+ returned: always
+ type: dict
+after:
+ description: Firewall global configuration after this module ran.
+ returned: when changed
+ type: dict
+commands:
+ description: List of API command tuples sent to the device.
+ returned: always
+ type: list
+gathered:
+ description: Current firewall global configuration as structured data.
+ returned: when state is gathered
+ type: dict
+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", "group"]
+
+# Map argspec key -> API key, value key
+_GROUP_TYPES = {
+ "address_group": ("address-group", "address"),
+ "network_group": ("network-group", "network"),
+ "port_group": ("port-group", "port"),
+ "interface_group": ("interface-group", "interface"),
+ "ipv6_network_group": ("ipv6-network-group", "network"),
+}
+
+
+def _parse_group_type(raw, val_key):
+ """Parse a group dict from API raw data."""
+ if not raw or not isinstance(raw, dict):
+ return []
+ result = []
+ for name, data in sorted(raw.items()):
+ entry = {"name": name}
+ data = data or {}
+ if data.get("description"):
+ entry["description"] = data["description"]
+ val = data.get(val_key)
+ if val is not None:
+ if isinstance(val, list):
+ entry[val_key.replace("-", "_")] = val
+ elif isinstance(val, str):
+ entry[val_key.replace("-", "_")] = [val]
+ elif isinstance(val, dict):
+ entry[val_key.replace("-", "_")] = list(val.keys())
+ result.append(entry)
+ return result
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {"group": {}}
+
+ for arg_key, (api_key, val_key) in _GROUP_TYPES.items():
+ groups = _parse_group_type(raw.get(api_key), val_key)
+ if groups:
+ result["group"][arg_key] = groups
+
+ if not result["group"]:
+ return {}
+ return result
+
+
+def _group_cmds(arg_key, groups, have_groups, state):
+ cmds = []
+ api_key, val_key = _GROUP_TYPES[arg_key]
+ have_map = {g["name"]: g for g in (have_groups or [])}
+ want_map = {g["name"]: g for g in (groups or [])}
+
+ if state == "replaced":
+ for name in set(have_map) - set(want_map):
+ cmds.append(("delete", _BASE + [api_key, name]))
+
+ for name, group in want_map.items():
+ have_group = have_map.get(name, {})
+ gbase = _BASE + [api_key, name]
+
+ if group.get("description") and group["description"] != have_group.get("description"):
+ cmds.append(("set", gbase + ["description", group["description"]]))
+
+ # normalize val_key for argspec (underscores)
+ arg_val_key = val_key.replace("-", "_")
+ want_vals = set(group.get(arg_val_key) or [])
+ have_vals = set(have_group.get(arg_val_key) or [])
+
+ for val in want_vals - have_vals:
+ cmds.append(("set", gbase + [val_key, val]))
+
+ if state == "replaced":
+ for val in have_vals - want_vals:
+ cmds.append(("delete", gbase + [val_key, val]))
+
+ return cmds
+
+
+def build_commands(config, have, state):
+ cmds = []
+
+ if state == "deleted":
+ if have:
+ cmds.append(("delete", _BASE))
+ return cmds
+
+ if state == "replaced":
+ # Check if anything differs
+ would_set = build_commands(config, {}, "merged")
+ have_set = build_commands(have, {}, "merged")
+ if would_set == have_set:
+ return []
+
+ config = config or {}
+ want_group = config.get("group") or {}
+ have_group = have.get("group") or {}
+
+ for arg_key in _GROUP_TYPES:
+ want_groups = want_group.get(arg_key) or []
+ have_groups = have_group.get(arg_key) or []
+ if want_groups or (state == "replaced" and have_groups):
+ cmds += _group_cmds(arg_key, want_groups, have_groups, state)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ group=dict(
+ type="dict",
+ options=dict(
+ address_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ address=dict(type="list", elements="str"),
+ ),
+ ),
+ network_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ network=dict(type="list", elements="str"),
+ ),
+ ),
+ port_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ port=dict(type="list", elements="str"),
+ ),
+ ),
+ interface_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ interface=dict(type="list", elements="str"),
+ ),
+ ),
+ ipv6_network_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ network=dict(type="list", elements="str"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ default="merged",
+ choices=["merged", "replaced", "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()