diff options
Diffstat (limited to 'plugins/module_utils')
3 files changed, 506 insertions, 20 deletions
diff --git a/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py index aeef42bc..dd17ef85 100644 --- a/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py @@ -4,23 +4,6 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -############################################# -# WARNING # -############################################# -# -# This file is auto generated by the resource -# module builder playbook. -# -# Do not edit this file manually. -# -# Changes to this file will be over written -# by the resource module builder. -# -# Changes should be made in the model used to -# generate this file or in the resource module -# builder template. -# -############################################# """ The arg spec for the vyos_firewall_global module """ @@ -185,6 +168,64 @@ class Firewall_globalArgs(object): # pylint: disable=R0903 ], "type": "str", }, + "zone": { + "elements": "dict", + "type": "list", + "options": { + "description": {"type": "str"}, + "default_action": { + "choices": [ + "drop", + "reject", + ], + "default": "drop", + "type": "str", + }, + "default_log": {"type": "bool"}, + "interfaces": { + "elements": "str", + "type": "list", + }, + "local_zone": {"type": "bool"}, + "name": { + "required": True, + "type": "str", + }, + "intra_zone_filtering": { + "type": "dict", + "options": { + "action": { + "choices": ["accept", "drop"], + "type": "str", + }, + "firewall": { + "type": "dict", + "options": { + "name": {"type": "str"}, + "ipv6_name": {"type": "str"}, + }, + }, + }, + }, + "sources": { + "elements": "dict", + "type": "list", + "options": { + "zone": { + "required": True, + "type": "str", + }, + "firewall": { + "type": "dict", + "options": { + "name": {"type": "str"}, + "ipv6_name": {"type": "str"}, + }, + }, + }, + }, + }, + }, }, "type": "dict", }, diff --git a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py index fcacb913..289037ef 100644 --- a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py @@ -241,6 +241,8 @@ class Firewall_global(ConfigBase): commands.extend(self._render_state_policy(key, w, h, opr=opr)) elif key == "route_redirects": commands.extend(self._render_route_redirects(key, w, h, opr=opr)) + elif key == "zone": + commands.extend(self._render_zone(key, w, h, opr=opr)) return commands def _add_global_attr(self, w, h, opr=True): @@ -692,7 +694,7 @@ class Firewall_global(ConfigBase): cmd = "set firewall " if ( attr - and key != "group" + and key not in ["group", "zone"] and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") ): cmd += "global-options " @@ -810,3 +812,324 @@ class Firewall_global(ConfigBase): elif attrib == "validation": regex = "source-validation" return regex + + def _render_zone(self, attr, w, h, opr): + """ + This function forms the commands for group list/members attributes based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param opr: True/False. + :return: generated list of commands. + """ + commands = [] + h_grp = [] + w_grp = [] + l_set = ("name", "description", "default_action", "default_log", "local_zone") + if w: + w_grp = w.get(attr) or [] + if h: + h_grp = h.get(attr) or [] + + if w_grp: + for want in w_grp: + h = self.search_attrib_in_have(h_grp, want, "name") + + cmd = self._compute_command(key="zone", attr="", opr=opr) + + if not opr and self._is_grp_del(h, want, "name"): + commands.append(cmd + " " + want["name"]) + continue + + for key, val in want.items(): + if val: + if opr and key in l_set and not (h and self._is_w_same(want, h, key)): + if key == "name": + pass + elif isinstance(val, bool): + commands.append( + cmd + " " + want["name"] + " " + key.replace("_", "-"), + ) + else: + commands.append( + cmd + + " " + + want["name"] + + " " + + key.replace("_", "-") + + " '" + + str(want[key]) + + "'", + ) + elif not opr and key in l_set: + if not (h and in_target_not_none(h, key)) and not self._is_grp_del( + h, + want, + "name", + ): + commands.append( + cmd + " " + want["name"] + " " + key.replace("_", "-"), + ) + elif key == "interfaces": + commands.extend( + self._render_interfaces( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + elif key == "intra_zone_filtering": + commands.extend( + self._render_izf( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + elif key == "sources": + commands.extend( + self._render_sources( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + return commands + + def _render_interfaces(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for interfaces + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + # VyOS 1.5.0 GA moved 'interface' under a new 'member' node + # ("set firewall zone <name> member interface <ifname>"). 1.4.x and + # 1.5-rolling snapshots predating this change still use the bare + # 'interface' node. Known limitation: a 1.5-rolling build reporting + # "1.5" that predates this change will incorrectly get the new + # syntax -- accepted trade-off, see PR notes. + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.5"): + iface_kw = "member interface" + else: + iface_kw = "interface" + + if want: + if opr: + interfaces = list_diff_want_only(want, have) + + for interface in interfaces: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + elif not opr and have: + interfaces = list_diff_want_only(want, have) + for interface in interfaces: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + elif not opr and not have: + for interface in want: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + else: + self._module.fail_json(msg={"want": want, "have": have, "opr": opr}) + + return commands + + def _render_izf(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for intra zone filtering + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + if want: + if opr: + izfs = self._dict_diff(want, have) + for izf in izfs: + commands.append( + cmd + + " " + + name + + " intra-zone-filtering " + + izf[0].replace(".", " ") + + " " + + izf[1], + ) + elif not opr and have: + izfs = self._dict_diff(want, have) + + for izf in izfs: + commands.append( + cmd + " " + name + " intra-zone-filtering " + izf[0].replace(".", " "), + ) + elif not opr and not have: + commands.append( + cmd + " " + name + " intra-zone-filtering", + ) + return commands + + def _dict_diff(self, want, have, path=""): + """ + Recursively find keys/values in `want` that differ or are missing in `have`. + Returns list of tuples: (full_path, value_in_want) + """ + diffs = [] + + have = have or {} + + for key, want_val in want.items(): + current_path = f"{path}.{key.replace('_', '-')}" if path else key.replace("_", "-") + + if key not in have: + if isinstance(want_val, dict): + diffs.extend(self._dict_diff(want_val, {}, current_path)) + elif isinstance(want_val, list): + for i, item in enumerate(want_val): + if isinstance(item, dict): + diffs.extend(self._dict_diff(item, {}, f"{current_path}[{i}]")) + else: + diffs.append((f"{current_path}[{i}]", item)) + else: + diffs.append((current_path, want_val)) + + else: + have_val = have[key] + + if isinstance(want_val, dict) and isinstance(have_val, dict): + diffs.extend(self._dict_diff(want_val, have_val, current_path)) + + elif isinstance(want_val, list) and isinstance(have_val, list): + for i, item in enumerate(want_val): + if i >= len(have_val): + diffs.append((f"{current_path}[{i}]", item)) + elif isinstance(item, dict) and isinstance(have_val[i], dict): + diffs.extend( + self._dict_diff(item, have_val[i], f"{current_path}[{i}]"), + ) + elif item != have_val[i]: + diffs.append((f"{current_path}[{i}]", item)) + + elif want_val != have_val: + diffs.append((current_path, want_val)) + + return diffs + + def _render_sources(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for sources (from) + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + have_index = {item["zone"]: item for item in have} + + for item1 in want: + zone = item1["zone"] + + if zone in have_index: + item2 = have_index[zone] + + wfw = item1.get("firewall", {}) + hfw = item2.get("firewall", {}) + if wfw: + if opr: + sources = self._dict_diff(wfw, hfw) + for source in sources: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + source[0].replace("_", "-") + + " " + + source[1], + ) + elif not opr and hfw: + sources = self._dict_diff(wfw, hfw) + for source in sources: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + source[0].replace("_", "-"), + ) + elif not opr and not hfw: + commands.append( + cmd + " " + name + " from " + zone, + ) + elif opr: + wfw = item1.get("firewall", {}) + for key, val in wfw.items(): + if val: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + key.replace("_", "-") + + " " + + val, + ) + elif not opr: + commands.append( + cmd + " " + name + " from " + zone, + ) + return commands diff --git a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py index c74e63d3..e13c1939 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py @@ -77,13 +77,17 @@ class Firewall_globalFacts(object): :rtype: dictionary :returns: The generated config """ + conf = "\n".join( filter( - lambda x: ("firewall ipv6-name" and "firewall name" not in x), + lambda x: not ( + x.startswith("set firewall name") + or x.startswith("set firewall ipv6-name") + or x.startswith("set firewall ipv6 name") + ), conf, ), ) - a_lst = [ "config_trap", "validation", @@ -97,6 +101,7 @@ class Firewall_globalFacts(object): "group": self.parse_group(conf), "route_redirects": self.route_redirects(conf), "state_policy": self.parse_state_policy(conf), + "zone": self.parse_zone(conf), } firewall.update(f_sub) return firewall @@ -400,3 +405,120 @@ class Firewall_globalFacts(object): "twa_hazards_protection", ) return True if attrib in bool_set else False + + def parse_zone(self, conf): + """ + This function triggers the parsing of 'zone' attributes. + :param conf: configuration. + :return: generated config dictionary. + """ + cfg_dict = {} + + KEY_MAP = { + "interface": "interfaces", + "intra-zone-filtering": "intra-zone-filtering", + "from": "sources", + } + + LIST_ATTRS = { + "interfaces", + "intra_zone_filtering", + "sources", + } + + for line in conf.splitlines(): + + m = search( + r"^set firewall zone (?P<zone>\S+)\s+(?P<attr>[a-z-]+)(?:\s+(?P<value>'[^']+'|[^\n]+))?$", + line, + ) + if not m: + continue + + zone_name = m.group("zone") + raw_attr = m.group("attr").replace("-", "_") + value = m.group("value") + + if value is None: + value = True + else: + value = value.strip("'") + + # VyOS 1.5.0 GA wraps 'interface' under a new 'member' node: + # "set firewall zone <name> member interface <ifname>". Unwrap + # it here so it lands in the same 'interfaces' list as the + # pre-1.5.0 bare "interface <ifname>" form. No version check + # needed -- 1.4.x/1.5-rolling configs never emit 'member'. + if raw_attr == "member" and isinstance(value, str) and value.startswith("interface "): + raw_attr = "interface" + value = value.split(None, 1)[1].strip("'") + + zone = cfg_dict.setdefault(zone_name, {"name": zone_name}) + + attr = KEY_MAP.get(raw_attr, raw_attr) + + if attr in LIST_ATTRS: + if attr == "intra_zone_filtering": + izf = zone.setdefault(attr, {}) + izf_attr = self._parse_izf(value) + for k, v in izf_attr.items(): + if isinstance(v, dict): + izf.setdefault(k, {}).update(v) + else: + izf[k] = v + elif attr == "sources": + self._parse_sources(zone, value) + else: + zone.setdefault(attr, []).append(value) + else: + zone[attr] = value + + return list(cfg_dict.values()) + + def _parse_izf(self, value): + + tokens = value.replace("'", "").split() + + result = {} + + key = tokens[0].replace("-", "_") + + if len(tokens) == 2: + result[key] = tokens[1] + + elif len(tokens) >= 3: + subkey = tokens[1].replace("-", "_") + result[key] = {subkey: tokens[2]} + + return result + + def _parse_sources(self, zone, value): + + tokens = value.split() + + if len(tokens) < 1: + return + + src_zone = tokens[0] + + sources = zone.setdefault("sources", []) + + entry = None + for s in sources: + if s.get("zone") == src_zone: + entry = s + break + + if entry is None: + entry = {"zone": src_zone} + sources.append(entry) + + if len(tokens) == 1: + return + + if tokens[1] == "firewall" and len(tokens) >= 4: + key = tokens[2].replace("-", "_") + val = tokens[3].strip("'") + + firewall = entry.setdefault("firewall", {}) + firewall[key] = val |
