diff options
40 files changed, 3379 insertions, 245 deletions
diff --git a/changelogs/fragments/cliconf.yml b/changelogs/fragments/cliconf.yml new file mode 100644 index 0000000..53c26ad --- /dev/null +++ b/changelogs/fragments/cliconf.yml @@ -0,0 +1,4 @@ +--- + +minor_changes: + - added `network_os_major_version` to facts diff --git a/changelogs/fragments/firewall_global_14.yml b/changelogs/fragments/firewall_global_14.yml new file mode 100644 index 0000000..269c3e5 --- /dev/null +++ b/changelogs/fragments/firewall_global_14.yml @@ -0,0 +1,7 @@ +--- +minor_changes: + - with 1.4+, use the the global keyword to define global firewall rules + - Fixed ipv6 route-redirects and tests + - Added support for input, output, and forward chains (1.4+) + - Fixed state-policy deletion (partial and full) + - Added support for log-level in state-policy (1.4+) diff --git a/changelogs/fragments/firewall_rules.yml b/changelogs/fragments/firewall_rules.yml new file mode 100644 index 0000000..7cd02a2 --- /dev/null +++ b/changelogs/fragments/firewall_rules.yml @@ -0,0 +1,8 @@ +--- +breaking_changes: + - firewall_rules - tcp.flags is now a list with an inversion flag to support 1.4+ firewall rules, but still supports 1.3- + +minor_changes: + - firewall_rules - Added support for 1.4+ firewall rules + - fix tests for 1.4+ firewall rules (ICMP V6 code and type) + - added support for 1.5+ firewall `match-ipsec-in`, `match-ipsec-out`, `match-none-in`, `match-none-out` diff --git a/changelogs/fragments/interfaces_update.yml b/changelogs/fragments/interfaces_update.yml new file mode 100644 index 0000000..e9a6d21 --- /dev/null +++ b/changelogs/fragments/interfaces_update.yml @@ -0,0 +1,9 @@ +--- +minor_changes: + - fixed bug where 'replace' would delete an active disable and not reinstate it + - make l3_interfaces pick up loopback interfaces + - added tests for verifying both of these + - fixed over-zealous handling of disable, which could catch other interface + items that are disabled. + - enable support for 1.4 firewall + - support for 1.4 ospf diff --git a/changelogs/fragments/tests.yml b/changelogs/fragments/tests.yml new file mode 100644 index 0000000..78e3d59 --- /dev/null +++ b/changelogs/fragments/tests.yml @@ -0,0 +1,3 @@ +--- +trivial: + - ignore 2.19 sanity tests for now diff --git a/docs/vyos.vyos.vyos_firewall_global_module.rst b/docs/vyos.vyos.vyos_firewall_global_module.rst index 34293b1..a77ce80 100644 --- a/docs/vyos.vyos.vyos_firewall_global_module.rst +++ b/docs/vyos.vyos.vyos_firewall_global_module.rst @@ -852,6 +852,7 @@ Examples - connection_type: established action: accept log: true + log_level: emer - connection_type: invalid action: reject route_redirects: @@ -897,6 +898,7 @@ Examples # "set firewall config-trap 'enable'", # "set firewall state-policy established action 'accept'", # "set firewall state-policy established log 'enable'", + # "set firewall state-policy established log-level 'emer'", # "set firewall state-policy invalid action 'reject'", # "set firewall broadcast-ping 'enable'", # "set firewall all-ping 'enable'", diff --git a/docs/vyos.vyos.vyos_firewall_rules_module.rst b/docs/vyos.vyos.vyos_firewall_rules_module.rst index 246824b..b3d619b 100644 --- a/docs/vyos.vyos.vyos_firewall_rules_module.rst +++ b/docs/vyos.vyos.vyos_firewall_rules_module.rst @@ -546,8 +546,14 @@ Parameters </td> <td> <ul style="margin: 0; padding: 0"><b>Choices:</b> + <br><i>VyOS 1.4 & older:</i><br> <li>match-ipsec</li> <li>match-none</li> + <br><i>VyOS 1.5+ :</i><br> + <li>match-ipsec-in</li> + <li>match-ipsec-out</li> + <li>match-none-in</li> + <li>match-none-out</li> </ul> </td> <td> diff --git a/plugins/cliconf/vyos.py b/plugins/cliconf/vyos.py index 7e6b0b1..5beffaa 100644 --- a/plugins/cliconf/vyos.py +++ b/plugins/cliconf/vyos.py @@ -80,6 +80,11 @@ class Cliconf(CliconfBase): if match: device_info["network_os_version"] = match.group(1) + if device_info["network_os_version"]: + match = re.search(r"VyOS\s*(\d+\.\d+)", device_info["network_os_version"]) + if match: + device_info["network_os_major_version"] = match.group(1) + match = re.search(r"(?:HW|Hardware) model:\s*(\S+)", data) if match: device_info["network_os_model"] = match.group(1) 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 2326bea..f79454e 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 @@ -134,6 +134,9 @@ class Firewall_globalArgs(object): # pylint: disable=R0903 "type": "str", }, "log": {"type": "bool"}, + "log_level": { + "choices": ["emerg", "alert", "crit", "err", "warn", "notice", "info", "debug"] + } }, "type": "list", }, diff --git a/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py index eb285cf..4d0973e 100644 --- a/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py @@ -50,11 +50,16 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "elements": "dict", "options": { "default_action": { - "choices": ["drop", "reject", "accept"], + "choices": ["drop", "reject", "accept", "jump"], "type": "str", }, + "default_jump_target": {"type": "str"}, "description": {"type": "str"}, "enable_default_log": {"type": "bool"}, + "filter": { + "choices": ["input", "output", "forward"], + "type": "str" + }, "name": {"type": "str"}, "rules": { "elements": "dict", @@ -65,6 +70,11 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "reject", "accept", "inspect", + "continue", + "return", + "jump", + "queue", + "synproxy", ], "type": "str", }, @@ -147,9 +157,23 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 }, "type": "dict", }, + "inbound_interface": { + "options": { + "group": { + "type": "str", + }, + "name": { + "type": "str", + }, + }, + "type": "dict", + }, "ipsec": { - "choices": ["match-ipsec", "match-none"], - "type": "str", + "choices": ["match-ipsec", "match-none", "match-ipsec-in", "match-ipsec-out", "match-none-in", "match-none-out"], + "type": "str" + }, + "jump_target": { + "type": "str" }, "limit": { "options": { @@ -169,6 +193,17 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "choices": ["enable", "disable"], }, "number": {"required": True, "type": "int"}, + "outbound_interface": { + "options": { + "group": { + "type": "str", + }, + "name": { + "type": "str", + }, + }, + "type": "dict", + }, "p2p": { "elements": "dict", "options": { @@ -185,19 +220,52 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "type": "str", }, }, + "type": "list" + }, + "packet_length": { + "elements": "dict", + "options": { + "length": { + "type": "str", + }, + }, + "type": "list" + }, + "packet_length_exclude": { + "elements": "dict", + "options": { + "length": { + "type": "str", + } + }, "type": "list", }, + "packet_type": { + "choices": [ + "broadcast", + "multicast", + "host", + "other" + ], + "type": "str" + }, "protocol": {"type": "str"}, + "queue": {"type": "str"}, + "queue_options": { + "choices": ["bypass", "fanout"], + "type": "str" + }, "recent": { "options": { "count": {"type": "int"}, - "time": {"type": "int"}, + "time": {"type": "str"}, }, "type": "dict", }, "source": { "options": { "address": {"type": "str"}, + "fqdn": {"type": "str"}, "group": { "options": { "address_group": {"type": "str"}, @@ -220,8 +288,37 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 }, "type": "dict", }, + "synproxy": { + "options": { + "mss": {"type": "int"}, + "window_scale": {"type": "int"}, + }, + "type": "dict", + }, "tcp": { - "options": {"flags": {"type": "str"}}, + "options": { + "flags": { + "elements": "dict", + "options": { + "flag": { + "choices": [ + "ack", + "cwr", + "ecn", + "fin", + "psh", + "rst", + "syn", + "urg", + "all", + ], + "type": "str" + }, + "invert": {"type": "bool"} + }, + "type": "list" + } + }, "type": "dict", }, "time": { 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 8694f11..7e978ff 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 @@ -31,6 +31,10 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils list_diff_want_only, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion + class Firewall_global(ConfigBase): """ @@ -255,7 +259,7 @@ class Firewall_global(ConfigBase): continue if ( key in l_set - and not (h and self._in_target(h, key)) + and not self._in_target(h, key) and not self._is_del(l_set, h) ): commands.append( @@ -455,7 +459,10 @@ class Firewall_global(ConfigBase): """ commands = [] have = [] - l_set = ("log", "action", "connection_type") + 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: @@ -478,25 +485,23 @@ class Firewall_global(ConfigBase): ), ) elif not opr and key in l_set: - if not (h and self._in_target(h, key)) and not self._is_del( - l_set, - h, - ): - if key == "action": - commands.append( - self._form_attr_cmd( - attr=attr + " " + w["connection_type"], - opr=opr, - ), - ) - else: - commands.append( - self._form_attr_cmd( - attr=attr + " " + w["connection_type"], - val=self._bool_to_str(val), - opr=opr, - ), - ) + 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): @@ -520,6 +525,14 @@ class Firewall_global(ConfigBase): 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 iteritems(w): if val and key != "afi": if opr and key in l_set and not (h and self._is_w_same(w, h, key)): @@ -528,6 +541,7 @@ class Firewall_global(ConfigBase): attr=key, val=self._bool_to_str(val), opr=opr, + type=afi ), ) elif not opr and key in l_set: @@ -537,6 +551,7 @@ class Firewall_global(ConfigBase): attr=key, val=self._bool_to_str(val), opr=opr, + type=afi ), ) continue @@ -546,6 +561,7 @@ class Firewall_global(ConfigBase): attr=key, val=self._bool_to_str(val), opr=opr, + type=afi ), ) elif key == "icmp_redirects": @@ -565,20 +581,27 @@ class Firewall_global(ConfigBase): 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 iteritems(w[attr]): 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), + 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)) + commands.append(self._form_attr_cmd(attr=item, opr=opr, type=afi)) return commands def search_attrib_in_have(self, have, want, attr): @@ -595,16 +618,17 @@ class Firewall_global(ConfigBase): return h return None - def _form_attr_cmd(self, key=None, attr=None, val=None, opr=True): + 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), val=val, opr=opr) + 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): @@ -621,13 +645,15 @@ class Firewall_global(ConfigBase): cmd = "delete firewall " else: cmd = "set firewall " + if key != "group" 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: cmd += " '" + str(val) + "'" - return cmd + return cmd.strip() def _bool_to_str(self, val): """ @@ -698,7 +724,7 @@ class Firewall_global(ConfigBase): :param key: number. :return: True/False. """ - return key in b_set and not (h and self._in_target(h, key)) + return key in b_set and not self._in_target(h, key) def _map_attrib(self, attrib, type=None): """ diff --git a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py index 09e19d7..106b2b8 100644 --- a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py @@ -15,8 +15,6 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import re - from copy import deepcopy from ansible.module_utils.six import iteritems @@ -33,6 +31,10 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils list_diff_want_only, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion + class Firewall_rules(ConfigBase): """ @@ -171,12 +173,8 @@ class Firewall_rules(ConfigBase): # In the desired configuration, search for the rule set we # already have (to be replaced by our desired # configuration's rule set). - wanted_rule_set = self.search_r_sets_in_have( - want, - rs["name"], - "r_list", - h["afi"], - ) + rs_id = self._rs_id(rs, h["afi"]) + wanted_rule_set = self.search_r_sets_in_have(want, rs_id, "r_list") if wanted_rule_set is not None: # Remove the rules that we already have if the wanted # rules exist under the same name. @@ -202,11 +200,12 @@ class Firewall_rules(ConfigBase): commands = [] if have: for h in have: - r_sets = self._get_r_sets(h) - for rs in r_sets: - w = self.search_r_sets_in_have(want, rs["name"], "r_list", h["afi"]) + have_r_sets = self._get_r_sets(h) + for rs in have_r_sets: + rs_id = self._rs_id(rs, h["afi"]) + w = self.search_r_sets_in_have(want, rs_id, "r_list") if not w: - commands.append(self._compute_command(h["afi"], rs["name"], remove=True)) + commands.append(self._compute_command(rs_id, remove=True)) else: commands.extend(self._add_r_sets(h["afi"], rs, w, opr=False)) commands.extend(self._state_merged(want, have)) @@ -223,7 +222,8 @@ class Firewall_rules(ConfigBase): for w in want: r_sets = self._get_r_sets(w) for rs in r_sets: - h = self.search_r_sets_in_have(have, rs["name"], "r_list", w["afi"]) + rs_id = self._rs_id(rs, w["afi"]) + h = self.search_r_sets_in_have(have, rs_id, "r_list") commands.extend(self._add_r_sets(w["afi"], rs, h)) return commands @@ -240,18 +240,21 @@ class Firewall_rules(ConfigBase): r_sets = self._get_r_sets(w) if r_sets: for rs in r_sets: - h = self.search_r_sets_in_have(have, rs["name"], "r_list", w["afi"]) + rs_id = self._rs_id(rs, w["afi"]) + h = self.search_r_sets_in_have(have, rs_id, "r_list") if h: - commands.append(self._compute_command(w["afi"], h["name"], remove=True)) + commands.append(self._compute_command(rs_id, remove=True)) elif have: for h in have: if h["afi"] == w["afi"]: - commands.append(self._compute_command(w["afi"], remove=True)) + commands.append( + self._compute_command(self._rs_id(None, w["afi"]), remove=True) + ) elif have: for h in have: r_sets = self._get_r_sets(h) if r_sets: - commands.append(self._compute_command(afi=h["afi"], remove=True)) + commands.append(self._compute_command(self._rs_id(None, h["afi"]), remove=True)) return commands def _add_r_sets(self, afi, want, have, opr=True): @@ -265,11 +268,12 @@ class Firewall_rules(ConfigBase): :return: generated commands list. """ commands = [] - l_set = ("description", "default_action", "enable_default_log") + l_set = ("description", "default_action", "default_jump_target", "enable_default_log") h_rs = {} h_rules = {} w_rs = deepcopy(remove_empties(want)) w_rules = w_rs.pop("rules", None) + rs_id = self._rs_id(want, afi=afi) if have: h_rs = deepcopy(remove_empties(have)) h_rules = h_rs.pop("rules", None) @@ -278,9 +282,9 @@ class Firewall_rules(ConfigBase): if opr and key in l_set and not (h_rs and self._is_w_same(w_rs, h_rs, key)): if key == "enable_default_log": if val and (not h_rs or key not in h_rs or not h_rs[key]): - commands.append(self._add_rs_base_attrib(afi, want["name"], key, w_rs)) + commands.append(self._add_rs_base_attrib(rs_id, key, w_rs)) else: - commands.append(self._add_rs_base_attrib(afi, want["name"], key, w_rs)) + commands.append(self._add_rs_base_attrib(rs_id, key, w_rs)) elif not opr and key in l_set: if ( key == "enable_default_log" @@ -288,22 +292,24 @@ class Firewall_rules(ConfigBase): and h_rs and (key not in h_rs or not h_rs[key]) ): - commands.append(self._add_rs_base_attrib(afi, want["name"], key, w_rs, opr)) + commands.append(self._add_rs_base_attrib(rs_id, key, w_rs, opr)) elif not (h_rs and self._in_target(h_rs, key)): - commands.append(self._add_rs_base_attrib(afi, want["name"], key, w_rs, opr)) - commands.extend(self._add_rules(afi, want["name"], w_rules, h_rules, opr)) + commands.append(self._add_rs_base_attrib(rs_id, key, w_rs, opr)) + commands.extend(self._add_rules(rs_id, w_rules, h_rules, opr)) if h_rules: have["rules"] = h_rules if w_rules: want["rules"] = w_rules return commands - def _add_rules(self, afi, name, w_rules, h_rules, opr=True): + def _add_rules(self, rs_id, w_rules, h_rules, opr=True): """ This function forms the set/delete commands based on the 'opr' type for rules attributes. - :param want: desired config. - :param have: target config. + :param rs_id: rule-set identifier. + :param w_rules: desired config. + :param h_rules: target config. + :param opr: True/False. :return: generated commands list. """ commands = [] @@ -316,31 +322,70 @@ class Firewall_rules(ConfigBase): "disable", "description", "log", + "jump_target", ) if w_rules: for w in w_rules: - cmd = self._compute_command(afi, name, w["number"], opr=opr) - h = self.search_r_sets_in_have(h_rules, w["number"], type="rules") + cmd = self._compute_command(rs_id, w["number"], opr=opr) + h = self.search_rules_in_have_rs(h_rules, w["number"]) for key, val in iteritems(w): if val: if opr and key in l_set and not (h and self._is_w_same(w, h, key)): if key == "disable": if not (not val and (not h or key not in h or not h[key])): - commands.append(self._add_r_base_attrib(afi, name, key, w)) + commands.append(self._add_r_base_attrib(rs_id, key, w)) else: - commands.append(self._add_r_base_attrib(afi, name, key, w)) + commands.append(self._add_r_base_attrib(rs_id, key, w)) elif not opr: + # Note: if you are experiencing sticky configuration on replace + # you may need to add an explicit check for the key here. Anything that + # doesn't have a custom operation is taken care of by the `l_set` check + # below, but I'm not sure how any of the others work. + # It's possible that historically the delete was forced (but now it's + # checked). if key == "number" and self._is_del(l_set, h): - commands.append(self._add_r_base_attrib(afi, name, key, w, opr=opr)) + commands.append(self._add_r_base_attrib(rs_id, key, w, opr=opr)) continue - if key == "disable" and val and h and (key not in h or not h[key]): - commands.append(self._add_r_base_attrib(afi, name, key, w, opr=opr)) + if ( + key == "tcp" + and val + and h + and (key not in h or not h[key] or h[key] != w[key]) + ): + commands.extend(self._add_tcp(key, w, h, cmd, opr)) + if ( + key == "state" + and val + and h + and (key not in h or not h[key] or h[key] != w[key]) + ): + commands.extend(self._add_state(key, w, h, cmd, opr)) + if ( + key == "icmp" + and val + and h + and (key not in h or not h[key] or h[key] != w[key]) + ): + commands.extend(self._add_icmp(key, w, h, cmd, opr)) + if ( + key in ("packet_length", "packet_length_exclude") + and val + and h + and (key not in h or not h[key] or h[key] != w[key]) + ): + commands.extend(self._add_packet_length(key, w, h, cmd, opr)) + elif key == "disable" and val and h and (key not in h or not h[key]): + commands.append(self._add_r_base_attrib(rs_id, key, w, opr=opr)) + elif key in ("inbound_interface", "outbound_interface") and not ( + h and self._is_w_same(w, h, key) + ): + commands.extend(self._add_interface(key, w, h, cmd, opr)) elif ( key in l_set and not (h and self._in_target(h, key)) and not self._is_del(l_set, h) ): - commands.append(self._add_r_base_attrib(afi, name, key, w, opr=opr)) + commands.append(self._add_r_base_attrib(rs_id, key, w, opr=opr)) elif key == "p2p": commands.extend(self._add_p2p(key, w, h, cmd, opr)) elif key == "tcp": @@ -357,6 +402,10 @@ class Firewall_rules(ConfigBase): commands.extend(self._add_recent(key, w, h, cmd, opr)) elif key == "destination" or key == "source": commands.extend(self._add_src_or_dest(key, w, h, cmd, opr)) + elif key in ("packet_length", "packet_length_exclude"): + commands.extend(self._add_packet_length(key, w, h, cmd, opr)) + elif key in ("inbound_interface", "outbound_interface"): + commands.extend(self._add_interface(key, w, h, cmd, opr)) return commands def _add_p2p(self, attr, w, h, cmd, opr): @@ -405,8 +454,11 @@ class Firewall_rules(ConfigBase): and item in l_set and not (h_state and self._is_w_same(w[attr], h_state, item)) ): - commands.append(cmd + (" " + attr + " " + item + " " + self._bool_to_str(val))) - elif not opr and item in l_set and not (h_state and self._in_target(h_state, item)): + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + commands.append(cmd + (" " + attr + " " + item)) + else: + commands.append(cmd + (" " + attr + " " + item + " " + self._bool_to_str(val))) + elif not opr and item in l_set and not self._in_target(h_state, item): commands.append(cmd + (" " + attr + " " + item)) return commands @@ -460,26 +512,50 @@ class Firewall_rules(ConfigBase): and not (h_icmp and self._is_w_same(w[attr], h_icmp, item)) ): if item == "type_name": - os_version = self._get_os_version() - ver = re.search( - "vyos ([\\d\\.]+)-?.*", # noqa: W605 - os_version, - re.IGNORECASE, - ) - if ver.group(1) >= "1.4": + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.3"): param_name = "type-name" else: param_name = "type" - if "ipv6-name" in cmd: + if "ipv6" in cmd: # ipv6-name or ipv6 commands.append(cmd + (" " + "icmpv6" + " " + param_name + " " + val)) else: commands.append( cmd + (" " + attr + " " + item.replace("_", "-") + " " + val), ) else: - commands.append(cmd + (" " + attr + " " + item + " " + str(val))) - elif not opr and item in l_set and not (h_icmp and self._in_target(h_icmp, item)): - commands.append(cmd + (" " + attr + " " + item)) + if "ipv6" in cmd: # ipv6-name or ipv6 + commands.append(cmd + (" " + "icmpv6" + " " + item + " " + str(val))) + else: + commands.append(cmd + (" " + attr + " " + item + " " + str(val))) + elif not opr and item in l_set and not self._in_target(h_icmp, item): + commands.append(cmd + (" " + attr + " " + item.replace("_", "-") + " " + str(val))) + return commands + + def _add_interface(self, attr, w, h, cmd, opr): + """ + This function forms the commands for 'interface' attributes based on the 'opr'. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + commands = [] + h_if = {} + l_set = ("name", "group") + if w[attr]: + if h and attr in h.keys(): + h_if = h.get(attr) or {} + for item, val in iteritems(w[attr]): + if opr and item in l_set and not (h_if and self._is_w_same(w[attr], h_if, item)): + commands.append( + cmd + + (" " + attr.replace("_", "-") + " " + item.replace("_", "-") + " " + val) + ) + elif not opr and item in l_set and not (h_if and self._in_target(h_if, item)): + commands.append( + cmd + (" " + attr.replace("_", "-") + " " + item.replace("_", "-")) + ) return commands def _add_time(self, attr, w, h, cmd, opr): @@ -524,15 +600,107 @@ class Firewall_rules(ConfigBase): commands.append(cmd + (" " + attr + " " + item)) return commands + def _add_tcp_1_4(self, attr, w, h, cmd, opr): + """ + This function forms the commands for 'tcp' attributes based on the 'opr'. + Version 1.4+ + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + commands = [] + have = [] + key = "flags" + want = [] + + if w: + if w.get(attr): + want = w.get(attr).get(key) or [] + if h: + if h.get(attr): + have = h.get(attr).get(key) or [] + if want: + if opr: + flags = list_diff_want_only(want, have) + for flag in flags: + invert = flag.get("invert", False) + commands.append( + cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]) + ) + elif not opr: + flags = list_diff_want_only(want, have) + for flag in flags: + invert = flag.get("invert", False) + commands.append( + cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]) + ) + return commands + + def _add_packet_length(self, attr, w, h, cmd, opr): + """ + This function forms the commands for 'packet_length[_exclude]' attributes based on the 'opr'. + If < 1.4, handle tcp attributes. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + commands = [] + have = [] + want = [] + + if w: + if w.get(attr): + want = w.get(attr) or [] + if h: + if h.get(attr): + have = h.get(attr) or [] + attr = attr.replace("_", "-") + if want: + if opr: + lengths = list_diff_want_only(want, have) + for l_rec in lengths: + commands.append(cmd + " " + attr + " " + str(l_rec["length"])) + elif not opr: + lengths = list_diff_want_only(want, have) + for l_rec in lengths: + commands.append(cmd + " " + attr + " " + str(l_rec["length"])) + return commands + + def _tcp_flags_string(self, flags): + """ + This function forms the tcp flags string. + :param flags: flags list. + :return: flags string or None. + """ + if not flags: + return "" + flag_str = "" + for flag in flags: + this_flag = flag["flag"].upper() + if flag.get("invert", False): + this_flag = "!" + this_flag + if len(flag_str) > 0: + flag_str = ",".join([flag_str, this_flag]) + else: + flag_str = this_flag + return flag_str + def _add_tcp(self, attr, w, h, cmd, opr): """ This function forms the commands for 'tcp' attributes based on the 'opr'. + If < 1.4, handle tcp attributes. :param attr: attribute name. :param w: base config. :param h: target config. :param cmd: commands to be prepend. :return: generated list of commands. """ + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + return self._add_tcp_1_4(attr, w, h, cmd, opr) h_tcp = {} commands = [] if w[attr]: @@ -542,10 +710,11 @@ class Firewall_rules(ConfigBase): if h and key in h[attr].keys(): h_tcp = h[attr].get(key) or {} if flags: - if opr and not (h_tcp and self._is_w_same(w[attr], h[attr], key)): - commands.append(cmd + (" " + attr + " " + key + " " + flags)) - if not opr and not (h_tcp and self._is_w_same(w[attr], h[attr], key)): - commands.append(cmd + (" " + attr + " " + key + " " + flags)) + flag_str = self._tcp_flags_string(flags) + if opr and not (h_tcp and flags == h_tcp): + commands.append(cmd + (" " + attr + " " + "flags" + " " + flag_str)) + if not opr and not (h_tcp and flags == h_tcp): + commands.append(cmd + (" " + attr + " " + "flags" + " " + flag_str)) return commands def _add_limit(self, attr, w, h, cmd, opr): @@ -671,43 +840,68 @@ class Firewall_rules(ConfigBase): ) return commands - def search_r_sets_in_have(self, have, w_name, type="rule_sets", afi=None): + def search_rules_in_have_rs(self, have_rules, r_number): + """ + This function returns the rule if it is present in target config. + :param have: target config. + :param rs_id: rule-set identifier. + :param r_number: rule-number. + :return: rule. + """ + if have_rules: + for h in have_rules: + key = "number" + for r in have_rules: + if key in r and r[key] == r_number: + return r + return None + + def search_r_sets_in_have(self, have, rs_id, type="rule_sets"): """ This function returns the rule-set/rule if it is present in target config. :param have: target config. - :param w_name: rule-set name. - :param type: rule_sets/rule/r_list. - :param afi: address family (when type is r_list). + :param rs_id: rule-identifier. + :param type: rule_sets if searching a rule_set and r_list if searching from a rule_list. :return: rule-set/rule. """ - if have: + if "afi" in rs_id: + afi = rs_id["afi"] + else: + afi = None + if rs_id["filter"]: + key = "filter" + w_value = rs_id["filter"] + elif rs_id["name"]: key = "name" - if type == "rules": - key = "number" - for r in have: - if r[key] == w_name: - return r - elif type == "r_list": + w_value = rs_id["name"] + else: + raise ValueError("id must be specific to name or filter") + + if type not in ("r_list", "rule_sets"): + raise ValueError("type must be rule_sets or r_list") + if have: + if type == "r_list": for h in have: if h["afi"] == afi: r_sets = self._get_r_sets(h) for rs in r_sets: - if rs[key] == w_name: + if key in rs and rs[key] == w_value: return rs else: + # searching a ruleset for rs in have: - if rs[key] == w_name: + if key in rs and rs[key] == w_value: return rs return None - def _get_r_sets(self, item, type="rule_sets"): + def _get_r_sets(self, item): """ - This function returns the list of rule-sets/rules. + This function returns the list of rule-sets. :param item: config dictionary. - :param type: rule_sets/rule/r_list. :return: list of rule-sets/rules. """ rs_list = [] + type = "rule_sets" r_sets = item[type] if r_sets: for rs in r_sets: @@ -716,8 +910,7 @@ class Firewall_rules(ConfigBase): def _compute_command( self, - afi, - name=None, + rs_id, number=None, attrib=None, value=None, @@ -726,46 +919,53 @@ class Firewall_rules(ConfigBase): ): """ This function construct the add/delete command based on passed attributes. - :param afi: address type. - :param name: rule-set name. + :param rs_id: rule-set identifier. :param number: rule-number. :param attrib: attribute name. :param value: value. :param remove: True if delete command needed to be construct. - :param opr: opeeration flag. + :param opr: operation flag. :return: generated command. """ + if rs_id["name"] and rs_id["filter"]: + raise ValueError("name and filter cannot be used together") if remove or not opr: - cmd = "delete firewall " + self._get_fw_type(afi) + cmd = "delete firewall " + self._get_fw_type(rs_id["afi"]) else: - cmd = "set firewall " + self._get_fw_type(afi) - if name: - cmd += " " + name + cmd = "set firewall " + self._get_fw_type(rs_id["afi"]) + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if rs_id["name"]: + cmd += " name " + rs_id["name"] + elif rs_id["filter"]: + cmd += " " + rs_id["filter"] + " filter" + elif rs_id["name"]: + cmd += " " + rs_id["name"] if number: cmd += " rule " + str(number) if attrib: - cmd += " " + attrib.replace("_", "-") + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") and attrib == "enable_default_log": + cmd += " " + "default-log" + else: + cmd += " " + attrib.replace("_", "-") if value and opr and attrib != "enable_default_log" and attrib != "disable": cmd += " '" + str(value) + "'" return cmd - def _add_r_base_attrib(self, afi, name, attr, rule, opr=True): + def _add_r_base_attrib(self, rs_id, attr, rule, opr=True): """ This function forms the command for 'rules' attributes which doesn't have further sub attributes. - :param afi: address type. - :param name: rule-set name + :param rs_id: rule-set identifier. :param attrib: attribute name :param rule: rule config dictionary. :param opr: True/False. :return: generated command. """ if attr == "number": - command = self._compute_command(afi=afi, name=name, number=rule["number"], opr=opr) + command = self._compute_command(rs_id, number=rule["number"], opr=opr) else: command = self._compute_command( - afi=afi, - name=name, + rs_id=rs_id, number=rule["number"], attrib=attr, value=rule[attr], @@ -773,21 +973,54 @@ class Firewall_rules(ConfigBase): ) return command - def _add_rs_base_attrib(self, afi, name, attrib, rule, opr=True): + def _rs_id(self, have, afi, name=None, filter=None): """ + This function returns the rule-set identifier based on + the example rule, overriding the components as specified. - This function forms the command for 'rule-sets' attributes which doesn't - have further sub attributes. + :param have: example rule. :param afi: address type. - :param name: rule-set name + :param name: rule-set name. + :param filter: filter name. + :return: rule-set identifier. + """ + identifier = {"name": None, "filter": None} + if afi: + identifier["afi"] = afi + else: + raise ValueError("afi must be provided") + + if name: + identifier["name"] = name + return identifier + elif filter: + identifier["filter"] = filter + return identifier + if have: + if "name" in have and have["name"]: + identifier["name"] = have["name"] + return identifier + if "filter" in have and have["filter"]: + identifier["filter"] = have["filter"] + return identifier + # raise ValueError("name or filter must be provided or present in have") + # unless we want a wildcard + return identifier + + def _add_rs_base_attrib(self, rs_id, attrib, rule, opr=True): + """ + + This function forms the command for 'rule-sets' attributes which don't + have further sub attributes. + + :param rs_id: rule-set identifier. :param attrib: attribute name :param rule: rule config dictionary. :param opr: True/False. :return: generated command. """ command = self._compute_command( - afi=afi, - name=name, + rs_id=rs_id, attrib=attrib, value=rule[attrib], opr=opr, @@ -808,6 +1041,8 @@ class Firewall_rules(ConfigBase): :param afi: address type :return: rule-set type. """ + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + return "ipv6" if afi == "ipv6" else "ipv4" return "ipv6-name" if afi == "ipv6" else "name" def _is_del(self, l_set, h, key="number"): @@ -834,37 +1069,9 @@ class Firewall_rules(ConfigBase): def _in_target(self, h, key): """ - This function checks whether the target nexist and key present in target config. + This function checks whether the target exists 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_base_attrib(self, key): - """ - This function checks whether key is present in predefined - based attribute set. - :param key: - :return: True/False. - """ - r_set = ( - "p2p", - "ipsec", - "log", - "action", - "fragment", - "protocol", - "disable", - "description", - "mac_address", - "default_action", - "enable_default_log", - ) - return True if key in r_set else False - - def _get_os_version(self): - os_version = "1.2" - if self._connection: - os_version = self._connection.get_device_info()["network_os_version"] - return os_version diff --git a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py index 731014c..62e4f92 100644 --- a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py @@ -275,7 +275,7 @@ class Interfaces(ConfigBase): commands.append( self._compute_commands(key=key, interface=want_copy["name"], remove=True), ) - if have_copy["enabled"] is False: + if have_copy["enabled"] is False and not ('enabled' in want_copy and want_copy["enabled"] is False): commands.append( self._compute_commands(key="enabled", value=True, interface=want_copy["name"]), ) diff --git a/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py index a7652a6..51b4749 100644 --- a/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py @@ -27,9 +27,17 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.u from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces import ( - Ospf_interfacesTemplate, + Ospf_interfacesTemplate ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces_14 import ( + Ospf_interfacesTemplate14 +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion + class Ospf_interfaces(ResourceModule): """ @@ -61,12 +69,30 @@ class Ospf_interfaces(ResourceModule): "passive", ] + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.4"): + self._tmplt = Ospf_interfacesTemplate14() + else: + self._tmplt = Ospf_interfacesTemplate() + + def parse(self): + """ override parse to check template """ + self._validate_template() + return super().parse() + + def get_parser(self, name): + """get_parsers""" + self._validate_template() + return super().get_parser(name) + def execute_module(self): """Execute the module :rtype: A dictionary :returns: The result from module execution """ + self._validate_template() if self.state not in ["parsed", "gathered"]: self.generate_commands() self.run_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 5b47222..3f4da3e 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 @@ -174,9 +174,8 @@ class Firewall_globalFacts(object): :return: generated rule list configuration. """ sp_lst = [] - attrib = "state-policy" - policies = findall(r"^set firewall " + attrib + " (\\S+)", conf, M) - + 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): @@ -197,7 +196,7 @@ class Firewall_globalFacts(object): :param attrib: connection type. :return: generated rule configuration dictionary. """ - a_lst = ["action", "log"] + a_lst = ["action", "log", "log_level"] cfg_dict = self.parse_attr(conf, a_lst, match=attrib) return cfg_dict @@ -304,16 +303,15 @@ class Firewall_globalFacts(object): regex = match + " " + regex if conf: if self.is_bool(attrib): - attr = self.map_regex(attrib, type) - out = conf.find(attr.replace("_", "-")) - dis = conf.find(attr.replace("_", "-") + " 'disable'") - if out >= 1: - if dis >= 1: + # 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 + " (.+)", conf, M) + out = search(r"^.*" + regex + r" (.+)", conf, M) if out: val = out.group(1).strip("'") if self.is_num(attrib): diff --git a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py index ead038a..1fc7025 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py @@ -14,8 +14,6 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import re - from copy import deepcopy from re import M, findall, search @@ -61,15 +59,31 @@ class Firewall_rulesFacts(object): data = self.get_device_data(connection) # split the config into instances of the resource objs = [] - v6_rules = findall(r"^set firewall ipv6-name (?:\'*)(\S+)(?:\'*)", data, M) - v4_rules = findall(r"^set firewall name (?:\'*)(\S+)(?:\'*)", data, M) + # check 1.4+ first + new_rules = True + v6_rules = findall(r"^set firewall ipv6 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", data, M) + if not v6_rules: + v6_rules = findall(r"^set firewall ipv6-name (?:\'*)(\S+)(?:\'*)", data, M) + if v6_rules: + new_rules = False + v4_rules = findall(r"^set firewall ipv4 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", data, M) + if not v4_rules: + v4_rules = findall(r"^set firewall name (?:\'*)(\S+)(?:\'*)", data, M) + if v4_rules: + new_rules = False if v6_rules: - config = self.get_rules(data, v6_rules, type="ipv6") + if new_rules: + config = self.get_rules_post_1_4(data, v6_rules, type="ipv6") + else: + config = self.get_rules(data, v6_rules, type="ipv6") if config: config = utils.remove_empties(config) objs.append(config) if v4_rules: - config = self.get_rules(data, v4_rules, type="ipv4") + if new_rules: + config = self.get_rules_post_1_4(data, v4_rules, type="ipv4") + else: + config = self.get_rules(data, v4_rules, type="ipv4") if config: config = utils.remove_empties(config) objs.append(config) @@ -113,6 +127,39 @@ class Firewall_rulesFacts(object): config = {"afi": "ipv6", "rule_sets": r_v6} return config + def get_rules_post_1_4(self, data, rules, type): + """ + This function performs following: + - Form regex to fetch 'rule-sets' specific config from data. + - Form the rule-set list based on ip address. + Specifically for v1.4+ version. + :param data: configuration. + :param rules: list of rule-sets. + :param type: ip address type. + :return: generated rule-sets configuration. + """ + r_v4 = [] + r_v6 = [] + for kind, name in set(rules): + rule_regex = r" %s %s %s .+$" % (type, kind, name.strip("'")) + cfg = findall(rule_regex, data, M) + fr = self.render_config(cfg, name.strip("'")) + if kind == "name": + fr["name"] = name.strip("'") + elif kind in ("forward", "input", "output"): + fr["filter"] = kind + else: + raise ValueError("Unknown rule kind: %s %s" % kind, name) + if type == "ipv6": + r_v6.append(fr) + else: + r_v4.append(fr) + if r_v4: + config = {"afi": "ipv4", "rule_sets": r_v4} + if r_v6: + config = {"afi": "ipv6", "rule_sets": r_v6} + return config + def render_config(self, conf, match): """ Render config as dictionary structure and delete keys @@ -124,10 +171,12 @@ class Firewall_rulesFacts(object): :returns: The generated config """ conf = "\n".join(filter(lambda x: x, conf)) - a_lst = ["description", "default_action", "enable_default_log"] + a_lst = ["description", "default_action", "default_jump_target", "enable_default_log", "default_log"] config = self.parse_attr(conf, a_lst, match) if not config: config = {} + if 'default_log' in config: + config['enable_default_log'] = config.pop('default_log') config["rules"] = self.parse_rules_lst(conf) return config @@ -169,11 +218,14 @@ class Firewall_rulesFacts(object): "disable", "description", "icmp", + "jump_target", + "queue", + "queue_options", ] rule = self.parse_attr(conf, a_lst) r_sub = { "p2p": self.parse_p2p(conf), - "tcp": self.parse_tcp(conf, "tcp"), + "tcp": self.parse_tcp(conf), "icmp": self.parse_icmp(conf, "icmp"), "time": self.parse_time(conf, "time"), "limit": self.parse_limit(conf, "limit"), @@ -181,10 +233,42 @@ class Firewall_rulesFacts(object): "recent": self.parse_recent(conf, "recent"), "source": self.parse_src_or_dest(conf, "source"), "destination": self.parse_src_or_dest(conf, "destination"), + "inbound_interface": self.parse_interface(conf, "inbound-interface"), + "outbound_interface": self.parse_interface(conf, "outbound-interface"), + "packet_length": self.parse_packet_length(conf, "packet-length"), + "packet_length_exclude": self.parse_packet_length(conf, "packet-length-exclude"), } rule.update(r_sub) return rule + def parse_interface(self, conf, attrib): + """ + This function triggers the parsing of 'interface' attributes. + :param conf: configuration. + :param attrib: 'interface'. + :return: generated config dictionary. + """ + a_lst = ["name", "group"] + cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + return cfg_dict + + def parse_packet_length(self, conf, attrib=None): + """ + This function triggers the parsing of 'packet-length' attributes. + :param conf: configuration. + :param attrib: 'packet-length'. + :return: generated config dictionary. + """ + lengths = [] + rule_regex = r"%s (\d+)" % attrib + found_lengths = findall(rule_regex, conf, M) + if found_lengths: + lengths = [] + for l in set(found_lengths): + obj = {"length": l.strip("'")} + lengths.append(obj) + return lengths + def parse_p2p(self, conf): """ This function forms the regex to fetch the 'p2p' with in @@ -226,15 +310,41 @@ class Firewall_rulesFacts(object): cfg_dict = self.parse_attr(conf, a_lst, match=attrib) return cfg_dict - def parse_tcp(self, conf, attrib=None): + def parse_tcp(self, conf): """ This function triggers the parsing of 'tcp' attributes. :param conf: configuration. :param attrib: 'tcp'. :return: generated config dictionary. """ - cfg_dict = self.parse_attr(conf, ["flags"], match=attrib) - return cfg_dict + f_lst = [] + flags = findall(r"tcp flags (not )?(?:\'*)([\w!,]+)(?:\'*)", conf, M) + # for pre 1.4, this is a string including possible commas + # and ! as an inverter. For 1.4+ this is a single flag per + # command and 'not' as the inverter + if flags: + flag_lst = [] + for n, f in set(flags): + f = f.strip("'").lower() + if "," in f: + # pre 1.4 version with multiple flags + fs = f.split(",") + for f in fs: + if "!" in f: + obj = {"flag": f.strip("'!"), "invert": True} + else: + obj = {"flag": f.strip("'")} + flag_lst.append(obj) + elif "!" in f: + obj = {"flag": f.strip("'!"), "invert": True} + flag_lst.append(obj) + else: + obj = {"flag": f.strip("'")} + if n: + obj["invert"] = True + flag_lst.append(obj) + f_lst = sorted(flag_lst, key=lambda i: i["flag"]) + return {"flags": f_lst} def parse_time(self, conf, attrib=None): """ @@ -276,6 +386,44 @@ class Firewall_rulesFacts(object): cfg_dict = self.parse_attr(conf, a_lst, match=attrib) return cfg_dict + def parse_icmp_attr(self, conf, match): + """ + This function peforms the following: + - parse ICMP arguemnts for firewall rules + - consider that older versions may need numbers or letters + in type, newer ones are more specific + :param conf: configuration. + :param match: parent node/attribute name. + :return: generated config dictionary. + """ + config = {} + if not conf: + return config + + for attrib in ("code", "type", "type-name"): + regex = self.map_regex(attrib) + if match: + regex = match + " " + regex + out = search(r"^.*" + regex + " (.+)", conf, M) + if out: + val = out.group(1).strip("'") + if attrib == 'type-name': + config['type_name'] = val + if attrib == 'code': + config['code'] = int(val) + if attrib == 'type': + # <1.3 could be # (type), #/# (type/code) or 'type' (type_name) + # recent this is only for strings + if "/" in val: # type/code + (type_no, code) = val.split(".") + config['type'] = type_no + config['code'] = code + elif val.isnumeric(): + config['type'] = type_no + else: + config['type_name'] = val + return config + def parse_icmp(self, conf, attrib=None): """ This function triggers the parsing of 'icmp' attributes. @@ -283,11 +431,9 @@ class Firewall_rulesFacts(object): :param attrib: 'icmp'. :return: generated config dictionary. """ - a_lst = ["code", "type", "type_name"] - if attrib == "icmp": - attrib = "icmpv6" - conf = re.sub("icmpv6 type", "icmpv6 type-name", conf) - cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + cfg_dict = self.parse_icmp_attr(conf, "icmp") + if (len(cfg_dict) == 0): + cfg_dict = self.parse_icmp_attr(conf, "icmpv6") return cfg_dict def parse_limit(self, conf, attrib=None): @@ -330,7 +476,6 @@ class Firewall_rulesFacts(object): if conf: if self.is_bool(attrib): out = conf.find(attrib.replace("_", "-")) - dis = conf.find(attrib.replace("_", "-") + " 'disable'") if out >= 1: if dis >= 1: @@ -375,6 +520,7 @@ class Firewall_rulesFacts(object): "disabled", "established", "enable_default_log", + "default_log", ) return True if attrib in bool_set else False diff --git a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py index 995be91..cd8008c 100644 --- a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py @@ -17,7 +17,7 @@ __metaclass__ = type from copy import deepcopy -from re import M, findall +from re import M, findall, search from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils @@ -66,7 +66,7 @@ class InterfacesFacts(object): ) if interface_names: for interface in set(interface_names): - intf_regex = r" %s .+$" % interface.strip("'") + intf_regex = r" %s (.+$)" % interface.strip("'") cfg = findall(intf_regex, data, M) obj = self.render_config(cfg) obj["name"] = interface.strip("'") @@ -106,7 +106,7 @@ class InterfacesFacts(object): if vif_names: vifs_list = [] for vif in set(vif_names): - vif_regex = r" %s .+$" % vif + vif_regex = r"%s (.+$)" % vif cfg = "\n".join(findall(vif_regex, conf, M)) obj = self.parse_attribs(["description", "mtu"], cfg) obj["vlan_id"] = int(vif) @@ -117,6 +117,14 @@ class InterfacesFacts(object): return vifs_list def parse_attribs(self, attribs, conf): + """ + Parse the attributes of a network interface. + + :param attribs: List of attribute names. + :param conf: Configuration string. + :rtype: dict + :returns: Parsed configuration dictionary. + """ config = {} for item in attribs: value = utils.parse_conf_arg(conf, item) @@ -126,7 +134,11 @@ class InterfacesFacts(object): config[item] = value.strip("'") else: config[item] = None - if "disable" in conf: + + # match only on disable next to the interface name + # there are other sub-commands that can be disabled + match = search(r"^ *disable", conf, M) + if match: config["enabled"] = False else: config["enabled"] = True diff --git a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py index be467a0..7d4d1a0 100644 --- a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py @@ -62,7 +62,7 @@ class L3_interfacesFacts(object): # operate on a collection of resource x objs = [] interface_names = re.findall( - r"set interfaces (?:ethernet|bonding|bridge|dummy|tunnel|vti|vxlan) (?:\'*)(\S+)(?:\'*)", + r"set interfaces (?:ethernet|bonding|bridge|dummy|tunnel|vti|loopback|vxlan) (?:\'*)(\S+)(?:\'*)", data, re.M, ) diff --git a/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py index af6c577..c0e7489 100644 --- a/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py @@ -23,9 +23,17 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.osp Ospf_interfacesArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces import ( - Ospf_interfacesTemplate, + Ospf_interfacesTemplate ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces_14 import ( + Ospf_interfacesTemplate14 +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion + class Ospf_interfacesFacts(object): """The vyos ospf_interfaces facts class""" @@ -35,9 +43,30 @@ class Ospf_interfacesFacts(object): self.argument_spec = Ospf_interfacesArgs.argument_spec def get_device_data(self, connection): + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + # use set protocols ospf in order to get both ospf and ospfv3 + return connection.get("show configuration commands | match 'set protocols ospf'") return connection.get('show configuration commands | match "set interfaces"') - def get_config_set(self, data): + def get_config_set_1_4(self, data): + """To classify the configurations beased on interface""" + interface_list = [] + config_set = [] + int_string = "" + for config_line in data.splitlines(): + ospf_int = re.search(r"set protocols (?:ospf|ospfv3) interface (\S+) .*", config_line) + if ospf_int: + if ospf_int.group(1) not in interface_list: + if int_string: + config_set.append(int_string) + interface_list.append(ospf_int.group(1)) + int_string = "" + int_string = int_string + config_line + "\n" + if int_string: + config_set.append(int_string) + return config_set + + def get_config_set_1_2(self, data): """To classify the configurations beased on interface""" interface_list = [] config_set = [] @@ -55,6 +84,12 @@ class Ospf_interfacesFacts(object): config_set.append(int_string) return config_set + def get_config_set(self, data, connection): + """To classify the configurations beased on interface""" + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + return self.get_config_set_1_4(data) + return self.get_config_set_1_2(data) + def populate_facts(self, connection, ansible_facts, data=None): """Populate the facts for Ospf_interfaces network resource @@ -67,16 +102,20 @@ class Ospf_interfacesFacts(object): """ facts = {} objs = [] - ospf_interfaces_parser = Ospf_interfacesTemplate(lines=[], module=self._module) + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + ospf_interface_class = Ospf_interfacesTemplate14 + else: + ospf_interface_class = Ospf_interfacesTemplate + ospf_interfaces_parser = ospf_interface_class(lines=[], module=self._module) if not data: data = self.get_device_data(connection) # parse native config using the Ospf_interfaces template ospf_interfaces_facts = [] - resources = self.get_config_set(data) + resources = self.get_config_set(data, connection) for resource in resources: - ospf_interfaces_parser = Ospf_interfacesTemplate( + ospf_interfaces_parser = ospf_interface_class( lines=resource.split("\n"), module=self._module, ) @@ -93,7 +132,7 @@ class Ospf_interfacesFacts(object): self.argument_spec, {"config": ospf_interfaces_facts}, redact=True, - ), + ) ) if params.get("config"): for cfg in params["config"]: diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py new file mode 100644 index 0000000..7f49d47 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py @@ -0,0 +1,650 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +""" +The Ospf_interfaces parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _get_parameters(data): + if data["afi"] == "ipv6": + val = ["ospfv3", "ipv6"] + else: + val = ["ospf", "ip"] + return val + + +def _tmplt_ospf_int_delete(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + params[0] + " interface {name}".format(**config_data) + ) + + return command + + +def _tmplt_ospf_int_cost(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " cost {cost}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_auth_password(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " authentication plaintext-password {plaintext_password}".format( + **config_data["address_family"]["authentication"] + ) + ) + return command + + +def _tmplt_ospf_int_auth_md5(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " authentication md5 key-id {key_id} ".format( + **config_data["address_family"]["authentication"]["md5_key"] + ) + + "md5-key {key}".format(**config_data["address_family"]["authentication"]["md5_key"]) + ) + + return command + + +def _tmplt_ospf_int_auth_md5_delete(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " authentication" + ) + + return command + + +def _tmplt_ospf_int_bw(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " bandwidth {bandwidth}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_hello_interval(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " hello-interval {hello_interval}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_dead_interval(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " dead-interval {dead_interval}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_mtu_ignore(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " mtu-ignore" + ) + + return command + + +def _tmplt_ospf_int_network(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " network {network}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_priority(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " priority {priority}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_retransmit_interval(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " retransmit-interval {retransmit_interval}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_transmit_delay(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " transmit-delay {transmit_delay}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_ifmtu(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " ifmtu {ifmtu}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_instance(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " instance-id {instance}".format(**config_data["address_family"]) + ) + + return command + + +def _tmplt_ospf_int_passive(config_data): + params = _get_parameters(config_data["address_family"]) + command = ( + "protocols " + + params[0] + + " interface {name}".format(**config_data) + + " passive" + ) + + return command + + +class Ospf_interfacesTemplate14(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(Ospf_interfacesTemplate14, self).__init__( + lines=lines, tmplt=self, prefix=prefix, module=module + ) + + # fmt: off + PARSERS = [ + { + "name": "ip_ospf", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + *$""", + re.VERBOSE, + ), + "remval": _tmplt_ospf_int_delete, + "compval": "address_family", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + } + } + } + }, + { + "name": "authentication_password", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+authentication + \s+plaintext-password + \s+(?P<text>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_auth_password, + "compval": "address_family.authentication", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "authentication": { + "plaintext_password": "{{ text }}" + } + } + } + } + }, + { + "name": "authentication_md5", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+authentication + \s+md5 + \s+key-id + \s+(?P<id>\d+) + \s+md5-key + \s+(?P<text>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_auth_md5, + "remval": _tmplt_ospf_int_auth_md5_delete, + "compval": "address_family.authentication", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "authentication": { + "md5_key": { + "key_id": "{{ id }}", + "key": "{{ text }}" + } + } + } + } + } + }, + { + "name": "bandwidth", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+bandwidth + \s+(?P<bw>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_bw, + "compval": "address_family.bandwidth", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "bandwidth": "{{ bw }}" + } + } + } + }, + { + "name": "cost", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+cost + \s+(?P<val>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_cost, + "compval": "address_family.cost", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "cost": "{{ val }}" + } + } + } + }, + { + "name": "hello_interval", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+hello-interval + \s+(?P<val>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_hello_interval, + "compval": "address_family.hello_interval", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "hello_interval": "{{ val }}" + } + } + } + }, + { + "name": "dead_interval", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+dead-interval + \s+(?P<val>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_dead_interval, + "compval": "address_family.dead_interval", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "dead_interval": "{{ val }}" + } + } + } + }, + { + "name": "mtu_ignore", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+(?P<mtu>\'mtu-ignore\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_mtu_ignore, + "compval": "address_family.mtu_ignore", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "mtu_ignore": "{{ True if mtu is defined }}" + } + } + } + }, + { + "name": "network", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+network + \s+(?P<val>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_network, + "compval": "address_family.network", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "network": "{{ val }}" + } + } + } + }, + { + "name": "priority", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+priority + \s+(?P<val>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_priority, + "compval": "address_family.priority", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "priority": "{{ val }}" + } + } + } + }, + { + "name": "retransmit_interval", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+retransmit-interval + \s+(?P<val>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_retransmit_interval, + "compval": "address_family.retransmit_interval", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "retransmit_interval": "{{ val }}" + } + } + } + }, + { + "name": "transmit_delay", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+transmit-delay + \s+(?P<val>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_transmit_delay, + "compval": "address_family.transmit_delay", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "transmit_delay": "{{ val }}" + } + } + } + }, + { + "name": "ifmtu", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+ifmtu + \s+(?P<val>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_ifmtu, + "compval": "address_family.ifmtu", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "ifmtu": "{{ val }}" + } + } + } + }, + { + "name": "instance", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+instance-id + \s+(?P<val>\'\d+\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_instance, + "compval": "address_family.instance", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "instance": "{{ val }}" + } + } + } + }, + { + "name": "passive", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + \s+(?P<pass>\'passive\') + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_passive, + "compval": "address_family.passive", + "result": { + "name": "{{ name }}", + "address_family": { + '{{ "ipv4" if proto == "ospf" else "ipv6" }}': { + "afi": '{{ "ipv4" if proto == "ospf" else "ipv6" }}', + "passive": "{{ True if pass is defined }}" + } + } + } + }, + { + "name": "interface_name", + "getval": re.compile( + r""" + ^set + \s+protocols + \s+(?P<proto>ospf|ospfv3) + \s+interface + \s+(?P<name>\S+) + .*$""", + re.VERBOSE, + ), + "setval": "set protocols {{ proto }} interface {{ name }}", + "result": { + "name": "{{ name }}", + } + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/utils/version.py b/plugins/module_utils/network/vyos/utils/version.py new file mode 100644 index 0000000..cc3028c --- /dev/null +++ b/plugins/module_utils/network/vyos/utils/version.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Provide version object to compare version numbers.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.module_utils.compat.version import LooseVersion # pylint: disable=unused-import diff --git a/plugins/module_utils/network/vyos/vyos.py b/plugins/module_utils/network/vyos/vyos.py index 4fcb331..1430b1b 100644 --- a/plugins/module_utils/network/vyos/vyos.py +++ b/plugins/module_utils/network/vyos/vyos.py @@ -34,7 +34,6 @@ import json from ansible.module_utils._text import to_text from ansible.module_utils.connection import Connection, ConnectionError - _DEVICE_CONFIGS = {} @@ -100,3 +99,10 @@ def load_config(module, commands, commit=False, comment=None): module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) return response.get("diff") + + +def get_os_version(module): + connection = get_connection(module) + if connection.get_device_info(): + os_version = connection.get_device_info()["network_os_major_version"] + return os_version diff --git a/plugins/modules/vyos_firewall_global.py b/plugins/modules/vyos_firewall_global.py index 205ef13..befe5e7 100644 --- a/plugins/modules/vyos_firewall_global.py +++ b/plugins/modules/vyos_firewall_global.py @@ -253,6 +253,19 @@ options: description: - Enable logging of packets part of an established connection. type: bool + log_level: + description: + - Only available in 1.4+ + type: str + choices: + - emerg + - alert + - crit + - err + - warn + - notice + - info + - debug running_config: description: - The module, by default, will connect to the remote device and retrieve the current diff --git a/plugins/modules/vyos_firewall_rules.py b/plugins/modules/vyos_firewall_rules.py index 06a300f..fd2e7d5 100644 --- a/plugins/modules/vyos_firewall_rules.py +++ b/plugins/modules/vyos_firewall_rules.py @@ -31,6 +31,11 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network' +} DOCUMENTATION = """ module: vyos_firewall_rules @@ -62,9 +67,16 @@ options: type: list elements: dict suboptions: + filter: + description: + - Filter type (exclusive to "name"). + - Supported in 1.4 and later. + type: str + choices: ['input', 'output', 'forward'] name: description: - Firewall rule set name. + - Required for 1.3- and optional for 1.4+. type: str default_action: description: @@ -72,11 +84,15 @@ options: - drop (Drop if no prior rules are hit (default)) - reject (Drop and notify source if no prior rules are hit) - accept (Accept if no prior rules are hit) + - jump (Jump to another rule-set, 1.4+) + type: str + choices: ['drop', 'reject', 'accept', 'jump'] + default_jump_target: + description: + - Default jump target if the default action is jump. + - Only valid in 1.4 and later. + - Only valid when default_action = jump. type: str - choices: - - drop - - reject - - accept description: description: - Rule set description. @@ -103,12 +119,19 @@ options: action: description: - Specifying the action. + - inspect is available < 1.4 + - continue, return, jump, queue, synproxy are available >= 1.4 type: str choices: - drop - reject - accept - inspect + - continue + - return + - jump + - queue + - synproxy destination: description: - Specifying the destination parameters. @@ -148,6 +171,7 @@ options: disable: description: - Option to disable firewall rule. + - aliased to disabled type: bool aliases: ["disabled"] fragment: @@ -215,6 +239,21 @@ options: description: - ICMP type. type: int + inbound_interface: + description: + - Inbound interface. + - Only valid in 1.4 and later. + type: dict + suboptions: + name: + description: + - Interface name. + - Can have wildcards + type: str + group: + description: + - Interface group. + type: str ipsec: description: - Inbound ip sec packets. @@ -222,13 +261,16 @@ options: choices: - match-ipsec - match-none - log: + - match-ipsec-in + - match-ipsec-out + - match-none-in + - match-none-out + jump_target: description: - - Option to log packets matching rule + - Jump target if the action is jump. + - Only valid in 1.4 and later. + - Only valid when action = jump. type: str - choices: - - disable - - enable limit: description: - Rate limit using a token bucket filter. @@ -255,6 +297,55 @@ options: description: - This is the time unit. type: str + log: + description: + - Log matching packets. + type: str + choices: ['disable', 'enable'] + outbound_interface: + description: + - Match outbound interface. + - Only valid in 1.4 and later. + type: dict + suboptions: + name: + description: + - Interface name. + - Can have wildcards + type: str + group: + description: + - Interface group. + type: str + packet_length: + description: + - Packet length match. + - Only valid in 1.4 and later. + - Multiple values from 1 to 65535 and ranges are supported + type: list + elements: dict + suboptions: + length: + description: + - Packet length or range. + type: str + packet_length_exclude: + description: + - Packet length match. + - Only valid in 1.4 and later. + - Multiple values from 1 to 65535 and ranges are supported + type: list + elements: dict + suboptions: + length: + description: + - Packet length or range. + type: str + packet_type: + description: + - Packet type match. + type: str + choices: ['broadcast', 'multicast', 'host', 'other'] p2p: description: - P2P application packets. @@ -283,6 +374,20 @@ options: - all All IP protocols. - (!)All IP protocols except for the specified name or number. type: str + queue: + description: + - Queue options. + - Only valid in 1.4 and later. + - Only valid when action = queue. + - Can be a queue number or range. + type: str + queue_options: + description: + - Queue options. + - Only valid in 1.4 and later. + - Only valid when action = queue. + type: str + choices: ['bypass', 'fanout'] recent: description: - Parameters for matching recently seen sources. @@ -295,7 +400,8 @@ options: time: description: - Source addresses seen in the last N seconds. - type: int + - Since 1.4, this is a string of second/minute/hour + type: str source: description: - Source parameters. @@ -337,6 +443,12 @@ options: - <MAC address> MAC address to match. - <!MAC address> Match everything except the specified MAC address. type: str + fqdn: + description: + - Fully qualified domain name. + - Available in 1.4 and later. + type: str + state: description: - Session state. @@ -358,6 +470,21 @@ options: description: - Related state. type: bool + synproxy: + description: + - SYN proxy options. + - Only valid in 1.4 and later. + - Only valid when action = synproxy. + type: dict + suboptions: + mss: + description: + - Adjust MSS (501-65535) + type: int + window_scale: + description: + - Window scale (1-14). + type: int tcp: description: - TCP flags to match. @@ -365,8 +492,22 @@ options: suboptions: flags: description: - - TCP flags to be matched. - type: str + - list of tcp flags to be matched + - 5.0 breaking change to support 1.4+ and 1.3- + type: list + elements: dict + suboptions: + flag: + description: + - TCP flag to be matched. + - syn, ack, fin, rst, urg, psh, all (1.3-) + - syn, ack, fin, rst, urg, psh, cwr, ecn (1.4+) + type: str + choices: ['ack', 'cwr', 'ecn', 'fin', 'psh', 'rst', 'syn', 'urg', 'all'] + invert: + description: + - Invert the match. + type: bool time: description: - Time to match rule. @@ -1460,14 +1601,14 @@ RETURN = """ before: description: The configuration prior to the model invocation. returned: always - type: list + type: dict sample: > The configuration returned will always be in the same format of the parameters above. after: description: The resulting configuration model invocation. returned: when changed - type: list + type: dict sample: > The configuration returned will always be in the same format of the parameters above. @@ -1486,17 +1627,14 @@ commands: from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.firewall_rules.firewall_rules import ( - Firewall_rulesArgs, -) -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.firewall_rules.firewall_rules import ( - Firewall_rules, -) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.firewall_rules.firewall_rules import Firewall_rulesArgs +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.firewall_rules.firewall_rules import Firewall_rules def main(): """ Main entry point for module execution + :returns: the result form module invocation """ required_if = [ @@ -1518,5 +1656,5 @@ def main(): module.exit_json(**result) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/plugins/modules/vyos_ospf_interfaces.py b/plugins/modules/vyos_ospf_interfaces.py index c232689..3329058 100644 --- a/plugins/modules/vyos_ospf_interfaces.py +++ b/plugins/modules/vyos_ospf_interfaces.py @@ -901,7 +901,7 @@ def main(): argument_spec=Ospf_interfacesArgs.argument_spec, mutually_exclusive=[], required_if=[], - supports_check_mode=False, + supports_check_mode=True, ) result = Ospf_interfaces(module).execute_module() diff --git a/requirements.txt b/requirements.txt index e4c0f75..ee91c10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -ansible-pylibssh paramiko scp diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt new file mode 100644 index 0000000..c835eef --- /dev/null +++ b/tests/sanity/ignore-2.19.txt @@ -0,0 +1 @@ +plugins/action/vyos.py action-plugin-docs # base class for deprecated network platform modules using `connection: local` 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 new file mode 100644 index 0000000..7b281de --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg @@ -0,0 +1,16 @@ +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 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' diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config.cfg index a3aec78..f1fdf1e 100644 --- a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config.cfg +++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config.cfg @@ -9,6 +9,7 @@ set firewall name V4-INGRESS rule 101 set firewall name V4-INGRESS rule 101 'disable' set firewall name V4-INGRESS rule 101 action 'accept' set firewall name V4-INGRESS rule 101 ipsec 'match-ipsec' +set firewall name V4-INGRESS rule 101 log 'enable' set firewall name EGRESS default-action 'reject' set firewall ipv6-name EGRESS default-action 'reject' set firewall ipv6-name EGRESS rule 20 diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config_v14.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config_v14.cfg new file mode 100644 index 0000000..ef596cd --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config_v14.cfg @@ -0,0 +1,26 @@ +set firewall ipv4 name V4-INGRESS default-action 'accept' +set firewall ipv6 name V6-INGRESS default-action 'accept' +set firewall ipv4 name V4-INGRESS description 'This is IPv4 V4-INGRESS rule set' +set firewall ipv4 name V4-INGRESS default-log +set firewall ipv4 name V4-INGRESS rule 101 protocol 'icmp' +set firewall ipv4 name V4-INGRESS rule 101 description 'Rule 101 is configured by Ansible' +set firewall ipv4 name V4-INGRESS rule 101 packet-length-exclude 100 +set firewall ipv4 name V4-INGRESS rule 101 packet-length-exclude 300 +set firewall ipv4 name V4-INGRESS rule 101 log +set firewall ipv4 name V4-INGRESS rule 101 +set firewall ipv4 name V4-INGRESS rule 101 'disable' +set firewall ipv4 name V4-INGRESS rule 101 action 'accept' +set firewall ipv4 name EGRESS default-action 'reject' +set firewall ipv6 name EGRESS default-action 'reject' +set firewall ipv6 name EGRESS rule 20 +set firewall ipv6 name EGRESS rule 20 icmpv6 type-name 'echo-request' +set firewall ipv6 input filter 1 jump-target 'V6-INGRESS' +set firewall ipv6 output filter 1 jump-target 'EGRESS' +set firewall ipv4 input filter 1 jump-target 'INGRESS' +set firewall ipv4 output filter 1 jump-target 'EGRESS' +set firewall ipv4 name IF-TEST rule 10 'disable' +set firewall ipv4 name IF-TEST rule 10 action 'accept' +set firewall ipv4 name IF-TEST rule 10 inbound-interface name 'eth0' +set firewall ipv4 name IF-TEST rule 10 outbound-interface group 'the-ethers' +set firewall ipv4 name IF-TEST rule 10 icmp type-name 'echo-request' +set firewall ipv4 name IF-TEST rule 10 state 'related' diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config.cfg index bed0b01..175a656 100644 --- a/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config.cfg +++ b/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config.cfg @@ -3,6 +3,7 @@ set interfaces ethernet eth0 hw-id '08:00:27:7c:85:05' set interfaces ethernet eth1 description 'test-interface' set interfaces ethernet eth2 hw-id '08:00:27:04:85:99' set interfaces ethernet eth3 hw-id '08:00:27:1c:82:d1' +set interfaces ethernet eth3 disable set interfaces ethernet eth3 description 'Ethernet 3' set interfaces wireguard wg02 description 'wire guard int 2' set interfaces loopback 'lo' diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_ospf_interfaces_config_14.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_ospf_interfaces_config_14.cfg new file mode 100644 index 0000000..d630d94 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_ospf_interfaces_config_14.cfg @@ -0,0 +1,4 @@ +set protocols ospfv3 interface eth0 instance-id '33' +set protocols ospfv3 interface eth0 'mtu-ignore' +set protocols ospf interface eth1 cost '100' +set protocols ospfv3 interface eth1 ifmtu '33' diff --git a/tests/unit/modules/network/vyos/test_vyos_facts.py b/tests/unit/modules/network/vyos/test_vyos_facts.py index dd3a796..7e192e3 100644 --- a/tests/unit/modules/network/vyos/test_vyos_facts.py +++ b/tests/unit/modules/network/vyos/test_vyos_facts.py @@ -54,6 +54,7 @@ class TestVyosFactsModule(TestVyosModule): "network_os_hostname": "vyos01", "network_os_model": "VMware", "network_os_version": "VyOS 1.1.7", + "network_os_major_version": "1.1", }, "network_api": "cliconf", } diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_global.py b/tests/unit/modules/network/vyos/test_vyos_firewall_global.py index 25c5632..0cc611c 100644 --- a/tests/unit/modules/network/vyos/test_vyos_firewall_global.py +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global.py @@ -58,6 +58,12 @@ class TestVyosFirewallRulesModule(TestVyosModule): "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.2" + self.execute_show_command = self.mock_execute_show_command.start() def tearDown(self): @@ -67,6 +73,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): 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): @@ -89,6 +96,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): connection_type="established", action="accept", log=True, + log_level="emerg", ), dict(connection_type="invalid", action="reject"), ], @@ -358,5 +366,123 @@ class TestVyosFirewallRulesModule(TestVyosModule): def test_vyos_firewall_global_set_01_deleted(self): set_module_args(dict(config=dict(), state="deleted")) - commands = ["delete firewall "] + commands = ["delete firewall"] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_01_replaced_version(self): + self.get_os_version.return_value = "1.4" + 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, + ), + 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=True, + 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")], + ) + ], + ), + ), + 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 send-redirects 'enable'", + "set firewall global-options config-trap 'enable'", + "set firewall global-options ipv6-src-route 'enable'", + "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 'enable'", + "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'", + ] self.execute_module(changed=True, commands=commands) diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py new file mode 100644 index 0000000..c594a1f --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py @@ -0,0 +1,460 @@ +# (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 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=True, + 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")], + ) + ], + ), + ), + 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 send-redirects 'enable'", + "set firewall global-options config-trap 'enable'", + "set firewall global-options ipv6-src-route 'enable'", + "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 'enable'", + "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'", + ] + 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="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="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"), + ], + ), + ], + 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")], + ), + ], + ), + ), + state="replaced", + ), + ) + commands = [ + "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 global-options all-ping", + "delete firewall global-options state-policy related", + "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", + "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_replaced_idem(self): + set_module_args( + dict( + config=dict( + ping=dict(all=True), + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + 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.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="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"), + ], + ), + ], + 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")], + ), + ], + ), + ), + state="replaced", + ), + ) + commands = [ + "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 global-options all-ping", + "set firewall global-options state-policy related action 'drop'", + "delete firewall global-options state-policy related log-level", + "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", + "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 global-options"] + self.execute_module(changed=True, commands=commands) diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_rules.py b/tests/unit/modules/network/vyos/test_vyos_firewall_rules.py index b43b11c..c0815bf 100644 --- a/tests/unit/modules/network/vyos/test_vyos_firewall_rules.py +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_rules.py @@ -63,10 +63,10 @@ class TestVyosFirewallRulesModule(TestVyosModule): self.execute_show_command = self.mock_execute_show_command.start() self.mock_get_os_version = patch( - "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.firewall_rules.firewall_rules.Firewall_rules._get_os_version", + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.firewall_rules.firewall_rules.get_os_version", ) self.get_os_version = self.mock_get_os_version.start() - self.get_os_version.return_value = "Vyos 1.2" + self.get_os_version.return_value = "1.2" def tearDown(self): super(TestVyosFirewallRulesModule, self).tearDown() @@ -374,7 +374,12 @@ class TestVyosFirewallRulesModule(TestVyosModule): weekdays="!Sat,Sun", utc=True, ), - tcp=dict(flags="ALL"), + tcp=dict( + flags=[ + dict(flag="all"), + ] + ), + ), ], ), @@ -416,7 +421,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): description="Rule 101 is configured by Ansible", ipsec="match-ipsec", protocol="icmp", - disabled=True, + disable=True, icmp=dict(type_name="echo-request"), ), ], @@ -566,8 +571,22 @@ class TestVyosFirewallRulesModule(TestVyosModule): weekdays="!Sat,Sun", utc=True, ), - tcp=dict(flags="ALL"), + tcp=dict( + flags=[ + dict(flag="all"), + ] + ), ), + dict( + number="102", + tcp=dict( + flags=[ + dict(flag="ack"), + dict(flag="syn"), + dict(flag="fin", invert=True), + ], + ) + ) ], ), ], @@ -586,6 +605,8 @@ class TestVyosFirewallRulesModule(TestVyosModule): "set firewall ipv6-name INBOUND rule 101 time weekdays !Sat,Sun", "set firewall ipv6-name INBOUND rule 101 time stoptime 13:30:00", "set firewall ipv6-name INBOUND rule 101 time starttime 13:20:00", + "set firewall ipv6-name INBOUND rule 102", + "set firewall ipv6-name INBOUND rule 102 tcp flags ACK,SYN,!FIN", ] self.execute_module(changed=True, commands=commands) @@ -743,14 +764,14 @@ class TestVyosFirewallRulesModule(TestVyosModule): ipsec="match-ipsec", protocol="tcp", fragment="match-frag", - disabled=False, + disable=False, ), dict( number="102", action="accept", description="Rule 102 is configured by Ansible RM", protocol="icmp", - disabled=True, + disable=True, ), ], ), @@ -787,6 +808,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): "set firewall name V4-INGRESS rule 101 protocol 'tcp'", "set firewall name V4-INGRESS rule 101 description 'Rule 101 is configured by Ansible RM'", "set firewall name V4-INGRESS rule 101 action 'reject'", + "delete firewall name V4-INGRESS rule 101 log", "set firewall name V4-INGRESS rule 102 disable", "set firewall name V4-INGRESS rule 102 action 'accept'", "set firewall name V4-INGRESS rule 102 protocol 'icmp'", @@ -817,7 +839,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): ipsec="match-ipsec", protocol="icmp", fragment="match-frag", - disabled=True, + disable=True, ), ], ), @@ -848,6 +870,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): ) commands = [ "delete firewall name V4-INGRESS enable-default-log", + "delete firewall name V4-INGRESS rule 101 log", ] self.execute_module(changed=True, commands=commands) @@ -871,8 +894,9 @@ class TestVyosFirewallRulesModule(TestVyosModule): ipsec="match-ipsec", protocol="icmp", fragment="match-frag", - disabled=True, - ), + disable=True, + log="enable", + ) ], ), dict( @@ -926,7 +950,8 @@ class TestVyosFirewallRulesModule(TestVyosModule): ipsec="match-ipsec", protocol="icmp", fragment="match-frag", - disabled=True, + disable=True, + log="enable" ), ], ), @@ -958,8 +983,8 @@ class TestVyosFirewallRulesModule(TestVyosModule): ipsec="match-ipsec", protocol="icmp", fragment="match-frag", - disabled=True, - ), + disable=True, + ) ], ), dict( @@ -1014,7 +1039,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): log="enable", protocol="tcp", fragment="match-frag", - disabled=False, + disable=False, source=dict( group=dict( address_group="IN-ADDR-GROUP", @@ -1028,7 +1053,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): action="accept", description="Rule 102 is configured by Ansible RM", protocol="icmp", - disabled=True, + disable=True, ), ], ), @@ -1103,8 +1128,9 @@ class TestVyosFirewallRulesModule(TestVyosModule): ipsec="match-ipsec", protocol="icmp", fragment="match-frag", - disabled=True, - ), + disable=True, + log="enable", + ) ], ), dict( @@ -1139,7 +1165,7 @@ class TestVyosFirewallRulesModule(TestVyosModule): self.execute_module(changed=False, commands=[]) def test_vyos_firewall_v6_rule_sets_rule_merged_01_version(self): - self.get_os_version.return_value = "VyOS 1.4-rolling-202007010117" + self.get_os_version.return_value = "1.4" set_module_args( dict( config=[ @@ -1158,8 +1184,16 @@ class TestVyosFirewallRulesModule(TestVyosModule): description="Rule 101 is configured by Ansible", ipsec="match-ipsec", protocol="icmp", - disabled=True, + disable=True, icmp=dict(type_name="echo-request"), + log="enable", + ), + dict( + number="102", + action="reject", + description="Rule 102 is configured by Ansible", + protocol="ipv6-icmp", + icmp=dict(type=7), ), ], ), @@ -1170,15 +1204,503 @@ class TestVyosFirewallRulesModule(TestVyosModule): ), ) commands = [ - "set firewall ipv6-name INBOUND default-action 'accept'", - "set firewall ipv6-name INBOUND description 'This is IPv6 INBOUND rule set'", - "set firewall ipv6-name INBOUND enable-default-log", - "set firewall ipv6-name INBOUND rule 101 protocol 'icmp'", - "set firewall ipv6-name INBOUND rule 101 description 'Rule 101 is configured by Ansible'", - "set firewall ipv6-name INBOUND rule 101", - "set firewall ipv6-name INBOUND rule 101 disable", - "set firewall ipv6-name INBOUND rule 101 action 'accept'", - "set firewall ipv6-name INBOUND rule 101 ipsec 'match-ipsec'", - "set firewall ipv6-name INBOUND rule 101 icmpv6 type-name echo-request", + "set firewall ipv6 name INBOUND default-action 'accept'", + "set firewall ipv6 name INBOUND description 'This is IPv6 INBOUND rule set'", + "set firewall ipv6 name INBOUND default-log", + "set firewall ipv6 name INBOUND rule 101 protocol 'icmp'", + "set firewall ipv6 name INBOUND rule 101 description 'Rule 101 is configured by Ansible'", + "set firewall ipv6 name INBOUND rule 101", + "set firewall ipv6 name INBOUND rule 101 disable", + "set firewall ipv6 name INBOUND rule 101 action 'accept'", + "set firewall ipv6 name INBOUND rule 101 ipsec 'match-ipsec'", + "set firewall ipv6 name INBOUND rule 101 icmpv6 type-name echo-request", + "set firewall ipv6 name INBOUND rule 101 log 'enable'", + "set firewall ipv6 name INBOUND rule 102", + "set firewall ipv6 name INBOUND rule 102 action 'reject'", + "set firewall ipv6 name INBOUND rule 102 description 'Rule 102 is configured by Ansible'", + "set firewall ipv6 name INBOUND rule 102 protocol 'ipv6-icmp'", + 'set firewall ipv6 name INBOUND rule 102 icmpv6 type 7', + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_jump_rules_merged_01(self): + self.get_os_version.return_value = "1.4" + set_module_args( + dict( + config=[ + dict( + afi="ipv6", + rule_sets=[ + dict( + name="INBOUND", + description="This is IPv6 INBOUND rule set with a jump action", + default_action="accept", + enable_default_log=True, + rules=[ + dict( + number="101", + action="jump", + description="Rule 101 is configured by Ansible", + ipsec="match-ipsec", + protocol="icmp", + icmp=dict(type_name="echo-request"), + jump_target="PROTECT-RE", + packet_length_exclude=[dict(length=100), dict(length=200)] + ), + dict( + number="102", + action="reject", + description="Rule 102 is configured by Ansible", + protocol="ipv6-icmp", + icmp=dict(type=7), + ), + ], + ), + ], + ) + ], + state="merged", + ) + ) + commands = [ + "set firewall ipv6 name INBOUND default-action 'accept'", + "set firewall ipv6 name INBOUND description 'This is IPv6 INBOUND rule set with a jump action'", + "set firewall ipv6 name INBOUND default-log", + "set firewall ipv6 name INBOUND rule 101 protocol 'icmp'", + "set firewall ipv6 name INBOUND rule 101 packet-length-exclude 100", + "set firewall ipv6 name INBOUND rule 101 packet-length-exclude 200", + "set firewall ipv6 name INBOUND rule 101 description 'Rule 101 is configured by Ansible'", + "set firewall ipv6 name INBOUND rule 101", + "set firewall ipv6 name INBOUND rule 101 ipsec 'match-ipsec'", + "set firewall ipv6 name INBOUND rule 101 icmpv6 type-name echo-request", + "set firewall ipv6 name INBOUND rule 101 action 'jump'", + "set firewall ipv6 name INBOUND rule 101 jump-target 'PROTECT-RE'", + "set firewall ipv6 name INBOUND rule 102", + "set firewall ipv6 name INBOUND rule 102 action 'reject'", + "set firewall ipv6 name INBOUND rule 102 description 'Rule 102 is configured by Ansible'", + "set firewall ipv6 name INBOUND rule 102 protocol 'ipv6-icmp'", + 'set firewall ipv6 name INBOUND rule 102 icmpv6 type 7', + ] + self.execute_module(changed=True, commands=commands) + + +class TestVyosFirewallRulesModule14(TestVyosModule): + module = vyos_firewall_rules + + 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.static_routes.static_routes.Static_routesFacts.get_device_data" + ) + + self.mock_execute_show_command = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_rules.firewall_rules.Firewall_rulesFacts.get_device_data" + ) + self.execute_show_command = self.mock_execute_show_command.start() + + self.mock_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.firewall_rules.firewall_rules.get_os_version" + ) + self.get_os_version = self.mock_get_os_version.start() + self.get_os_version.return_value = "1.4" + 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_rules_config_v14.cfg") + + self.execute_show_command.side_effect = load_from_file + + def test_vyos_firewall_packet_length_merged_01(self): + set_module_args( + dict( + config=[ + dict( + afi="ipv6", + rule_sets=[ + dict( + name="INBOUND", + description="This is IPv6 INBOUND rule set with a jump action", + default_action="accept", + enable_default_log=True, + rules=[ + dict( + number="101", + action="jump", + description="Rule 101 is configured by Ansible", + jump_target="PROTECT-RE", + packet_length_exclude=[dict(length=100), dict(length=200)], + packet_length=[dict(length=22)] + ), + ], + ), + ], + ) + ], + state="merged", + ) + ) + commands = [ + "set firewall ipv6 name INBOUND default-action 'accept'", + "set firewall ipv6 name INBOUND description 'This is IPv6 INBOUND rule set with a jump action'", + "set firewall ipv6 name INBOUND default-log", + "set firewall ipv6 name INBOUND rule 101 packet-length-exclude 100", + "set firewall ipv6 name INBOUND rule 101 packet-length-exclude 200", + "set firewall ipv6 name INBOUND rule 101 packet-length 22", + "set firewall ipv6 name INBOUND rule 101 description 'Rule 101 is configured by Ansible'", + "set firewall ipv6 name INBOUND rule 101", + "set firewall ipv6 name INBOUND rule 101 action 'jump'", + "set firewall ipv6 name INBOUND rule 101 jump-target 'PROTECT-RE'", + ] + self.maxDiff = None + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_packet_length_replace_01(self): + set_module_args( + dict( + config=[ + dict( + afi="ipv4", + rule_sets=[ + dict( + name="V4-INGRESS", + description="This is IPv4 V4-INGRESS rule set", + default_action="accept", + enable_default_log=True, + rules=[ + dict( + number="101", + action="accept", + description="Rule 101 is configured by Ansible", + packet_length_exclude=[dict(length=100), dict(length=200)], + packet_length=[dict(length=22)] + ), + ], + ), + ], + ) + ], + state="replaced", + ) + ) + commands = [ + "delete firewall ipv4 name V4-INGRESS rule 101 protocol", + "delete firewall ipv4 name V4-INGRESS rule 101 disable", + "delete firewall ipv4 name V4-INGRESS rule 101 packet-length-exclude 300", + "set firewall ipv4 name V4-INGRESS rule 101 packet-length-exclude 200", + "set firewall ipv4 name V4-INGRESS rule 101 packet-length 22", + ] + self.maxDiff = None + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_filter_merged_01(self): + set_module_args( + dict( + config=[ + dict( + afi="ipv6", + rule_sets=[ + dict( + filter="input", + description="This is IPv6 INBOUND rule set with a jump action", + default_action="accept", + enable_default_log=True, + rules=[ + dict( + number="101", + action="jump", + description="Rule 101 is configured by Ansible", + jump_target="PROTECT-RE", + packet_length_exclude=[dict(length=100), dict(length=200)], + packet_length=[dict(length=22)] + ), + ], + ), + ], + ) + ], + state="merged", + ) + ) + commands = [ + "set firewall ipv6 input filter default-action 'accept'", + "set firewall ipv6 input filter description 'This is IPv6 INBOUND rule set with a jump action'", + "set firewall ipv6 input filter default-log", + "set firewall ipv6 input filter rule 101 packet-length-exclude 100", + "set firewall ipv6 input filter rule 101 packet-length-exclude 200", + "set firewall ipv6 input filter rule 101 packet-length 22", + "set firewall ipv6 input filter rule 101 description 'Rule 101 is configured by Ansible'", + "set firewall ipv6 input filter rule 101", + "set firewall ipv6 input filter rule 101 action 'jump'", + "set firewall ipv6 input filter rule 101 jump-target 'PROTECT-RE'", + ] + self.maxDiff = None + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_interface_merged_01(self): + set_module_args( + dict( + config=[ + dict( + afi="ipv6", + rule_sets=[ + dict( + name="V6-INGRESS", + description="This is IPv6 INBOUND rule set with a jump action", + default_action="accept", + rules=[ + dict( + number="101", + action="jump", + description="Rule 101 is configured by Ansible", + jump_target="PROTECT-RE", + inbound_interface=dict(name="eth0"), + outbound_interface=dict(group="eth1"), + ), + ], + ), + ], + ) + ], + state="merged", + ) + ) + commands = [ + "set firewall ipv6 name V6-INGRESS description 'This is IPv6 INBOUND rule set with a jump action'", + "set firewall ipv6 name V6-INGRESS rule 101 inbound-interface name eth0", + "set firewall ipv6 name V6-INGRESS rule 101 outbound-interface group eth1", + "set firewall ipv6 name V6-INGRESS rule 101 description 'Rule 101 is configured by Ansible'", + "set firewall ipv6 name V6-INGRESS rule 101", + "set firewall ipv6 name V6-INGRESS rule 101 action 'jump'", + "set firewall ipv6 name V6-INGRESS rule 101 jump-target 'PROTECT-RE'", + ] + self.maxDiff = None + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_interface_replace_02(self): + set_module_args( + dict( + config=[ + dict( + afi="ipv4", + rule_sets=[ + dict( + name="IF-TEST", + description="Changed", + rules=[ + dict( + number="10", + action="accept", + description="Rule 10 is configured by Ansible", + inbound_interface=dict(name="eth1"), + ), + ], + ), + ], + ) + ], + state="replaced", + ) + ) + commands = [ + "set firewall ipv4 name IF-TEST description 'Changed'", + "set firewall ipv4 name IF-TEST rule 10 description 'Rule 10 is configured by Ansible'", + 'set firewall ipv4 name IF-TEST rule 10 inbound-interface name eth1', + "delete firewall ipv4 name IF-TEST rule 10 outbound-interface group", + "delete firewall ipv4 name IF-TEST rule 10 disable", + "delete firewall ipv4 name IF-TEST rule 10 state related", + "delete firewall ipv4 name IF-TEST rule 10 icmp type-name echo-request", + ] + self.maxDiff = None + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_v4_rule_sets_rule_merged_02(self): + set_module_args( + dict( + config=[ + dict( + afi="ipv4", + rule_sets=[ + dict( + name="INBOUND", + rules=[ + dict( + number="101", + protocol="tcp", + source=dict( + address="192.0.2.0", + mac_address="38:00:25:19:76:0c", + port=2127, + ), + destination=dict(address="192.0.1.0", port=2124), + limit=dict( + burst=10, + rate=dict(number=20, unit="second"), + ), + recent=dict(count=10, time=20), + state=dict( + established=True, + related=True, + invalid=True, + new=True, + ), + ), + ], + ), + ], + ), + ], + state="merged", + ), + ) + commands = [ + "set firewall ipv4 name INBOUND rule 101 protocol 'tcp'", + "set firewall ipv4 name INBOUND rule 101 destination port 2124", + "set firewall ipv4 name INBOUND rule 101", + "set firewall ipv4 name INBOUND rule 101 destination address 192.0.1.0", + "set firewall ipv4 name INBOUND rule 101 source address 192.0.2.0", + "set firewall ipv4 name INBOUND rule 101 source mac-address 38:00:25:19:76:0c", + "set firewall ipv4 name INBOUND rule 101 source port 2127", + "set firewall ipv4 name INBOUND rule 101 state new", + "set firewall ipv4 name INBOUND rule 101 state invalid", + "set firewall ipv4 name INBOUND rule 101 state related", + "set firewall ipv4 name INBOUND rule 101 state established", + "set firewall ipv4 name INBOUND rule 101 limit burst 10", + "set firewall ipv4 name INBOUND rule 101 limit rate 20/second", + "set firewall ipv4 name INBOUND rule 101 recent count 10", + "set firewall ipv4 name INBOUND rule 101 recent time 20", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_v4_rule_sets_change_state_01(self): + set_module_args( + dict( + config=[ + dict( + afi="ipv4", + rule_sets=[ + dict( + name="IF-TEST", + rules=[ + dict( + number="10", + disable=False, + action="accept", + state=dict( + established=True, + new=True, + ), + ), + ], + ), + ], + ), + ], + state="replaced", + ), + ) + commands = [ + "delete firewall ipv4 name IF-TEST rule 10 disable", + "delete firewall ipv4 name IF-TEST rule 10 inbound-interface name", + "delete firewall ipv4 name IF-TEST rule 10 icmp type-name echo-request", + "delete firewall ipv4 name IF-TEST rule 10 outbound-interface group", + "delete firewall ipv4 name IF-TEST rule 10 state related", + "set firewall ipv4 name IF-TEST rule 10 state established", + "set firewall ipv4 name IF-TEST rule 10 state new", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_v4v6_rule_sets_del_03(self): + set_module_args(dict(config=[], state="deleted")) + commands = ["delete firewall ipv4", "delete firewall ipv6"] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_v6_rule_sets_rule_merged_04(self): + set_module_args( + dict( + config=[ + dict( + afi="ipv6", + rule_sets=[ + dict( + name="INBOUND", + rules=[ + dict( + number="101", + time=dict( + monthdays="2", + startdate="2020-01-24", + starttime="13:20:00", + stopdate="2020-01-28", + stoptime="13:30:00", + weekdays="!Sat,Sun", + utc=True, + ), + tcp=dict( + flags=[ + dict(flag="all"), + ] + ), + ), + dict( + number="102", + tcp=dict( + flags=[ + dict(flag="ack"), + dict(flag="syn"), + dict(flag="fin", invert=True), + ], + ) + ) + ], + ), + ], + ), + ], + state="merged", + ), + ) + commands = [ + "set firewall ipv6 name INBOUND rule 101", + "set firewall ipv6 name INBOUND rule 101 tcp flags all", + "set firewall ipv6 name INBOUND rule 101 time utc", + "set firewall ipv6 name INBOUND rule 101 time monthdays 2", + "set firewall ipv6 name INBOUND rule 101 time startdate 2020-01-24", + "set firewall ipv6 name INBOUND rule 101 time stopdate 2020-01-28", + "set firewall ipv6 name INBOUND rule 101 time weekdays !Sat,Sun", + "set firewall ipv6 name INBOUND rule 101 time stoptime 13:30:00", + "set firewall ipv6 name INBOUND rule 101 time starttime 13:20:00", + "set firewall ipv6 name INBOUND rule 102", + "set firewall ipv6 name INBOUND rule 102 tcp flags ack", + "set firewall ipv6 name INBOUND rule 102 tcp flags not fin", + "set firewall ipv6 name INBOUND rule 102 tcp flags syn", ] self.execute_module(changed=True, commands=commands) diff --git a/tests/unit/modules/network/vyos/test_vyos_interfaces.py b/tests/unit/modules/network/vyos/test_vyos_interfaces.py index affb4f8..14e49c3 100644 --- a/tests/unit/modules/network/vyos/test_vyos_interfaces.py +++ b/tests/unit/modules/network/vyos/test_vyos_interfaces.py @@ -183,5 +183,40 @@ class TestVyosFirewallInterfacesModule(TestVyosModule): "set interfaces ethernet eth4 speed 'auto'", "delete interfaces wireguard wg02 description", "delete interfaces ethernet eth3 description", + "delete interfaces ethernet eth3 disable", ] self.execute_module(changed=True, commands=commands) + + def test_vyos_interfaces_idempotent_disable(self): + set_module_args( + dict( + config=[ + dict( + name="eth3", + description="Ethernet 3", + enabled=False, + ), + ], + state="merged", + ) + ) + + commands = [] + self.execute_module(changed=False, commands=commands) + + def test_vyos_interfaces_idempotent_disable_replace(self): + set_module_args( + dict( + config=[ + dict( + name="eth3", + description="Ethernet 3", + enabled=False, + ), + ], + state="replaced", + ) + ) + + commands = [] + self.execute_module(changed=False, commands=commands) diff --git a/tests/unit/modules/network/vyos/test_vyos_logging_global.py b/tests/unit/modules/network/vyos/test_vyos_logging_global.py index 872769e..a675151 100644 --- a/tests/unit/modules/network/vyos/test_vyos_logging_global.py +++ b/tests/unit/modules/network/vyos/test_vyos_logging_global.py @@ -386,7 +386,6 @@ class TestVyosLoggingGlobalModule(TestVyosModule): playbook["state"] = "overridden" set_module_args(playbook) result = self.execute_module(changed=True) - print(result["commands"]) self.maxDiff = None self.assertEqual(sorted(result["commands"]), sorted(compare_cmds)) diff --git a/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces.py b/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces.py index 1d12a3c..c7d69d0 100644 --- a/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces.py +++ b/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces.py @@ -43,11 +43,25 @@ class TestVyosOspfInterfacesModule(TestVyosModule): "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospf_interfaces.ospf_interfaces.Ospf_interfacesFacts.get_device_data", ) self.execute_show_command = self.mock_execute_show_command.start() + self.mock_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospf_interfaces.ospf_interfaces.get_os_version" + ) + self.test_version = "1.2" + self.get_os_version = self.mock_get_os_version.start() + self.get_os_version.return_value = self.test_version + self.mock_facts_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospf_interfaces.ospf_interfaces.get_os_version" + ) + self.get_facts_os_version = self.mock_facts_get_os_version.start() + self.get_facts_os_version.return_value = self.test_version + self.maxDiff = None def tearDown(self): super(TestVyosOspfInterfacesModule, self).tearDown() self.mock_get_resource_connection_config.stop() self.mock_execute_show_command.stop() + self.mock_get_os_version.stop() + self.mock_facts_get_os_version.stop() def load_fixtures(self, commands=None, filename=None): if filename is None: diff --git a/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces14.py b/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces14.py new file mode 100644 index 0000000..ef27860 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces14.py @@ -0,0 +1,511 @@ +# Spawned from test_vyos_ospf_interfaces (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_ospf_interfaces +from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args + +from .vyos_module import TestVyosModule, load_fixture + + +class TestVyosOspfInterfacesModule14(TestVyosModule): + module = vyos_ospf_interfaces + + def setUp(self): + super(TestVyosOspfInterfacesModule14, self).setUp() + self.mock_get_resource_connection_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base.get_resource_connection" + ) + self.get_resource_connection_config = self.mock_get_resource_connection_config.start() + + self.mock_execute_show_command = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospf_interfaces.ospf_interfaces.Ospf_interfacesFacts.get_device_data" + ) + self.execute_show_command = self.mock_execute_show_command.start() + self.mock_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospf_interfaces.ospf_interfaces.get_os_version" + ) + self.test_version = "1.4" + self.get_os_version = self.mock_get_os_version.start() + self.get_os_version.return_value = self.test_version + self.mock_facts_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospf_interfaces.ospf_interfaces.get_os_version" + ) + self.get_facts_os_version = self.mock_facts_get_os_version.start() + self.get_facts_os_version.return_value = self.test_version + self.maxDiff = None + + def tearDown(self): + super(TestVyosOspfInterfacesModule14, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_execute_show_command.stop() + self.mock_get_os_version.stop() + + def load_fixtures(self, commands=None, filename=None): + if filename is None: + filename = "vyos_ospf_interfaces_config_14.cfg" + + def load_from_file(*args, **kwargs): + output = load_fixture(filename) + return output + + self.execute_show_command.side_effect = load_from_file + + def sort_address_family(self, entry_list): + for entry in entry_list: + if entry.get("address_family"): + entry["address_family"].sort(key=lambda i: i.get("afi")) + + def test_vyos_ospf_interfaces_merged_new_config(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict( + afi="ipv4", + cost=100, + authentication=dict(plaintext_password="abcdefg!"), + priority=55, + ), + dict(afi="ipv6", mtu_ignore=True, instance=20), + ], + ), + dict( + name="bond2", + address_family=[ + dict( + afi="ipv4", + transmit_delay=9, + ), + dict(afi="ipv6", passive=True), + ], + ), + ], + state="merged", + ) + ) + commands = [ + "set protocols ospf interface bond2 transmit-delay 9", + "set protocols ospfv3 interface bond2 passive", + "set protocols ospf interface eth0 cost 100", + "set protocols ospf interface eth0 priority 55", + "set protocols ospf interface eth0 authentication plaintext-password abcdefg!", + "set protocols ospfv3 interface eth0 instance-id 20", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_ospf_interfaces_merged_idempotent(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict(afi="ipv6", mtu_ignore=True, instance=33), + ], + ), + dict( + name="eth1", + address_family=[ + dict( + afi="ipv4", + cost=100, + ), + dict(afi="ipv6", ifmtu=33), + ], + ), + ], + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_ospf_interfaces_existing_config_merged(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict(afi="ipv6", cost=500), + ], + ), + dict( + name="eth1", + address_family=[ + dict( + afi="ipv4", + priority=100, + ), + dict(afi="ipv6", ifmtu=25), + ], + ), + ], + ) + ) + commands = [ + "set protocols ospfv3 interface eth0 cost 500", + "set protocols ospf interface eth1 priority 100", + "set protocols ospfv3 interface eth1 ifmtu 25", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_ospf_interfaces_replaced(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict( + afi="ipv4", + cost=100, + authentication=dict(plaintext_password="abcdefg!"), + priority=55, + ), + ], + ), + dict( + name="bond2", + address_family=[ + dict( + afi="ipv4", + transmit_delay=9, + ), + dict(afi="ipv6", passive=True), + ], + ), + ], + state="replaced", + ) + ) + commands = [ + "set protocols ospf interface bond2 transmit-delay 9", + "set protocols ospfv3 interface bond2 passive", + "set protocols ospf interface eth0 cost 100", + "set protocols ospf interface eth0 priority 55", + "set protocols ospf interface eth0 authentication plaintext-password abcdefg!", + "delete protocols ospfv3 interface eth0 instance-id 33", + "delete protocols ospfv3 interface eth0 mtu-ignore", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_ospf_passive_interfaces_replaced(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict( + afi="ipv4", + passive=True, + ), + ], + ), + dict( + name="eth1", + address_family=[ + dict( + afi="ipv4", + passive=True, + ), + dict( + afi="ipv6", + passive=True, + ), + ], + ), + dict( + name="bond2", + address_family=[ + dict( + afi="ipv4", + passive=True, + ), + dict(afi="ipv6", passive=True), + ], + ), + ], + state="replaced", + ) + ) + commands = [ + "delete protocols ospf interface eth1 cost 100", + "delete protocols ospfv3 interface eth0 instance-id 33", + "delete protocols ospfv3 interface eth0 mtu-ignore", + "delete protocols ospfv3 interface eth1 ifmtu 33", + "set protocols ospf interface bond2 passive", + "set protocols ospfv3 interface bond2 passive", + "set protocols ospf interface eth0 passive", + "set protocols ospf interface eth1 passive", + "set protocols ospfv3 interface eth1 passive", + ] + self.maxDiff = None + self.execute_module(changed=True, commands=commands) + + def test_vyos_ospf_interfaces_replaced_idempotent(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict(afi="ipv6", mtu_ignore=True, instance=33), + ], + ), + dict( + name="eth1", + address_family=[ + dict( + afi="ipv4", + cost=100, + ), + dict(afi="ipv6", ifmtu=33), + ], + ), + ], + state="replaced", + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_ospf_interfaces_overridden(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict( + afi="ipv4", + cost=100, + authentication=dict(plaintext_password="abcdefg!"), + priority=55, + ), + ], + ), + dict( + name="bond2", + address_family=[ + dict( + afi="ipv4", + transmit_delay=9, + ), + dict(afi="ipv6", passive=True), + ], + ), + ], + state="overridden", + ) + ) + commands = [ + "set protocols ospf interface bond2 transmit-delay 9", + "set protocols ospfv3 interface bond2 passive", + "set protocols ospf interface eth0 cost 100", + "set protocols ospf interface eth0 priority 55", + "set protocols ospf interface eth0 authentication plaintext-password abcdefg!", + "delete protocols ospf interface eth1", + "delete protocols ospfv3 interface eth1", + "delete protocols ospfv3 interface eth0 mtu-ignore", + "delete protocols ospfv3 interface eth0 instance-id 33", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_ospf_interfaces_overridden_idempotent(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict(afi="ipv6", mtu_ignore=True, instance=33), + ], + ), + dict( + name="eth1", + address_family=[ + dict( + afi="ipv4", + cost=100, + ), + dict(afi="ipv6", ifmtu=33), + ], + ), + ], + state="overridden", + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_ospf_interfaces_deleted(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + ), + ], + state="deleted", + ) + ) + commands = ["delete protocols ospfv3 interface eth0"] + self.execute_module(changed=True, commands=commands) + + def test_vyos_ospf_interfaces_notpresent_deleted(self): + set_module_args( + dict( + config=[ + dict( + name="eth3", + ), + ], + state="deleted", + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_ospf_interfaces_rendered(self): + set_module_args( + dict( + config=[ + dict( + name="eth0", + address_family=[ + dict( + afi="ipv4", + cost=100, + authentication=dict(plaintext_password="abcdefg!"), + priority=55, + ), + dict(afi="ipv6", mtu_ignore=True, instance=20), + ], + ), + dict( + name="bond2", + address_family=[ + dict( + afi="ipv4", + transmit_delay=9, + ), + dict(afi="ipv6", passive=True), + ], + ), + ], + state="rendered", + ) + ) + commands = [ + "set protocols ospf interface eth0 cost 100", + "set protocols ospf interface eth0 authentication plaintext-password abcdefg!", + "set protocols ospf interface eth0 priority 55", + "set protocols ospfv3 interface eth0 mtu-ignore", + "set protocols ospfv3 interface eth0 instance-id 20", + "set protocols ospf interface bond2 transmit-delay 9", + "set protocols ospfv3 interface bond2 passive", + ] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result["rendered"]), sorted(commands), result["rendered"]) + + def test_vyos_ospf_interfaces_parsed(self): + commands = [ + "set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345'", + "set protocols ospf interface bond2 bandwidth '70'", + "set protocols ospf interface bond2 transmit-delay '45'", + "set protocols ospfv3 interface bond2 'passive'", + "set protocols ospf interface eth0 cost '50'", + "set protocols ospf interface eth0 priority '26'", + "set protocols ospfv3 interface eth0 instance-id '33'", + "set protocols ospfv3 interface eth0 'mtu-ignore'", + "set protocols ospf interface eth1 network 'point-to-point'", + "set protocols ospf interface eth1 priority '26'", + "set protocols ospf interface eth1 transmit-delay '50'", + "set protocols ospfv3 interface eth1 dead-interval '39'", + ] + + parsed_str = "\n".join(commands) + set_module_args(dict(running_config=parsed_str, state="parsed")) + result = self.execute_module(changed=False) + parsed_list = [ + { + "address_family": [ + { + "afi": "ipv4", + "authentication": { + "md5_key": { + "key": "1111111111232345", + "key_id": 10, + } + }, + "bandwidth": 70, + "transmit_delay": 45, + }, + {"afi": "ipv6", "passive": True}, + ], + "name": "bond2", + }, + { + "address_family": [ + {"afi": "ipv4", "cost": 50, "priority": 26}, + {"afi": "ipv6", "instance": "33", "mtu_ignore": True}, + ], + "name": "eth0", + }, + { + "address_family": [ + { + "afi": "ipv4", + "network": "point-to-point", + "priority": 26, + "transmit_delay": 50, + }, + {"afi": "ipv6", "dead_interval": 39}, + ], + "name": "eth1", + }, + ] + result_list = self.sort_address_family(result["parsed"]) + given_list = self.sort_address_family(parsed_list) + self.assertEqual(result_list, given_list) + + def test_vyos_ospf_interfaces_gathered(self): + set_module_args(dict(state="gathered")) + result = self.execute_module(changed=False, filename="vyos_ospf_interfaces_config.cfg") + gathered_list = [ + { + "address_family": [{"afi": "ipv6", "instance": "33", "mtu_ignore": True}], + "name": "eth0", + }, + { + "address_family": [ + {"afi": "ipv4", "cost": 100}, + {"afi": "ipv6", "ifmtu": 33}, + ], + "name": "eth1", + }, + ] + + result_list = self.sort_address_family(result["gathered"]) + given_list = self.sort_address_family(gathered_list) + self.assertEqual(result_list, given_list) |