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 61b914ec..21d51b60 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 @@ -1,1129 +1,1123 @@ # # -*- coding: utf-8 -*- # Copyright 2019 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ The vyos_firewall_global class It is in this file where the current configuration (as dict) is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( remove_empties, to_list, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( in_target_not_none, list_diff_want_only, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( LooseVersion, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( get_os_version, load_config, ) class Firewall_global(ConfigBase): """ The vyos_firewall_global class """ gather_subset = ["!all", "!min"] gather_network_resources = ["firewall_global"] def __init__(self, module): super(Firewall_global, self).__init__(module) def get_firewall_global_facts(self, data=None): """Get the 'facts' (the current configuration) :rtype: A dictionary :returns: The current configuration as a dictionary """ facts, _warnings = Facts(self._module).get_facts( self.gather_subset, self.gather_network_resources, data=data, ) firewall_global_facts = facts["ansible_network_resources"].get("firewall_global") if not firewall_global_facts: return [] return firewall_global_facts def execute_module(self): """Execute the module :rtype: A dictionary :returns: The result from module execution """ result = {"changed": False} warnings = list() commands = list() try: self._module.params["comment"] except KeyError: comment = [] else: comment = self._module.params["comment"] if self.state in self.ACTION_STATES: existing_firewall_global_facts = self.get_firewall_global_facts() else: existing_firewall_global_facts = [] if self.state in self.ACTION_STATES or self.state == "rendered": commands.extend(self.set_config(existing_firewall_global_facts)) if commands and self._module._diff: commit = not self._module.check_mode diff = load_config(self._module, commands, commit=commit, comment=comment) if diff: result["diff"] = {"prepared": str(diff)} if commands and self.state in self.ACTION_STATES: if not self._module.check_mode: self._connection.edit_config(commands) result["changed"] = True if self.state in self.ACTION_STATES: result["commands"] = commands if self.state in self.ACTION_STATES or self.state == "gathered": changed_firewall_global_facts = self.get_firewall_global_facts() elif self.state == "rendered": result["rendered"] = commands elif self.state == "parsed": running_config = self._module.params["running_config"] if not running_config: self._module.fail_json( msg="value of running_config parameter must not be empty for state parsed", ) result["parsed"] = self.get_firewall_global_facts(data=running_config) else: changed_firewall_global_facts = [] if self.state in self.ACTION_STATES: result["before"] = existing_firewall_global_facts if result["changed"]: result["after"] = changed_firewall_global_facts elif self.state == "gathered": result["gathered"] = changed_firewall_global_facts result["warnings"] = warnings return result def set_config(self, existing_firewall_global_facts): """Collect the configuration from the args passed to the module, collect the current configuration (as a dict from facts) :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ want = self._module.params["config"] have = existing_firewall_global_facts resp = self.set_state(want, have) return to_list(resp) def set_state(self, w, h): """Select the appropriate function based on the state provided :param want: the desired configuration as a dictionary :param have: the current configuration as a dictionary :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ commands = [] if self.state in ("merged", "replaced", "rendered") and not w: self._module.fail_json( msg="value of config parameter must not be empty for state {0}".format(self.state), ) if self.state == "deleted": commands.extend(self._state_deleted(want=None, have=h)) elif w: if self.state == "merged" or self.state == "rendered": commands.extend(self._state_merged(w, h)) elif self.state == "replaced": commands.extend(self._state_replaced(w, h)) return commands def _state_replaced(self, w, h): """The command generator when state is replaced :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ commands = [] if h: commands.extend(self._state_deleted(h, w)) commands.extend(self._state_merged(w, h)) return commands def _state_merged(self, want, have): """The command generator when state is merged :rtype: A list :returns: the commands necessary to merge the provided into the current configuration """ commands = [] commands.extend(self._add_global_attr(want, have)) return commands def _state_deleted(self, want, have): """The command generator when state is deleted :rtype: A list :returns: the commands necessary to remove the current configuration of the provided objects """ commands = [] b_set = ( "config_trap", "validation", "log_martians", "syn_cookies", "twa_hazards_protection", ) if want: for key, val in want.items(): if val and key in b_set and not have: commands.append(self._form_attr_cmd(attr=key, opr=False)) elif val and key in b_set and have and key in have and have[key] != val: commands.append(self._form_attr_cmd(attr=key, opr=False)) else: commands.extend(self._render_attr_config(want, have, key)) elif not want and have: commands.append(self._compute_command(opr=False)) elif have: for key, val in have.items(): if val and key in b_set: commands.append(self._form_attr_cmd(attr=key, opr=False)) else: commands.extend(self._render_attr_config(want, have, key)) return commands def _render_attr_config(self, w, h, key, opr=False): """ This function invoke the function to extend commands based on the key. :param w: the desired configuration. :param h: the current configuration. :param key: attribute name :param opr: operation :return: list of commands """ commands = [] if key == "ping": commands.extend(self._render_ping(key, w, h, opr=opr)) elif key == "group": commands.extend(self._render_group(key, w, h, opr=opr)) elif key == "state_policy": 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): """ This function forms the set/delete commands based on the 'opr' type for firewall_global attributes. :param w: the desired config. :param h: the target config. :param opr: True/False. :return: generated commands list. """ commands = [] w_fg = deepcopy(remove_empties(w)) l_set = ( "config_trap", "validation", "log_martians", "syn_cookies", "twa_hazards_protection", ) if w_fg: for key, val in w_fg.items(): if opr and key in l_set and not (h and self._is_w_same(w_fg, h, key)): commands.append( self._form_attr_cmd(attr=key, val=self._bool_to_str(val), opr=opr), ) elif not opr: if key and self._is_del(l_set, h): commands.append( self._form_attr_cmd(attr=key, key=self._bool_to_str(val), opr=opr), ) continue if key in l_set and not self._in_target(h, key) and not self._is_del(l_set, h): commands.append( self._form_attr_cmd(attr=key, val=self._bool_to_str(val), opr=opr), ) else: commands.extend(self._render_attr_config(w_fg, h, key, opr)) return commands def _render_ping(self, attr, w, h, opr): """ This function forms the commands for 'ping' attributes based on the 'opr'. :param attr: attribute name. :param w: the desired configuration. :param h: the target config. :param opr: True/False. :return: generated list of commands. """ commands = [] h_ping = {} l_set = ("all", "broadcast") if h: h_ping = h.get(attr) or {} if self._is_root_del(w[attr], h_ping, attr): for item, value in h[attr].items(): if not opr and item in l_set: commands.append(self._form_attr_cmd(attr=item, opr=opr)) elif w[attr]: if h and attr in h.keys(): h_ping = h.get(attr) or {} for item, value in w[attr].items(): if ( opr and item in l_set and not (h_ping and self._is_w_same(w[attr], h_ping, item)) ): commands.append( self._form_attr_cmd(attr=item, val=self._bool_to_str(value), opr=opr), ) elif ( not opr and item in l_set and not (h_ping and self._is_w_same(w[attr], h_ping, item)) ): commands.append(self._form_attr_cmd(attr=item, opr=opr)) return commands def _render_group(self, attr, w, h, opr): """ This function forms the commands for 'group' attribute based on the 'opr'. :param attr: attribute name. :param w: base config. :param h: target config. :param opr: True/False. :return: generated list of commands. """ commands = [] h_grp = {} if not opr and self._is_root_del(h, w, attr): commands.append(self._form_attr_cmd(attr=attr, opr=opr)) else: if h: h_grp = h.get("group") or {} if w: commands.extend(self._render_grp_mem("port_group", w["group"], h_grp, opr)) commands.extend(self._render_grp_mem("address_group", w["group"], h_grp, opr)) commands.extend(self._render_grp_mem("network_group", w["group"], h_grp, opr)) return commands def _render_grp_mem(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") 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") if "afi" in want and want["afi"] == "ipv6": cmd = self._compute_command(key="group", attr="ipv6-" + attr, opr=opr) else: cmd = self._compute_command(key="group", attr=attr, opr=opr) 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": commands.append(cmd + " " + str(val)) else: commands.append( cmd + " " + want["name"] + " " + key + " '" + str(want[key]) + "'", ) elif not opr and key in l_set: if key == "name" and self._is_grp_del(h, want, key): if len(commands) > 0 and commands[-1] == cmd + " " + want[ "name" ] + " " + self._grp_type( attr, ): commands.pop() commands.append(cmd + " " + want["name"]) continue 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) elif key == "members": commands.extend( self._render_ports_addrs( key, want, h, opr, cmd, want["name"], attr, ), ) return commands def _render_ports_addrs(self, attr, w, h, opr, cmd, name, type): """ This function forms the commands for port/address/network group members 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: members = list_diff_want_only(want, have) for member in members: commands.append( cmd + " " + name + " " + self._grp_type(type) + " " + member[self._get_mem_type(type)], ) elif not opr and have: members = list_diff_want_only(want, have) for member in members: commands.append( cmd + " " + name + " " + self._grp_type(type) + " " + member[self._get_mem_type(type)], ) elif not opr and not have: commands.append( cmd + " " + name + " " + self._grp_type(type), ) return commands def _get_mem_type(self, group): """ This function returns the member type based on the type of group. """ return "port" if group == "port_group" else "address" def _render_state_policy(self, attr, w, h, opr): """ This function forms the commands for 'state-policy' 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 = [] have = [] if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): l_set = ("log", "action", "connection_type", "log_level") else: l_set = ("log", "action", "connection_type") if not opr and self._is_root_del(h, w, attr): commands.append(self._form_attr_cmd(attr=attr, opr=opr)) else: w_sp = deepcopy(remove_empties(w)) want = w_sp.get(attr) or [] if h: have = h.get(attr) or [] if want: for w in want: h = self.search_attrib_in_have(have, w, "connection_type") for key, val in w.items(): if val and key != "connection_type": if opr and key in l_set and not (h and self._is_w_same(w, h, key)): if key == "log" and LooseVersion( get_os_version(self._module), ) >= LooseVersion("1.4"): commands.append( self._form_attr_cmd( key=attr + " " + w["connection_type"], attr=key, opr=opr, ), ) else: commands.append( self._form_attr_cmd( key=attr + " " + w["connection_type"], attr=key, val=self._bool_to_str(val), opr=opr, ), ) elif not opr and key in l_set: if not h: commands.append( self._form_attr_cmd( attr=attr + " " + w["connection_type"], opr=opr, ), ) break # delete the whole thing and move on if (not self._in_target(h, key) or h[key] is None) and ( self._in_target(w, key) and w[key] ): # delete if not being replaced and value currently exists commands.append( self._form_attr_cmd( attr=attr + " " + w["connection_type"] + " " + key, val=self._bool_to_str(val), opr=opr, ), ) return commands def _render_route_redirects(self, attr, w, h, opr): """ This function forms the commands for 'route_redirects' 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 = [] have = [] l_set = ("afi", "ip_src_route") if w: want = w.get(attr) or [] if h: have = h.get(attr) or [] if want: for w in want: h = self.search_attrib_in_have(have, w, "afi") if "afi" in w: afi = w["afi"] else: if h and "afi" in h: afi = h["afi"] else: afi = None afi = None for key, val in w.items(): if val is not None and key != "afi": if opr and key in l_set and not (h and self._is_w_same(w, h, key)): commands.append( self._form_attr_cmd( attr=key, val=self._bool_to_str(val), opr=opr, type=afi, ), ) elif not opr and key in l_set: if self._is_del(l_set, h): commands.append( self._form_attr_cmd( attr=key, val=self._bool_to_str(val), opr=opr, type=afi, ), ) continue if not (h and self._in_target(h, key)) and not self._is_del(l_set, h): commands.append( self._form_attr_cmd( attr=key, val=self._bool_to_str(val), opr=opr, type=afi, ), ) elif key == "icmp_redirects": commands.extend(self._render_icmp_redirects(key, w, h, opr)) return commands def _render_icmp_redirects(self, attr, w, h, opr): """ This function forms the commands for 'icmp_redirects' 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_red = {} l_set = ("send", "receive") if w and "afi" in w: afi = w["afi"] else: if h and "afi" in h: afi = h["afi"] else: afi = None if w[attr]: if h and attr in h.keys(): h_red = h.get(attr) or {} for item, value in w[attr].items(): if opr and item in l_set and not (h_red and self._is_w_same(w[attr], h_red, item)): commands.append( self._form_attr_cmd( attr=item, val=self._bool_to_str(value), opr=opr, type=afi, ), ) elif ( not opr and item in l_set and not (h_red and self._is_w_same(w[attr], h_red, item)) ): commands.append(self._form_attr_cmd(attr=item, opr=opr, type=afi)) return commands def search_attrib_in_have(self, have, want, attr): """ This function returns the attribute if it is present in target config. :param have: the target config. :param want: the desired config. :param attr: attribute name . :return: attribute/None """ if have: for h in have: if h[attr] == want[attr]: return h return None def _form_attr_cmd(self, key=None, attr=None, val=None, opr=True, type=None): """ This function forms the command for leaf attribute. :param key: parent key. :param attr: attribute name :param value: value :param opr: True/False. :param type: AF type of attribute. :return: generated command. """ command = self._compute_command( key=key, attr=self._map_attrib(attr, type=type), val=val, opr=opr, ) return command def _compute_command(self, key=None, attr=None, val=None, remove=False, opr=True): """ This function construct the add/delete command based on passed attributes. :param key: parent key. :param attr: attribute name :param value: value :param remove: True/False. :param opr: True/False. :return: generated command. """ if remove or not opr: cmd = "delete firewall " else: cmd = "set firewall " if ( attr and key not in ["group", "zone"] and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") ): cmd += "global-options " if key: cmd += key.replace("_", "-") + " " if attr: cmd += attr.replace("_", "-") if val and opr: if key == "state_policy" and LooseVersion(get_os_version(self._module)) >= LooseVersion( "1.4", ): cmd += "" else: cmd += " '" + str(val) + "'" return cmd.strip() def _bool_to_str(self, val): """ This function converts the bool value into string. :param val: bool value. :return: enable/disable. """ return "enable" if str(val) == "True" else "disable" if str(val) == "False" else val def _grp_type(self, val): """ This function returns the group member type based on value argument. :param val: value. :return: member type. """ return ( "address" if val == "address_group" else "network" if val == "network_group" else "port" ) def _is_w_same(self, w, h, key): """ This function checks whether the key value is same in desired and target config dictionary. :param w: base config. :param h: target config. :param key:attribute name. :return: True/False. """ return True if h and key in h and h[key] == w[key] else False def _in_target(self, h, key): """ This function checks whether the target exist and key present in target config. :param h: target config. :param key: attribute name. :return: True/False. """ return True if h and key in h else False def _is_grp_del(self, w, h, key): """ This function checks whether group needed to be deleted based on desired and target configs. :param w: the desired config. :param h: the target config. :param key: group name. :return: True/False. """ return True if h and key in h and (not w or key not in w or not w[key]) else False def _is_root_del(self, w, h, key): """ This function checks whether a root attribute which can have further child attributes needed to be deleted. :param w: the desired config. :param h: the target config. :param key: attribute name. :return: True/False. """ return True if h and key in h and (not w or key not in w or not w[key]) else False def _is_del(self, b_set, h, key="number"): """ This function checks whether attribute needs to be deleted when operation is false and attribute present in present target config. :param b_set: attribute set. :param h: target config. :param key: number. :return: True/False. """ return key in b_set and not self._in_target(h, key) def _map_attrib(self, attrib, type=None): """ - This function construct the regex string. - replace the underscore with hyphen. :param attrib: attribute :return: regex string """ regex = attrib.replace("_", "-") if attrib == "send": if type == "ipv6": regex = "ipv6-send-redirects" else: regex = "send-redirects" elif attrib == "ip_src_route": if type == "ipv6": regex = "ipv6-src-route" elif attrib == "receive": if type == "ipv6": regex = "ipv6-receive-redirects" else: regex = "receive-redirects" elif attrib == "disabled": regex = "disable" elif attrib == "all": regex = "all-ping" elif attrib == "broadcast": regex = "broadcast-ping" 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 [] if want: if opr: interfaces = list_diff_want_only(want, have) for interface in interfaces: commands.append( cmd + " " + name + " interface " + interface, ) elif not opr and have: interfaces = list_diff_want_only(want, have) for interface in interfaces: commands.append( cmd + " " + name + " interface " + interface, ) elif not opr and not have: for interface in want: commands.append( cmd + " " + name + " interface " + 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(".", " ") - + " " - + izf[1], + 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("_", "-") - + " " - + source[1], - ) - elif not opr and not have: - sources = self._dict_diff(wfw, hfw) - for source in sources: - commands.append( - cmd + " " + name + " from " + zone, + + 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 d55c44b5..29784e26 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 @@ -1,513 +1,513 @@ # # -*- coding: utf-8 -*- # Copyright 2019 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ The vyos firewall_global fact class It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy from re import M, findall, search from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.firewall_global.firewall_global import ( Firewall_globalArgs, ) class Firewall_globalFacts(object): """The vyos firewall_global fact class""" def __init__(self, module, subspec="config", options="options"): self._module = module self.argument_spec = Firewall_globalArgs.argument_spec spec = deepcopy(self.argument_spec) if subspec: if options: facts_argument_spec = spec[subspec][options] else: facts_argument_spec = spec[subspec] else: facts_argument_spec = spec self.generated_spec = utils.generate_dict(facts_argument_spec) def get_device_data(self, connection): return connection.get_config() def populate_facts(self, connection, ansible_facts, data=None): """Populate the facts for firewall_global :param connection: the device connection :param ansible_facts: Facts dictionary :param data: previously collected conf :rtype: dictionary :returns: facts """ if not data: # typically data is populated from the current device configuration # data = connection.get('show running-config | section ^interface') # using mock data instead data = self.get_device_data(connection) objs = {} firewalls = findall(r"^set firewall .*$", data, M) if firewalls: objs = self.render_config(firewalls) facts = {} params = utils.validate_config(self.argument_spec, {"config": objs}) facts["firewall_global"] = utils.remove_empties(params["config"]) ansible_facts["ansible_network_resources"].update(facts) return ansible_facts def render_config(self, conf): """ Render config as dictionary structure and delete keys from spec for null values :param spec: The facts tree, generated from the argspec :param conf: The configuration :rtype: dictionary :returns: The generated config """ conf = "\n".join( filter( lambda x: not ( - x.startswith("set firewall ipv6 name") or x.startswith("set firewall ipv6 name") + x.startswith("set firewall name") or x.startswith("set firewall ipv6 name") ), conf, ), ) a_lst = [ "config_trap", "validation", "log_martians", "syn_cookies", "twa_hazards_protection", ] firewall = self.parse_attr(conf, a_lst) f_sub = { "ping": self.parse_ping(conf), "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 def route_redirects(self, conf): """ This function forms the regex to fetch the afi and invoke functions to fetch route redirects and source routes :param conf: configuration data. :return: generated rule list configuration. """ rr_lst = [] v6_attr = findall( r"^set firewall (?:global-options )?(?:ipv6-src-route|ipv6-receive-redirects) (\S+)", conf, M, ) if v6_attr: obj = self.parse_rr_attrib(conf, "ipv6") if obj: rr_lst.append(obj) v4_attr = findall( r"^set firewall (?:global-options )?(?:ip-src-route|receive-redirects|send-redirects) (\S+)", conf, M, ) if v4_attr: obj = self.parse_rr_attrib(conf, "ipv4") if obj: rr_lst.append(obj) return rr_lst def parse_rr_attrib(self, conf, attrib=None): """ This function fetches the 'ip_src_route' invoke function to parse icmp redirects. :param conf: configuration to be parsed. :param attrib: 'ipv4/ipv6'. :return: generated config dictionary. """ cfg_dict = self.parse_attr(conf, ["ip_src_route"], type=attrib) cfg_dict["icmp_redirects"] = self.parse_icmp_redirects(conf, attrib) cfg_dict["afi"] = attrib return cfg_dict def parse_icmp_redirects(self, conf, attrib=None): """ This function triggers the parsing of 'icmp_redirects' attributes. :param conf: configuration to be parsed. :param attrib: 'ipv4/ipv6'. :return: generated config dictionary. """ a_lst = ["send", "receive"] cfg_dict = self.parse_attr(conf, a_lst, type=attrib) return cfg_dict def parse_ping(self, conf): """ This function triggers the parsing of 'ping' attributes. :param conf: configuration to be parsed. :return: generated config dictionary. """ a_lst = ["all", "broadcast"] cfg_dict = self.parse_attr(conf, a_lst) return cfg_dict def parse_state_policy(self, conf): """ This function fetched the connecton type and invoke function to parse other state-policy attributes. :param conf: configuration data. :return: generated rule list configuration. """ sp_lst = [] policies = findall(r"^set firewall (?:global-options )?state-policy (\S+)", conf, M) policies = list(set(policies)) # remove redundancies if policies: rules_lst = [] for sp in set(policies): sp_regex = r"^set firewall (?:global-options )?state-policy %s .+$" % sp cfg = "\n".join(findall(sp_regex, conf, M)) obj = self.parse_policies(cfg, sp) obj["connection_type"] = sp if obj: rules_lst.append(obj) sp_lst = sorted(rules_lst, key=lambda i: i["connection_type"]) return sp_lst def parse_policies(self, conf, attrib=None): """ This function triggers the parsing of policy attributes action and log. :param conf: configuration :param attrib: connection type. :return: generated rule configuration dictionary. """ a_lst = ["action", "log", "log_level"] cfg_dict = self.parse_attr(conf, a_lst, match=attrib) return cfg_dict def parse_group(self, conf): """ This function triggers the parsing of 'group' attributes. :param conf: configuration. :return: generated config dictionary. """ cfg_dict = {} cfg_dict["port_group"] = self.parse_group_lst(conf, "port-group", False) cfg_dict["address_group"] = self.parse_group_lst( conf, "address-group", ) + self.parse_group_lst(conf, "ipv6-address-group") cfg_dict["network_group"] = self.parse_group_lst( conf, "network-group", ) + self.parse_group_lst(conf, "ipv6-network-group") return cfg_dict def parse_group_lst(self, conf, type, include_afi=True): """ This function fetches the name of group and invoke function to parse group attributes'. :param conf: configuration data. :param type: type of group. :param include_afi: if the afi should be included in the parsed object :return: generated group list configuration. """ g_lst = [] groups = findall(r"^set firewall group " + type + " (\\S+)", conf, M) if groups: rules_lst = [] for gr in set(groups): gr_regex = r"^set firewall group " + type + " %s .+$" % gr cfg = "\n".join(findall(gr_regex, conf, M)) if "ipv6" in type: # fmt: off obj = self.parse_groups(cfg, type[len("ipv6-"):], gr) # fmt: on if include_afi: obj["afi"] = "ipv6" else: obj = self.parse_groups(cfg, type, gr) if include_afi: obj["afi"] = "ipv4" obj["name"] = gr.strip("'") if obj: rules_lst.append(obj) g_lst = sorted(rules_lst, key=lambda i: i["name"]) return g_lst def parse_groups(self, conf, type, name): """ This function fetches the description and invoke the parsing of group members. :param conf: configuration. :param type: type of group. :param name: name of group. :return: generated configuration dictionary. """ a_lst = ["name", "description"] group = self.parse_attr(conf, a_lst) key = self.get_key(type) r_sub = {key[0]: self.parse_address_port_lst(conf, name, key[1])} group.update(r_sub) return group def parse_address_port_lst(self, conf, name, key): """ This function forms the regex to fetch the group members attributes. :param conf: configuration data. :param name: name of group. :param key: key value. :return: generated member list configuration. """ l_lst = [] attribs = findall(r"^.*" + name + " " + key + " (\\S+)", conf, M) if attribs: for attr in attribs: if key == "port": l_lst.append({"port": attr.strip("'")}) else: l_lst.append({"address": attr.strip("'")}) return l_lst def parse_attr(self, conf, attr_list, match=None, type=None): """ This function peforms the following: - Form the regex to fetch the required attribute config. - Type cast the output in desired format. :param conf: configuration. :param attr_list: list of attributes. :param match: parent node/attribute name. :return: generated config dictionary. """ config = {} for attrib in attr_list: regex = self.map_regex(attrib, type) if match: regex = match + " " + regex if conf: if self.is_bool(attrib): # fancy regex to make sure we don't get a substring out = search(r"^.*" + regex + r"( 'disable')?(?=\s|$)", conf, M) if out: if out.group(1): config[attrib] = False else: config[attrib] = True else: out = search(r"^.*" + regex + r" (.+)", conf, M) if out: val = out.group(1).strip("'") if self.is_num(attrib): val = int(val) config[attrib] = val return config def get_key(self, type): """ This function map the group type to member type :param type: :return: """ key = () if type == "port-group": key = ("members", "port") elif type == "address-group": key = ("members", "address") elif type == "network-group": key = ("members", "network") return key def map_regex(self, attrib, type=None): """ - This function construct the regex string. - replace the underscore with hyphen. :param attrib: attribute :return: regex string """ regex = attrib.replace("_", "-") if attrib == "all": regex = "all-ping" elif attrib == "disabled": regex = "disable" elif attrib == "broadcast": regex = "broadcast-ping" elif attrib == "send": if type == "ipv6": regex = "ipv6-send-redirects" else: regex = "send-redirects" elif attrib == "ip_src_route": if type == "ipv6": regex = "ipv6-src-route" elif attrib == "receive": if type == "ipv6": regex = "ipv6-receive-redirects" else: regex = "receive-redirects" return regex def is_num(self, attrib): """ This function looks for the attribute in predefined integer type set. :param attrib: attribute. :return: True/false. """ num_set = ("time", "code", "type", "count", "burst", "number") return True if attrib in num_set else False def get_src_route(self, attrib): """ This function looks for the attribute in predefined integer type set. :param attrib: attribute. :return: True/false. """ return "ipv6_src_route" if attrib == "ipv6" else "ip_src_route" def is_bool(self, attrib): """ This function looks for the attribute in predefined bool type set. :param attrib: attribute. :return: True/False """ bool_set = ( "all", "log", "send", "receive", "broadcast", "config_trap", "log_martians", "syn_cookies", "ip_src_route", "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\S+)\s+(?P[a-z-]+)(?:\s+(?P'[^']+'|[^\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("'") 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 diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg index c883ca78..42bea69b 100644 --- a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg +++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg @@ -1,24 +1,27 @@ set firewall group address-group A-EMPTY set firewall group address-group RND-HOSTS address 192.0.2.1 set firewall group address-group RND-HOSTS address 192.0.2.3 set firewall group address-group RND-HOSTS address 192.0.2.5 set firewall group address-group RND-HOSTS description 'This group has the Management hosts address lists' set firewall group address-group DELETE-HOSTS address 1.2.3.4 set firewall group address-group DELETE-HOSTS description 'The (single) last address from this group will be deleted in the tests' set firewall group ipv6-address-group LOCAL-v6 address ::1 set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1 set firewall group ipv6-address-group LOCAL-v6 description 'This group has the hosts address lists of this machine' set firewall group network-group RND network 192.0.2.0/24 set firewall group network-group RND description 'This group has the Management network addresses' set firewall group ipv6-network-group UNIQUE-LOCAL-v6 network fc00::/7 set firewall group ipv6-network-group UNIQUE-LOCAL-v6 description 'This group encompasses the ULA address space in IPv6' set firewall group port-group SSH port 22 set firewall group port-group SSH description 'This group has the ssh ports' set firewall global-options all-ping enable set firewall global-options state-policy related action 'accept' set firewall global-options state-policy related log-level 'alert' set firewall global-options ipv6-src-route 'enable' set firewall global-options send-redirects 'enable' set firewall zone ZONE-TEST interface 'eth0.1234' set firewall zone ZONE-TEST description 'zone-test test description' set firewall group address-group ZONE-TEST address '1.2.3.4' +set firewall zone ZONE-IZF description 'zone for izf and sources delete path testing' +set firewall zone ZONE-IZF intra-zone-filtering action 'drop' +set firewall zone ZONE-IZF from zone-src firewall name 'existing-ruleset' diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py index 4fd455c3..295d6b5a 100644 --- a/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py @@ -1,610 +1,1007 @@ # (c) 2016 Red Hat Inc. # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # Make coding more python3-ish from __future__ import absolute_import, division, print_function __metaclass__ = type from unittest.mock import patch from ansible_collections.vyos.vyos.plugins.modules import vyos_firewall_global from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args from .vyos_module import TestVyosModule, load_fixture class TestVyosFirewallRulesModule14(TestVyosModule): module = vyos_firewall_global def setUp(self): super(TestVyosFirewallRulesModule14, self).setUp() self.mock_get_config = patch( "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.get_config", ) self.get_config = self.mock_get_config.start() self.mock_load_config = patch( "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.load_config", ) self.load_config = self.mock_load_config.start() self.mock_get_resource_connection_config = patch( "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base.get_resource_connection", ) self.get_resource_connection_config = self.mock_get_resource_connection_config.start() self.mock_get_resource_connection_facts = patch( "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection", ) self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() self.mock_execute_show_command = patch( "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_global.firewall_global.Firewall_globalFacts.get_device_data", ) self.mock_get_os_version = patch( "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.firewall_global.firewall_global.get_os_version", ) self.get_os_version = self.mock_get_os_version.start() self.get_os_version.return_value = "1.4" self.execute_show_command = self.mock_execute_show_command.start() self.maxDiff = None def tearDown(self): super(TestVyosFirewallRulesModule14, self).tearDown() self.mock_get_resource_connection_config.stop() self.mock_get_resource_connection_facts.stop() self.mock_get_config.stop() self.mock_load_config.stop() self.mock_execute_show_command.stop() self.mock_get_os_version.stop() def load_fixtures(self, commands=None, filename=None): def load_from_file(*args, **kwargs): return load_fixture("vyos_firewall_global_config_v14.cfg") self.execute_show_command.side_effect = load_from_file def test_vyos_firewall_global_set_01_merged(self): set_module_args( dict( config=dict( validation="strict", config_trap=True, log_martians=True, syn_cookies=True, twa_hazards_protection=True, ping=dict(all=True, broadcast=True), state_policy=[ dict( connection_type="established", action="accept", log=True, log_level="emerg", ), dict(connection_type="invalid", action="reject"), ], route_redirects=[ dict( afi="ipv4", ip_src_route=True, icmp_redirects=dict(send=True, receive=False), ), dict( afi="ipv6", ip_src_route=False, icmp_redirects=dict(receive=False), ), ], group=dict( address_group=[ dict( afi="ipv4", name="MGMT-HOSTS", description="This group has the Management hosts address lists", members=[ dict(address="192.0.1.1"), dict(address="192.0.1.3"), dict(address="192.0.1.5"), ], ), dict( afi="ipv6", name="GOOGLE-DNS-v6", members=[ dict(address="2001:4860:4860::8888"), dict(address="2001:4860:4860::8844"), ], ), ], network_group=[ dict( afi="ipv4", name="MGMT", description="This group has the Management network addresses", members=[dict(address="192.0.1.0/24")], ), dict( afi="ipv6", name="DOCUMENTATION-v6", description="IPv6 Addresses reserved for documentation per RFC 3849", members=[ dict(address="2001:0DB8::/32"), dict(address="3FFF:FFFF::/32"), ], ), ], port_group=[ dict( name="TELNET", description="This group has the telnet ports", members=[dict(port="23")], ), ], ), zone=[ dict( name="FZP-2", default_action="reject", default_log=True, description="This is the Firewall zone fzp2", interfaces=[ "eth2", "lo0", ], local_zone=True, sources=[ dict( zone="fzp5", firewall=dict( name="fzp1", ipv6_name="fzp1-v6", ), ), ], intra_zone_filtering=dict( action="accept", ), ), ], ), state="merged", ), ) commands = [ "set firewall group address-group MGMT-HOSTS address 192.0.1.1", "set firewall group address-group MGMT-HOSTS address 192.0.1.3", "set firewall group address-group MGMT-HOSTS address 192.0.1.5", "set firewall group address-group MGMT-HOSTS description 'This group has the Management hosts address lists'", "set firewall group address-group MGMT-HOSTS", "set firewall group ipv6-address-group GOOGLE-DNS-v6 address 2001:4860:4860::8888", "set firewall group ipv6-address-group GOOGLE-DNS-v6 address 2001:4860:4860::8844", "set firewall group ipv6-address-group GOOGLE-DNS-v6", "set firewall group network-group MGMT network 192.0.1.0/24", "set firewall group network-group MGMT description 'This group has the Management network addresses'", "set firewall group network-group MGMT", "set firewall group ipv6-network-group DOCUMENTATION-v6 network 2001:0DB8::/32", "set firewall group ipv6-network-group DOCUMENTATION-v6 network 3FFF:FFFF::/32", "set firewall group ipv6-network-group DOCUMENTATION-v6 description 'IPv6 Addresses reserved for documentation per RFC 3849'", "set firewall group ipv6-network-group DOCUMENTATION-v6", "set firewall group port-group TELNET port 23", "set firewall group port-group TELNET description 'This group has the telnet ports'", "set firewall group port-group TELNET", "set firewall global-options ip-src-route 'enable'", "set firewall global-options receive-redirects 'disable'", "set firewall global-options config-trap 'enable'", "set firewall global-options ipv6-src-route 'disable'", "set firewall global-options ipv6-receive-redirects 'disable'", "set firewall global-options state-policy established action 'accept'", "set firewall global-options state-policy established log", "set firewall global-options state-policy established log-level 'emerg'", "set firewall global-options state-policy invalid action 'reject'", "set firewall global-options broadcast-ping 'enable'", "set firewall global-options log-martians 'enable'", "set firewall global-options twa-hazards-protection 'enable'", "set firewall global-options syn-cookies 'enable'", "set firewall global-options source-validation 'strict'", "set firewall zone FZP-2 default-action 'reject'", "set firewall zone FZP-2 default-log", "set firewall zone FZP-2 description 'This is the Firewall zone fzp2'", "set firewall zone FZP-2 from fzp5 firewall ipv6-name fzp1-v6", "set firewall zone FZP-2 from fzp5 firewall name fzp1", "set firewall zone FZP-2 interface eth2", "set firewall zone FZP-2 interface lo0", "set firewall zone FZP-2 intra-zone-filtering action accept", "set firewall zone FZP-2 local-zone", ] self.execute_module(changed=True, commands=commands) def test_vyos_firewall_global_set_01_merged_idem(self): set_module_args( dict( config=dict( group=dict( address_group=[ dict( afi="ipv4", name="A-EMPTY", ), dict( afi="ipv4", name="RND-HOSTS", description="This group has the Management hosts address lists", members=[ dict(address="192.0.2.1"), dict(address="192.0.2.3"), dict(address="192.0.2.5"), ], ), dict( afi="ipv6", name="LOCAL-v6", description="This group has the hosts address lists of this machine", members=[ dict(address="::1"), dict(address="fdec:2503:89d6:59b3::1"), ], ), ], network_group=[ dict( afi="ipv4", name="RND", description="This group has the Management network addresses", members=[dict(address="192.0.2.0/24")], ), dict( afi="ipv6", name="UNIQUE-LOCAL-v6", description="This group encompasses the ULA address space in IPv6", members=[dict(address="fc00::/7")], ), ], port_group=[ dict( name="SSH", description="This group has the ssh ports", members=[dict(port="22")], ), ], ), ), state="merged", ), ) self.execute_module(changed=False, commands=[]) def test_vyos_firewall_global_set_01_replaced(self): set_module_args( dict( config=dict( state_policy=[ dict(connection_type="invalid", action="reject"), ], group=dict( address_group=[ dict( afi="ipv4", name="RND-HOSTS", description="This group has the Management hosts address lists", members=[ dict(address="192.0.2.1"), dict(address="192.0.2.7"), dict(address="192.0.2.9"), ], ), dict( afi="ipv4", name="DELETE-HOSTS", description="The (single) last address from this group will be deleted in the tests", # No members here ), dict( afi="ipv6", name="LOCAL-v6", description="This group has the hosts address lists of this machine", members=[ dict(address="::1"), dict(address="fdec:2503:89d6:59b3::2"), ], ), dict( afi="ipv4", name="ZONE-TEST", members=[ dict(address="1.2.3.4"), ], description="This is a new description for a address group name that is also in a zone", ), ], network_group=[ dict( afi="ipv4", name="RND", # Deleted the description here. members=[dict(address="192.0.2.0/24")], ), dict( afi="ipv6", name="UNIQUE-LOCAL-v6", description="This group encompasses the ULA address space in IPv6", members=[dict(address="fc00::/7")], ), ], port_group=[ dict( name="SSH", description="This group has the ssh ports", members=[dict(port="2222")], ), ], ), zone=[ dict( name="FZP-2", default_action="drop", default_log=True, description="This is the Firewall zone fzp2", interfaces=[ "eth2", "lo0", ], local_zone=True, sources=[ dict( zone="fzp5", firewall=dict( name="fzp1", ipv6_name="fzp1-v6", ), ), ], intra_zone_filtering=dict( action="accept", ), ), ], ), state="replaced", ), ) commands = [ "delete firewall group address-group A-EMPTY", "delete firewall group address-group RND-HOSTS address 192.0.2.3", "delete firewall group address-group RND-HOSTS address 192.0.2.5", "delete firewall group address-group DELETE-HOSTS address", "delete firewall global-options all-ping", "delete firewall global-options state-policy related", "delete firewall global-options ipv6-src-route", "delete firewall global-options send-redirects", + "delete firewall zone ZONE-IZF", "set firewall global-options state-policy invalid action 'reject'", "set firewall group address-group RND-HOSTS address 192.0.2.7", "set firewall group address-group RND-HOSTS address 192.0.2.9", "set firewall group address-group ZONE-TEST description 'This is a new description for a address group name that is also in a zone'", "delete firewall zone ZONE-TEST", "delete firewall group network-group RND description", "delete firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1", "set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::2", "delete firewall group port-group SSH port 22", "set firewall group port-group SSH port 2222", "set firewall zone FZP-2 default-action 'drop'", "set firewall zone FZP-2 default-log", "set firewall zone FZP-2 description 'This is the Firewall zone fzp2'", "set firewall zone FZP-2 from fzp5 firewall ipv6-name fzp1-v6", "set firewall zone FZP-2 from fzp5 firewall name fzp1", "set firewall zone FZP-2 interface eth2", "set firewall zone FZP-2 interface lo0", "set firewall zone FZP-2 intra-zone-filtering action accept", "set firewall zone FZP-2 local-zone", ] self.execute_module(changed=True, commands=commands) def test_vyos_firewall_global_set_01_replaced_idem(self): set_module_args( dict( config=dict( ping=dict(all=True), route_redirects=[ dict(ip_src_route=True, afi="ipv6"), dict(icmp_redirects=dict(send=True), afi="ipv4"), ], state_policy=[ dict(connection_type="related", action="accept", log_level="alert"), ], group=dict( address_group=[ dict( afi="ipv4", name="A-EMPTY", ), dict( afi="ipv4", name="RND-HOSTS", description="This group has the Management hosts address lists", members=[ dict(address="192.0.2.1"), dict(address="192.0.2.3"), dict(address="192.0.2.5"), ], ), dict( afi="ipv4", name="DELETE-HOSTS", description="The (single) last address from this group will be deleted in the tests", members=[ dict(address="1.2.3.4"), ], ), dict( afi="ipv4", name="ZONE-TEST", members=[ dict(address="1.2.3.4"), ], ), dict( afi="ipv6", name="LOCAL-v6", description="This group has the hosts address lists of this machine", members=[ dict(address="::1"), dict(address="fdec:2503:89d6:59b3::1"), ], ), ], network_group=[ dict( afi="ipv4", name="RND", description="This group has the Management network addresses", members=[dict(address="192.0.2.0/24")], ), dict( afi="ipv6", name="UNIQUE-LOCAL-v6", description="This group encompasses the ULA address space in IPv6", members=[dict(address="fc00::/7")], ), ], port_group=[ dict( name="SSH", description="This group has the ssh ports", members=[dict(port="22")], ), ], ), zone=[ dict( name="ZONE-TEST", description="zone-test test description", - interfaces=[ - "eth0.1234", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="drop"), + sources=[ + dict( + zone="zone-src", + firewall=dict(name="existing-ruleset"), + ), ], ), ], ), state="replaced", ), ) self.execute_module(changed=False, commands=[]) def test_vyos_firewall_global_set_02_replaced(self): set_module_args( dict( config=dict( state_policy=[ dict(connection_type="invalid", action="reject"), dict(connection_type="related", action="drop"), ], group=dict( address_group=[ dict( afi="ipv4", name="RND-HOSTS", description="This group has the Management hosts address lists", members=[ dict(address="192.0.2.1"), dict(address="192.0.2.7"), dict(address="192.0.2.9"), ], ), dict( afi="ipv6", name="LOCAL-v6", description="This group has the hosts address lists of this machine", members=[ dict(address="::1"), dict(address="fdec:2503:89d6:59b3::2"), ], ), dict( afi="ipv4", name="ZONE-TEST", members=[ dict(address="4.3.2.1"), ], description="This is a new description for a address group name that is also in a zone", ), ], network_group=[ dict( afi="ipv4", name="RND", description="This group has the Management network addresses", members=[dict(address="192.0.2.0/24")], ), dict( afi="ipv6", name="UNIQUE-LOCAL-v6", description="This group encompasses the ULA address space in IPv6", members=[dict(address="fc00::/7")], ), ], port_group=[ dict( name="SSH", description="This group has the ssh ports", members=[dict(port="2222")], ), ], ), zone=[ dict( name="ZONE-TEST", description="zone-test test description", interfaces=[ "lo", ], ), ], ), state="replaced", ), ) commands = [ "delete firewall group address-group A-EMPTY", "delete firewall group address-group RND-HOSTS address 192.0.2.3", "delete firewall group address-group RND-HOSTS address 192.0.2.5", "delete firewall group address-group ZONE-TEST address 1.2.3.4", + "delete firewall zone ZONE-IZF", "delete firewall global-options all-ping", "delete firewall global-options ipv6-src-route", "delete firewall global-options send-redirects", "set firewall global-options state-policy related action 'drop'", "delete firewall global-options state-policy related log-level", "delete firewall group address-group DELETE-HOSTS", "set firewall global-options state-policy invalid action 'reject'", "set firewall group address-group RND-HOSTS address 192.0.2.7", "set firewall group address-group RND-HOSTS address 192.0.2.9", "set firewall group address-group ZONE-TEST address 4.3.2.1", "delete firewall zone ZONE-TEST interface eth0.1234", "set firewall zone ZONE-TEST interface lo", "set firewall group address-group ZONE-TEST description 'This is a new description for a address group name that is also in a zone'", "delete firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1", "set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::2", "delete firewall group port-group SSH port 22", "set firewall group port-group SSH port 2222", ] self.execute_module(changed=True, commands=commands) def test_vyos_firewall_global_set_01_deleted(self): set_module_args(dict(config=dict(), state="deleted")) commands = ["delete firewall"] self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_03_replaced_izf_action_change(self): + set_module_args( + dict( + config=dict( + ping=dict(all=True), + route_redirects=[ + dict(ip_src_route=True, afi="ipv6"), + dict(icmp_redirects=dict(send=True), afi="ipv4"), + ], + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + group=dict( + address_group=[ + dict(afi="ipv4", name="A-EMPTY"), + dict( + afi="ipv4", + name="RND-HOSTS", + description="This group has the Management hosts address lists", + members=[ + dict(address="192.0.2.1"), + dict(address="192.0.2.3"), + dict(address="192.0.2.5"), + ], + ), + dict( + afi="ipv4", + name="DELETE-HOSTS", + description="The (single) last address from this group will be deleted in the tests", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv4", + name="ZONE-TEST", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv6", + name="LOCAL-v6", + description="This group has the hosts address lists of this machine", + members=[ + dict(address="::1"), + dict(address="fdec:2503:89d6:59b3::1"), + ], + ), + ], + network_group=[ + dict( + afi="ipv4", + name="RND", + description="This group has the Management network addresses", + members=[dict(address="192.0.2.0/24")], + ), + dict( + afi="ipv6", + name="UNIQUE-LOCAL-v6", + description="This group encompasses the ULA address space in IPv6", + members=[dict(address="fc00::/7")], + ), + ], + port_group=[ + dict( + name="SSH", + description="This group has the ssh ports", + members=[dict(port="22")], + ), + ], + ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="accept"), # was 'drop' + sources=[ + dict( + zone="zone-src", + firewall=dict(name="existing-ruleset"), + ), + ], + ), + ], + ), + state="replaced", + ), + ) + commands = [ + "delete firewall zone ZONE-IZF intra-zone-filtering action", + "set firewall zone ZONE-IZF intra-zone-filtering action accept", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_04_replaced_sources_value_change(self): + # ZONE-IZF.sources[0].firewall.name changes from 'existing-ruleset' + # (fixture) to 'new-ruleset'. Exercises _render_sources' `not opr + # and hfw` (delete) branch. Pre-fix this leaked the old value into + # the delete command. + set_module_args( + dict( + config=dict( + ping=dict(all=True), + route_redirects=[ + dict(ip_src_route=True, afi="ipv6"), + dict(icmp_redirects=dict(send=True), afi="ipv4"), + ], + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + group=dict( + address_group=[ + dict(afi="ipv4", name="A-EMPTY"), + dict( + afi="ipv4", + name="RND-HOSTS", + description="This group has the Management hosts address lists", + members=[ + dict(address="192.0.2.1"), + dict(address="192.0.2.3"), + dict(address="192.0.2.5"), + ], + ), + dict( + afi="ipv4", + name="DELETE-HOSTS", + description="The (single) last address from this group will be deleted in the tests", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv4", + name="ZONE-TEST", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv6", + name="LOCAL-v6", + description="This group has the hosts address lists of this machine", + members=[ + dict(address="::1"), + dict(address="fdec:2503:89d6:59b3::1"), + ], + ), + ], + network_group=[ + dict( + afi="ipv4", + name="RND", + description="This group has the Management network addresses", + members=[dict(address="192.0.2.0/24")], + ), + dict( + afi="ipv6", + name="UNIQUE-LOCAL-v6", + description="This group encompasses the ULA address space in IPv6", + members=[dict(address="fc00::/7")], + ), + ], + port_group=[ + dict( + name="SSH", + description="This group has the ssh ports", + members=[dict(port="22")], + ), + ], + ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="drop"), + sources=[ + dict( + zone="zone-src", + firewall=dict(name="new-ruleset"), # was 'existing-ruleset' + ), + ], + ), + ], + ), + state="replaced", + ), + ) + commands = [ + "delete firewall zone ZONE-IZF from zone-src firewall name", + "set firewall zone ZONE-IZF from zone-src firewall name new-ruleset", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_05_replaced_sources_firewall_cleared(self): + # ZONE-IZF.sources[0] keeps the 'zone-src' key but drops the + # 'firewall' sub-dict entirely. Exercises _render_sources' `not opr + # and not hfw` branch -- unreachable before the fix, since it was + # guarded by `not have` instead of `not hfw`, and `have` can never + # be empty here (we're already inside `if zone in have_index`). + set_module_args( + dict( + config=dict( + ping=dict(all=True), + route_redirects=[ + dict(ip_src_route=True, afi="ipv6"), + dict(icmp_redirects=dict(send=True), afi="ipv4"), + ], + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + group=dict( + address_group=[ + dict(afi="ipv4", name="A-EMPTY"), + dict( + afi="ipv4", + name="RND-HOSTS", + description="This group has the Management hosts address lists", + members=[ + dict(address="192.0.2.1"), + dict(address="192.0.2.3"), + dict(address="192.0.2.5"), + ], + ), + dict( + afi="ipv4", + name="DELETE-HOSTS", + description="The (single) last address from this group will be deleted in the tests", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv4", + name="ZONE-TEST", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv6", + name="LOCAL-v6", + description="This group has the hosts address lists of this machine", + members=[ + dict(address="::1"), + dict(address="fdec:2503:89d6:59b3::1"), + ], + ), + ], + network_group=[ + dict( + afi="ipv4", + name="RND", + description="This group has the Management network addresses", + members=[dict(address="192.0.2.0/24")], + ), + dict( + afi="ipv6", + name="UNIQUE-LOCAL-v6", + description="This group encompasses the ULA address space in IPv6", + members=[dict(address="fc00::/7")], + ), + ], + port_group=[ + dict( + name="SSH", + description="This group has the ssh ports", + members=[dict(port="22")], + ), + ], + ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="drop"), + sources=[ + dict(zone="zone-src"), # firewall dropped entirely + ], + ), + ], + ), + state="replaced", + ), + ) + commands = [ + "delete firewall zone ZONE-IZF from zone-src", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_06_replaced_sources_entry_removed(self): + # ZONE-IZF's sources list drops the 'zone-src' entry entirely (not + # just its firewall sub-dict). Exercises the new `elif not opr:` + # branch -- previously this case emitted nothing at all. + set_module_args( + dict( + config=dict( + ping=dict(all=True), + route_redirects=[ + dict(ip_src_route=True, afi="ipv6"), + dict(icmp_redirects=dict(send=True), afi="ipv4"), + ], + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + group=dict( + address_group=[ + dict(afi="ipv4", name="A-EMPTY"), + dict( + afi="ipv4", + name="RND-HOSTS", + description="This group has the Management hosts address lists", + members=[ + dict(address="192.0.2.1"), + dict(address="192.0.2.3"), + dict(address="192.0.2.5"), + ], + ), + dict( + afi="ipv4", + name="DELETE-HOSTS", + description="The (single) last address from this group will be deleted in the tests", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv4", + name="ZONE-TEST", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv6", + name="LOCAL-v6", + description="This group has the hosts address lists of this machine", + members=[ + dict(address="::1"), + dict(address="fdec:2503:89d6:59b3::1"), + ], + ), + ], + network_group=[ + dict( + afi="ipv4", + name="RND", + description="This group has the Management network addresses", + members=[dict(address="192.0.2.0/24")], + ), + dict( + afi="ipv6", + name="UNIQUE-LOCAL-v6", + description="This group encompasses the ULA address space in IPv6", + members=[dict(address="fc00::/7")], + ), + ], + port_group=[ + dict( + name="SSH", + description="This group has the ssh ports", + members=[dict(port="22")], + ), + ], + ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="drop"), + # sources omitted entirely + ), + ], + ), + state="replaced", + ), + ) + commands = [ + "delete firewall zone ZONE-IZF from zone-src", + ] + self.execute_module(changed=True, commands=commands)