diff options
Diffstat (limited to 'plugins/modules/vyos_firewall_global.py')
| -rw-r--r-- | plugins/modules/vyos_firewall_global.py | 402 |
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() |
