diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-08-27 04:00:17 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-26 21:00:17 +0300 |
| commit | 433a274ce636573953f8a4be9c68743a2ed1d0a5 (patch) | |
| tree | da9e545f9ffdf50289c1b94580450a456177d74b | |
| parent | 899a6bf7955592ec40670944a860a1bee97b432c (diff) | |
| download | vyos.vyos-433a274ce636573953f8a4be9c68743a2ed1d0a5.tar.gz vyos.vyos-433a274ce636573953f8a4be9c68743a2ed1d0a5.zip | |
T8220: Firewall Zone Policy support (#447)
Add zone base policy firewall module
16 files changed, 1794 insertions, 24 deletions
diff --git a/changelogs/fragments/t8220-fpz.yaml b/changelogs/fragments/t8220-fpz.yaml new file mode 100644 index 00000000..5c256046 --- /dev/null +++ b/changelogs/fragments/t8220-fpz.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - vyos_firewall_global - Added Firewall Zone Policy support. diff --git a/docs/vyos.vyos.vyos_firewall_global_module.rst b/docs/vyos.vyos.vyos_firewall_global_module.rst index 8ab253c2..f7e4605d 100644 --- a/docs/vyos.vyos.vyos_firewall_global_module.rst +++ b/docs/vyos.vyos.vyos_firewall_global_module.rst @@ -800,6 +800,332 @@ Parameters <div>(strict) Enable Strict Reverse Path Forwarding as defined in RFC3704.</div> </td> </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>zone</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Defines a firewall zone.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>default_action</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li><div style="color: blue"><b>drop</b> ←</div></li> + <li>reject</li> + </ul> + </td> + <td> + <div>Specifies the default action for the zone.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>default_log</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Specifies whether or not to log packets for the firewall zone.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>description</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Allows you to specify a brief description for the firewall zone.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>interfaces</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=string</span> + </div> + </td> + <td> + </td> + <td> + <div>Specifies the interfaces associated with the zone.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>intra_zone_filtering</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Specifies a policy for intra-zone filtering.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>action</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>accept</li> + <li>drop</li> + </ul> + </td> + <td> + <div>Action for intra-zone traffic.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>firewall</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Firewall ruleset to apply to intra-zone traffic.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ipv6_name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Name of the IPv6 firewall ruleset to apply to intra-zone traffic.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Name of the firewall ruleset to apply to intra-zone traffic.</div> + </td> + </tr> + + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>local_zone</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Specifies whether or not the zone is local.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Name of the firewall zone.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>sources</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Specifies the source zones for the firewall rules.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>firewall</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Firewall ruleset to apply to the source zone.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ipv6_name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Name of the IPv6 firewall ruleset to apply to the source zone.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Name of the firewall ruleset to apply to the source zone.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>zone</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Name of the source zone.</div> + </td> + </tr> + + <tr> <td colspan="5"> 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 diff --git a/plugins/modules/vyos_firewall_global.py b/plugins/modules/vyos_firewall_global.py index 9abeb5a1..4967a564 100644 --- a/plugins/modules/vyos_firewall_global.py +++ b/plugins/modules/vyos_firewall_global.py @@ -280,6 +280,91 @@ options: - notice - info - debug + zone: + description: + - Defines a firewall zone. + type: list + elements: dict + suboptions: + name: + description: + - Name of the firewall zone. + type: str + required: true + description: + description: + - Allows you to specify a brief description for the firewall zone. + type: str + default_log: + description: + - Specifies whether or not to log packets for the firewall zone. + type: bool + local_zone: + description: + - Specifies whether or not the zone is local. + type: bool + default_action: + description: + - Specifies the default action for the zone. + type: str + default: drop + choices: + - drop + - reject + interfaces: + description: + - Specifies the interfaces associated with the zone. + type: list + elements: str + intra_zone_filtering: + description: + - Specifies a policy for intra-zone filtering. + type: dict + suboptions: + action: + description: + - Action for intra-zone traffic. + type: str + choices: + - accept + - drop + firewall: + description: + - Firewall ruleset to apply to intra-zone traffic. + type: dict + suboptions: + name: + description: + - Name of the firewall ruleset to apply to intra-zone traffic. + type: str + ipv6_name: + description: + - Name of the IPv6 firewall ruleset to apply to intra-zone traffic. + type: str + sources: + description: + - Specifies the source zones for the firewall rules. + type: list + elements: dict + suboptions: + zone: + description: + - Name of the source zone. + type: str + required: true + firewall: + description: + - Firewall ruleset to apply to the source zone. + type: dict + suboptions: + name: + description: + - Name of the firewall ruleset to apply to the source zone. + type: str + ipv6_name: + description: + - Name of the IPv6 firewall ruleset to apply to the source zone. + type: str running_config: description: - > diff --git a/tests/integration/targets/vyos_firewall_global/tests/cli/_get_version.yaml b/tests/integration/targets/vyos_firewall_global/tests/cli/_get_version.yaml index 2588b194..45bc2e18 100644 --- a/tests/integration/targets/vyos_firewall_global/tests/cli/_get_version.yaml +++ b/tests/integration/targets/vyos_firewall_global/tests/cli/_get_version.yaml @@ -8,6 +8,7 @@ - name: debug vyos_facts debug: var: vyos_facts + when: vyos_facts is not skipped - name: pull version from facts set_fact: @@ -19,10 +20,20 @@ vyos_version: "{{ vyos_version }}.0" when: vyos_version.count('.') == 1 -- name: include correct vars +- name: report resolved VyOS version + debug: + msg: "Using VyOS version {{ vyos_version }} for this testcase" + +- name: include correct vars (pre-1.4.0) include_vars: pre-v1_4.yaml when: vyos_version is version('1.4.0', '<', version_type='semver') -- name: include correct vars +- name: include correct vars (1.4.x) include_vars: v1_4.yaml - when: vyos_version is version('1.4.0', '>=', version_type='semver') + when: >- + vyos_version is version('1.4.0', '>=', version_type='semver') and + vyos_version is version('1.5.0', '<', version_type='semver') + +- name: include correct vars (1.5.0+) + include_vars: v1_5.yaml + when: vyos_version is version('1.5.0', '>=', version_type='semver') diff --git a/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_4.cfg b/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_4.cfg index 41435780..882d9aaa 100644 --- a/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_4.cfg +++ b/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_4.cfg @@ -16,3 +16,6 @@ set firewall global-options state-policy established log 'enable' set firewall global-options state-policy invalid action 'reject' set firewall global-options syn-cookies 'enable' set firewall global-options twa-hazards-protection 'enable' +set firewall zone ZONE-TEST interface 'eth0.1234' +set firewall zone ZONE-TEST description 'zone-test test description' +set firewall zone ZONE-TEST default-action 'drop' diff --git a/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_5.cfg b/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_5.cfg new file mode 100644 index 00000000..88553c00 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_5.cfg @@ -0,0 +1,21 @@ +set firewall global-options all-ping 'enable' +set firewall global-options broadcast-ping 'enable' +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 list' +set firewall group network-group MGMT description 'This group has the Management network addresses' +set firewall group network-group MGMT network '192.0.1.0/24' +set firewall global-options ip-src-route 'enable' +set firewall global-options log-martians 'enable' +set firewall global-options receive-redirects 'disable' +set firewall global-options send-redirects 'enable' +set firewall global-options source-validation 'strict' +set firewall global-options state-policy established action 'accept' +set firewall global-options state-policy established log 'enable' +set firewall global-options state-policy invalid action 'reject' +set firewall global-options syn-cookies 'enable' +set firewall global-options twa-hazards-protection 'enable' +set firewall zone ZONE-TEST member interface 'eth0.1234' +set firewall zone ZONE-TEST description 'zone-test test description' +set firewall zone ZONE-TEST default-action 'drop' diff --git a/tests/integration/targets/vyos_firewall_global/vars/main.yaml b/tests/integration/targets/vyos_firewall_global/vars/main.yaml index 0f041b60..94bdca87 100644 --- a/tests/integration/targets/vyos_firewall_global/vars/main.yaml +++ b/tests/integration/targets/vyos_firewall_global/vars/main.yaml @@ -37,6 +37,12 @@ merged: connection_type: invalid twa_hazards_protection: true validation: strict + zone: + - name: ZONE-TEST + default_action: drop + description: zone-test test description + interfaces: + - eth0.1234 config: validation: strict log_martians: true @@ -70,6 +76,12 @@ merged: description: This group has the Management network addresses members: - address: 192.0.1.0/24 + zone: + - name: ZONE-TEST + description: zone-test test description + interfaces: + - eth0.1234 + diff_config: validation: strict log_martians: true @@ -139,7 +151,12 @@ populate: members: - address: 192.0.1.0/24 afi: ipv4 - + zone: + - name: ZONE-TEST + description: zone-test test description + interfaces: + - eth0.1234 + default_action: drop replaced: commands: "{{ replaced_commands }}" after: @@ -183,6 +200,16 @@ replaced: syn_cookies: true twa_hazards_protection: true validation: strict + zone: + - name: FZP-2 + default_action: reject + default_log: true + description: This is the Firewall zone fzp2 + interfaces: + - eth2 + - lo + intra_zone_filtering: + action: accept config: validation: strict log_martians: true @@ -221,6 +248,16 @@ replaced: description: This group has the Management network addresses members: - address: 192.0.1.0/24 + zone: + - name: FZP-2 + default_action: reject + default_log: true + description: This is the Firewall zone fzp2 + interfaces: + - eth2 + - lo + intra_zone_filtering: + action: accept diff_config: validation: strict log_martians: true @@ -300,6 +337,12 @@ rendered: description: This group has the Management network addresses members: - address: 192.0.1.0/24 + zone: + - name: ZONE-TEST + description: zone-test test description + interfaces: + - eth0.1234 + default_action: drop deleted: commands: "{{ deleted_commands }}" @@ -341,6 +384,16 @@ round_trip: members: - address: 192.0.1.0/24 afi: ipv4 + zone: + - name: FZP-2 + default_action: reject + default_log: true + description: This is the Firewall zone fzp2 + interfaces: + - eth2 + - lo + intra_zone_filtering: + action: accept forward_config: validation: strict log_martians: true @@ -374,6 +427,16 @@ round_trip: description: This group has the Management network addresses members: - address: 192.0.1.0/24 + zone: + - name: FZP-2 + default_action: reject + default_log: true + description: This is the Firewall zone fzp2 + interfaces: + - eth2 + - lo + intra_zone_filtering: + action: accept revert_config: validation: strict log_martians: false diff --git a/tests/integration/targets/vyos_firewall_global/vars/v1_4.yaml b/tests/integration/targets/vyos_firewall_global/vars/v1_4.yaml index 68773b2c..fffa93d8 100644 --- a/tests/integration/targets/vyos_firewall_global/vars/v1_4.yaml +++ b/tests/integration/targets/vyos_firewall_global/vars/v1_4.yaml @@ -20,6 +20,9 @@ merged_commands: - 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 ZONE-TEST description 'zone-test test description' + - set firewall zone ZONE-TEST interface eth0.1234 + - set firewall zone ZONE-TEST default-action 'drop' populate_commands: - set firewall global-options all-ping 'enable' @@ -40,9 +43,12 @@ populate_commands: - set firewall global-options state-policy invalid action 'reject' - set firewall global-options syn-cookies 'enable' - set firewall global-options twa-hazards-protection 'enable' + - set firewall zone ZONE-TEST interface 'eth0.1234' + - set firewall zone ZONE-TEST description 'zone-test test description' replaced_commands: - delete firewall group address-group MGMT-HOSTS + - delete firewall zone ZONE-TEST - set firewall group address-group SALES-HOSTS address 192.0.2.1 - set firewall group address-group SALES-HOSTS address 192.0.2.2 - set firewall group address-group SALES-HOSTS address 192.0.2.3 @@ -52,6 +58,12 @@ replaced_commands: - set firewall group address-group ENG-HOSTS address 192.0.3.2 - set firewall group address-group ENG-HOSTS description 'Sales office hosts address list' - set firewall group address-group ENG-HOSTS + - 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 interface eth2 + - set firewall zone FZP-2 interface lo + - set firewall zone FZP-2 intra-zone-filtering action accept rendered_commands: - set firewall group address-group SALES-HOSTS address 192.0.2.1 @@ -78,6 +90,9 @@ rendered_commands: - 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 ZONE-TEST interface eth0.1234 + - set firewall zone ZONE-TEST description 'zone-test test description' + - set firewall zone ZONE-TEST default-action 'drop' deleted_commands: - "delete firewall" diff --git a/tests/integration/targets/vyos_firewall_global/vars/v1_5.yaml b/tests/integration/targets/vyos_firewall_global/vars/v1_5.yaml new file mode 100644 index 00000000..d32eacc7 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/vars/v1_5.yaml @@ -0,0 +1,110 @@ +--- +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 list' + - set firewall group address-group MGMT-HOSTS + - 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 global-options ip-src-route 'enable' + - set firewall global-options receive-redirects 'disable' + - set firewall global-options send-redirects 'enable' + - set firewall global-options state-policy established action 'accept' + - set firewall global-options state-policy established log + - set firewall global-options state-policy invalid action 'reject' + - set firewall global-options broadcast-ping 'enable' + - set firewall global-options all-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 ZONE-TEST description 'zone-test test description' + - set firewall zone ZONE-TEST member interface eth0.1234 + - set firewall zone ZONE-TEST default-action 'drop' + +populate_commands: + - set firewall global-options all-ping 'enable' + - set firewall global-options broadcast-ping 'enable' + - 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 list' + - set firewall group network-group MGMT description 'This group has the Management network addresses' + - set firewall group network-group MGMT network '192.0.1.0/24' + - set firewall global-options ip-src-route 'enable' + - set firewall global-options log-martians 'enable' + - set firewall global-options receive-redirects 'disable' + - set firewall global-options send-redirects 'enable' + - set firewall global-options source-validation 'strict' + - set firewall global-options state-policy established action 'accept' + - set firewall global-options state-policy established log + - set firewall global-options state-policy invalid action 'reject' + - set firewall global-options syn-cookies 'enable' + - set firewall global-options twa-hazards-protection 'enable' + - set firewall zone ZONE-TEST member interface 'eth0.1234' + - set firewall zone ZONE-TEST description 'zone-test test description' + +replaced_commands: + - delete firewall group address-group MGMT-HOSTS + - delete firewall zone ZONE-TEST + - set firewall group address-group SALES-HOSTS address 192.0.2.1 + - set firewall group address-group SALES-HOSTS address 192.0.2.2 + - set firewall group address-group SALES-HOSTS address 192.0.2.3 + - set firewall group address-group SALES-HOSTS description 'Sales office hosts address list' + - set firewall group address-group SALES-HOSTS + - set firewall group address-group ENG-HOSTS address 192.0.3.1 + - set firewall group address-group ENG-HOSTS address 192.0.3.2 + - set firewall group address-group ENG-HOSTS description 'Sales office hosts address list' + - set firewall group address-group ENG-HOSTS + - 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 member interface eth2 + - set firewall zone FZP-2 member interface lo + - set firewall zone FZP-2 intra-zone-filtering action accept + +rendered_commands: + - set firewall group address-group SALES-HOSTS address 192.0.2.1 + - set firewall group address-group SALES-HOSTS address 192.0.2.2 + - set firewall group address-group SALES-HOSTS address 192.0.2.3 + - set firewall group address-group SALES-HOSTS description 'Sales office hosts address list' + - set firewall group address-group SALES-HOSTS + - set firewall group address-group ENG-HOSTS address 192.0.3.1 + - set firewall group address-group ENG-HOSTS address 192.0.3.2 + - set firewall group address-group ENG-HOSTS description 'Sales office hosts address list' + - set firewall group address-group ENG-HOSTS + - 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 global-options ip-src-route 'enable' + - set firewall global-options receive-redirects 'disable' + - set firewall global-options send-redirects 'enable' + - set firewall global-options state-policy established action 'accept' + - set firewall global-options state-policy established log + - set firewall global-options state-policy invalid action 'reject' + - set firewall global-options broadcast-ping 'enable' + - set firewall global-options all-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 ZONE-TEST member interface eth0.1234 + - set firewall zone ZONE-TEST description 'zone-test test description' + - set firewall zone ZONE-TEST default-action 'drop' + +deleted_commands: + - "delete firewall" + +parsed_config_file: "_parsed_config_1_5.cfg" + +replaced_diff: + - '+ network "1.1.1.1/32"' + - '- network "192.0.1.0/24"' + +merged_diff: + - '+ network "1.1.1.1/32"' + +deleted_diff: + - '- network "192.0.1.0/24"' 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..f58f166c 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 @@ -22,3 +22,9 @@ 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' +set firewall name TESTRULESET-V4 default-action drop +set firewall ipv6-name TESTRULESET-V6-LEGACY default-action drop +set firewall ipv6 name TESTRULESET-V6-1_4PLUS default-action drop diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg new file mode 100644 index 00000000..23dd9777 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg @@ -0,0 +1,11 @@ +# Intentionally minimal: represents a VyOS 1.5.0 device with one existing +# zone already configured using the 1.5.0 'member interface' syntax. Used +# to test parse_zone's unwrap logic in isolation. TestVyosFirewallRulesModule15's +# other tests (e.g. test_01_merged) create a differently-named zone +# (ZONE-15), so under 'merged' state this existing zone is never touched +# and doesn't interfere. +set firewall zone ZONE-15-EXISTING member interface eth2 +set firewall zone ZONE-15-EXISTING description 'existing 1.5.0 zone for facts parsing test' +set firewall name TESTRULESET-V4 default-action drop +set firewall ipv6-name TESTRULESET-V6-LEGACY default-action drop +set firewall ipv6 name TESTRULESET-V6-1_4PLUS default-action drop 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 a7abfe8c..397ccfbd 100644 --- a/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py @@ -158,6 +158,31 @@ class TestVyosFirewallRulesModule14(TestVyosModule): ), ], ), + 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", ), @@ -195,6 +220,15 @@ class TestVyosFirewallRulesModule14(TestVyosModule): "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) @@ -321,6 +355,31 @@ class TestVyosFirewallRulesModule14(TestVyosModule): ), ], ), + 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", ), @@ -334,15 +393,26 @@ class TestVyosFirewallRulesModule14(TestVyosModule): "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) @@ -421,6 +491,24 @@ class TestVyosFirewallRulesModule14(TestVyosModule): ), ], ), + 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="existing-ruleset"), + ), + ], + ), + ], ), state="replaced", ), @@ -487,6 +575,15 @@ class TestVyosFirewallRulesModule14(TestVyosModule): ), ], ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=[ + "lo", + ], + ), + ], ), state="replaced", ), @@ -496,6 +593,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule): "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", @@ -506,6 +604,8 @@ class TestVyosFirewallRulesModule14(TestVyosModule): "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", @@ -518,3 +618,401 @@ class TestVyosFirewallRulesModule14(TestVyosModule): 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) + + def test_vyos_firewall_global_ruleset_lines_filtered_from_facts(self): + # Confirms render_config's pre-filter strips all three ruleset-line + # prefixes (IPv4 name, legacy hyphenated ipv6-name, and 1.4+ + # space-separated "ipv6 name") before zone/global-options parsing + # runs, on a real 1.4.x-shaped fixture alongside existing zone data. + set_module_args(dict(config=dict(), state="gathered")) + result = self.execute_module(changed=False) + facts = result["gathered"] + self.assertNotIn("TESTRULESET-V4", str(facts)) + self.assertNotIn("TESTRULESET-V6-LEGACY", str(facts)) + self.assertNotIn("TESTRULESET-V6-1_4PLUS", str(facts)) diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py b/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py new file mode 100644 index 00000000..22ab5364 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py @@ -0,0 +1,132 @@ +# (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 <http://www.gnu.org/licenses/>. + +# 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 TestVyosFirewallRulesModule15(TestVyosModule): + module = vyos_firewall_global + + def setUp(self): + super(TestVyosFirewallRulesModule15, 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.5" + + self.execute_show_command = self.mock_execute_show_command.start() + self.maxDiff = None + + def tearDown(self): + super(TestVyosFirewallRulesModule15, 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_v15.cfg") + + self.execute_show_command.side_effect = load_from_file + + def test_vyos_firewall_global_set_01_merged_interface_uses_member_keyword(self): + # On a 1.5.0 device (empty fixture, no existing zone), merging a + # zone with an interface should render "member interface", not the + # bare "interface" used on 1.4.x / 1.5-rolling. This is the sole + # thing this class exists to prove right now -- the version-gate + # added to _render_interfaces. + set_module_args( + dict( + config=dict( + zone=[ + dict( + name="ZONE-15", + interfaces=["eth1"], + ), + ], + ), + state="merged", + ), + ) + commands = [ + "set firewall zone ZONE-15 default-action 'drop'", + "set firewall zone ZONE-15 member interface eth1", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_02_gathered_member_interface_parsed(self): + # ZONE-15-EXISTING in the fixture uses 1.5.0's "member interface" + # syntax. Before the parse_zone fix, this misparsed into a bogus + # zone['member'] = "interface eth2" string field, and 'interfaces' + # was missing entirely. After the fix, it should land in + # 'interfaces' exactly like the pre-1.5.0 bare "interface" form. + set_module_args(dict(config=dict(), state="gathered")) + result = self.execute_module(changed=False) + zones = result["gathered"]["zone"] + zone = next(z for z in zones if z["name"] == "ZONE-15-EXISTING") + self.assertEqual(zone["interfaces"], ["eth2"]) + self.assertEqual(zone["description"], "existing 1.5.0 zone for facts parsing test") + self.assertNotIn("member", zone) + + def test_vyos_firewall_global_ruleset_lines_filtered_from_facts(self): + # Same coverage as the 1.4 version, confirmed independently on the + # 1.5.0 fixture/version path. + set_module_args(dict(config=dict(), state="gathered")) + result = self.execute_module(changed=False) + facts = result["gathered"] + self.assertNotIn("TESTRULESET-V4", str(facts)) + self.assertNotIn("TESTRULESET-V6-LEGACY", str(facts)) + self.assertNotIn("TESTRULESET-V6-1_4PLUS", str(facts)) |
