diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-07-28 05:09:47 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-28 05:09:47 +1000 |
| commit | b0243027a73583eed7014ae924fbc79bb55577b5 (patch) | |
| tree | 822de31d5246309612f50c6a1c8b6f5717dba8eb /tests/unit/modules/network | |
| parent | 2e2a0f2da3df1a3197bef3c19d3e7deecf869759 (diff) | |
| parent | 17041b6f6953b993762854a67953fb35176b46e7 (diff) | |
| download | vyos.vyos-b0243027a73583eed7014ae924fbc79bb55577b5.tar.gz vyos.vyos-b0243027a73583eed7014ae924fbc79bb55577b5.zip | |
Merge branch 'main' into fix/black-formatting
Diffstat (limited to 'tests/unit/modules/network')
6 files changed, 382 insertions, 0 deletions
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces.cfg new file mode 100644 index 00000000..b877a4b6 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces.cfg @@ -0,0 +1,10 @@ +Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down +Interface IP Address S/L Description +--------- ---------- --- ----------- +eth0 10.0.2.15/24 u/u +eth0.100 - u/u vlan-100 +eth1 - u/u +eth1.200 192.0.2.1/24 u/u vlan-200 +eth2 - u/u +lo 127.0.0.1/8 u/u + ::1/128 diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces_empty.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces_empty.cfg new file mode 100644 index 00000000..06ae56a0 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces_empty.cfg @@ -0,0 +1,7 @@ +Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down +Interface IP Address S/L Description +--------- ---------- --- ----------- +eth0 10.0.2.15/24 u/u +eth1 - u/u +eth2 - u/u +lo 127.0.0.1/8 u/u diff --git a/tests/unit/modules/network/vyos/test_rm_templates_perf.py b/tests/unit/modules/network/vyos/test_rm_templates_perf.py new file mode 100644 index 00000000..d2060f46 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_rm_templates_perf.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Performance budget tests for rm_templates parsers. + +These tests guard against re-introducing catastrophic regex backtracking +in the rm_template parsers (T8609). Pre-fix, parse() over realistic +device-output input could take 50+ seconds because of `(group)*` +quantifiers on groups containing `\\S+`. Post-fix, the same input +parses in single-digit milliseconds. + +A 1-second budget is comfortably above post-fix runtime and well below +the pre-regression cliff, so the test fails sharply if the bug returns. +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import time + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family_14 import ( + Bgp_address_familyTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_global_14 import ( + Bgp_globalTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps_14 import ( + Route_mapsTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.snmp_server import ( + Snmp_serverTemplate, +) + + +PARSE_BUDGET_SECONDS = 1.0 + + +def _time_parse(parser_class, lines): + """Time a single ``parse()`` call against ``lines``; returns elapsed seconds.""" + parser = parser_class(lines=lines) + t0 = time.perf_counter() + parser.parse() + return time.perf_counter() - t0 + + +def test_route_maps_14_parse_budget(): + """Realistic route-map config: parse() must finish under 1s. + + Pre-T8609: ~50s. Post-fix: <50ms. Inputs use 20+ char names + because the backtracking is exponential in the first \\S+ run + after the prefix. + """ + lines = [ + "set policy route-map ADVERTISE-ANYCAST-v6 rule 10 action 'permit'", + "set policy route-map ADVERTISE-ANYCAST-v6 rule 10 match ipv6 address prefix-list 'ANYCAST-AGGREGATE-v6'", + "set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 action 'permit'", + "set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 match ipv6 address prefix-list 'AS64496-SENTINEL-v6'", + "set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 set local-preference '120'", + "set policy route-map IXP-PEER-INGRESS-v4 rule 10 action 'permit'", + "set policy route-map IXP-PEER-INGRESS-v4 rule 10 match ip address prefix-list 'IXP-INBOUND-v4'", + "set policy route-map IXP-PEER-INGRESS-v4 rule 10 set community 'additive 65000:100'", + "set policy route-map TRANSIT-EGRESS-v4 rule 100 action 'permit'", + "set policy route-map TRANSIT-EGRESS-v4 rule 100 match ip address prefix-list 'CUSTOMER-PREFIXES-v4'", + "set policy route-map TRANSIT-EGRESS-v4 rule 100 set as-path prepend '65000 65000'", + "set policy route-map UPSTREAM-INGRESS-v4 rule 10 action 'permit'", + ] + elapsed = _time_parse(Route_mapsTemplate14, lines) + assert elapsed < PARSE_BUDGET_SECONDS, ( + "Route_mapsTemplate14.parse() took %.2fs (budget %.2fs); " + "possible regression of T8609 (rm_templates regex backtracking)." + % (elapsed, PARSE_BUDGET_SECONDS) + ) + + +def test_bgp_global_14_parse_budget(): + """Realistic BGP neighbor/address-family config: parse() under 1s.""" + lines = [ + "set protocols bgp 65001 neighbor 2001:db8:abcd:1234::1 remote-as '65002'", + "set protocols bgp 65001 neighbor 2001:db8:abcd:1234::1 description 'IXP-PEER-1'", + "set protocols bgp 65001 neighbor 2001:db8:abcd:1234::1 address-family ipv6-unicast route-map import 'IXP-INGRESS-v6'", + "set protocols bgp 65001 neighbor 2001:db8:abcd:1234::1 address-family ipv6-unicast route-map export 'IXP-EGRESS-v6'", + "set protocols bgp 65001 neighbor 192.0.2.1 remote-as '65003'", + "set protocols bgp 65001 neighbor 192.0.2.1 description 'TRANSIT-PROVIDER-1'", + "set protocols bgp 65001 neighbor 192.0.2.1 address-family ipv4-unicast route-map import 'TRANSIT-INGRESS-v4'", + "set protocols bgp 65001 neighbor 192.0.2.1 address-family ipv4-unicast route-map export 'TRANSIT-EGRESS-v4'", + ] + elapsed = _time_parse(Bgp_globalTemplate14, lines) + assert elapsed < PARSE_BUDGET_SECONDS, ( + "Bgp_globalTemplate14.parse() took %.2fs (budget %.2fs); " + "possible regression of T8609 (rm_templates regex backtracking)." + % (elapsed, PARSE_BUDGET_SECONDS) + ) + + +def test_snmp_server_parse_budget(): + """Realistic SNMP v3 config: parse() under 1s.""" + lines = [ + "set service snmp community PUBLIC-COMMUNITY-NAME-1 authorization 'ro'", + "set service snmp community PUBLIC-COMMUNITY-NAME-1 client '192.0.2.0/24'", + "set service snmp v3 trap-target TRAP-TARGET-LONG-NAME-1 user 'monitor'", + "set service snmp v3 trap-target TRAP-TARGET-LONG-NAME-1 protocol 'udp'", + "set service snmp v3 trap-target TRAP-TARGET-LONG-NAME-1 port '162'", + "set service snmp v3 user TRAP-USER-LONG-NAME-1 mode 'auth'", + "set service snmp v3 user TRAP-USER-LONG-NAME-1 group 'monitor'", + ] + elapsed = _time_parse(Snmp_serverTemplate, lines) + assert elapsed < PARSE_BUDGET_SECONDS, ( + "Snmp_serverTemplate.parse() took %.2fs (budget %.2fs); " + "possible regression of T8609 (rm_templates regex backtracking)." + % (elapsed, PARSE_BUDGET_SECONDS) + ) + + +def test_bgp_address_family_14_parse_budget(): + """Realistic BGP address-family aggregate config: parse() under 1s.""" + lines = [ + "set protocols bgp 65001 address-family ipv4-unicast network 198.51.100.0/24 backdoor", + "set protocols bgp 65001 address-family ipv4-unicast network 198.51.100.0/24 path-limit '4'", + "set protocols bgp 65001 address-family ipv4-unicast network 198.51.100.0/24 route-map 'NET-IN-v4'", + "set protocols bgp 65001 address-family ipv4-unicast aggregate-address 203.0.113.0/24 as-set", + "set protocols bgp 65001 address-family ipv4-unicast aggregate-address 203.0.113.0/24 summary-only", + "set protocols bgp 65001 address-family ipv6-unicast network 2001:db8:abcd:1234::/64 backdoor", + "set protocols bgp 65001 address-family ipv6-unicast network 2001:db8:abcd:1234::/64 route-map 'NET-IN-v6'", + "set protocols bgp 65001 address-family ipv6-unicast aggregate-address 2001:db8::/32 as-set", + "set protocols bgp 65001 address-family ipv6-unicast aggregate-address 2001:db8::/32 summary-only", + ] + elapsed = _time_parse(Bgp_address_familyTemplate14, lines) + assert elapsed < PARSE_BUDGET_SECONDS, ( + "Bgp_address_familyTemplate14.parse() took %.2fs (budget %.2fs); " + "possible regression of T8609 (rm_templates regex backtracking)." + % (elapsed, PARSE_BUDGET_SECONDS) + ) + + +def test_route_maps_14_set_comm_list_delete_matches_setval(): + """Round-trip check: the `set_comm_list_delete` parser must match the line its setval generates. + + `set_comm_list_delete`'s setval emits `set policy route-map X rule N set + comm-list delete` with no token after `delete`. Pre-T8609 the getval + happened to match this by accident (a `*` quantifier on the trailing + `(?P<delete>\\S+)` made the group optional after VERBOSE-strip). An + earlier draft of T8609's fix made the group required, causing the + parser to silently ignore its own output. This test guards the + round-trip. + """ + line = "set policy route-map MY-MAP rule 10 set comm-list delete" + parser = Route_mapsTemplate14(lines=[line]) + result = parser.parse() + rm = result.get("route_maps", {}).get("MY-MAP") + assert rm is not None, ( + "route_maps_14: set_comm_list_delete parser failed to match its " + "own setval-generated line %r; the parser is broken." % line + ) + entry = rm.get("entries", {}).get(10, {}) + comm_list = entry.get("set", {}).get("comm_list", {}) + assert comm_list.get("delete"), ( + "route_maps_14: set_comm_list_delete matched the line but did not " + "populate set.comm_list.delete; check the result template." + ) diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py b/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py index 07482f8e..457fc76b 100644 --- a/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py @@ -1575,3 +1575,17 @@ class TestVyosFirewallRulesModule13(TestVyosModule): ] self.maxDiff = None self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_rules_parsed_icmp_type_code(self): + """parse_icmp_attr: legacy 'type/code' token parses into integer type and code.""" + raw = ( + "set firewall name TEST rule 1 action 'accept'\n" + "set firewall name TEST rule 1 protocol 'icmp'\n" + "set firewall name TEST rule 1 icmp type '3/4'\n" + ) + set_module_args(dict(running_config=raw, state="parsed")) + result = self.execute_module(changed=False) + parsed = result["parsed"] + rule = parsed[0]["rule_sets"][0]["rules"][0] + self.assertEqual(rule["icmp"]["type"], 3) + self.assertEqual(rule["icmp"]["code"], 4) diff --git a/tests/unit/modules/network/vyos/test_vyos_user.py b/tests/unit/modules/network/vyos/test_vyos_user.py index 9d11c904..0e60f07e 100644 --- a/tests/unit/modules/network/vyos/test_vyos_user.py +++ b/tests/unit/modules/network/vyos/test_vyos_user.py @@ -61,6 +61,36 @@ class TestVyosUserModule(TestVyosModule): ["set system login user ansible authentication plaintext-password test"], ) + def test_vyos_user_password_special_chars(self): + set_module_args(dict(name="ansible", configured_password="test$123!@")) + result = self.execute_module(changed=True) + self.assertEqual( + result["commands"], + [ + "set system login user ansible authentication plaintext-password 'test$123!@'", + ], + ) + + def test_vyos_user_password_embedded_quote(self): + set_module_args(dict(name="ansible", configured_password="pa'ss")) + result = self.execute_module(changed=True) + self.assertEqual( + result["commands"], + [ + "set system login user ansible authentication plaintext-password 'pa'\"'\"'ss'", + ], + ) + + def test_vyos_user_password_complex_special_chars(self): + set_module_args(dict(name="ansible", configured_password="P@ss w0rd!$#'xy\\")) + result = self.execute_module(changed=True) + self.assertEqual( + result["commands"], + [ + "set system login user ansible authentication plaintext-password 'P@ss w0rd!$#'\"'\"'xy\\'", + ], + ) + def test_vyos_user_delete(self): set_module_args(dict(name="ansible", state="absent")) result = self.execute_module(changed=True) diff --git a/tests/unit/modules/network/vyos/test_vyos_vlan.py b/tests/unit/modules/network/vyos/test_vyos_vlan.py new file mode 100644 index 00000000..5bbf87c2 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_vlan.py @@ -0,0 +1,158 @@ +# (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_vlan +from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args + +from .vyos_module import TestVyosModule, load_fixture + + +class TestVyosVlanModule(TestVyosModule): + module = vyos_vlan + + def setUp(self): + super(TestVyosVlanModule, self).setUp() + + self.mock_load_config = patch( + "ansible_collections.vyos.vyos.plugins.modules.vyos_vlan.load_config", + ) + self.load_config = self.mock_load_config.start() + + self.mock_run_commands = patch( + "ansible_collections.vyos.vyos.plugins.modules.vyos_vlan.run_commands", + ) + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + super(TestVyosVlanModule, self).tearDown() + self.mock_load_config.stop() + self.mock_run_commands.stop() + + def load_fixtures(self, commands=None, filename=None): + self.load_config.return_value = dict(diff=None, session="session") + if filename == "empty": + self.run_commands.return_value = [ + load_fixture("vyos_vlan_show_interfaces_empty.cfg"), + ] + else: + self.run_commands.return_value = [ + load_fixture("vyos_vlan_show_interfaces.cfg"), + ] + + def test_vyos_vlan_present(self): + """Create a new VLAN with a description on eth2 (not in have).""" + set_module_args( + dict( + vlan_id=300, + name="vlan-300", + interfaces=["eth2"], + state="present", + ) + ) + commands = ["set interfaces ethernet eth2 vif 300 description vlan-300"] + self.execute_module(changed=True, commands=commands) + + def test_vyos_vlan_present_no_change(self): + """Existing VLAN 100 on eth0 — no commands should be generated.""" + set_module_args( + dict( + vlan_id=100, + name="vlan-100", + interfaces=["eth0"], + state="present", + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_vlan_absent(self): + """Delete an existing VLAN (200 on eth1).""" + set_module_args( + dict( + vlan_id=200, + interfaces=["eth1"], + state="absent", + ) + ) + commands = ["delete interfaces ethernet eth1 vif 200"] + self.execute_module(changed=True, commands=commands) + + def test_vyos_vlan_absent_no_change(self): + """Delete a VLAN that does not exist — no commands.""" + set_module_args( + dict( + vlan_id=999, + interfaces=["eth0"], + state="absent", + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_vyos_vlan_aggregate(self): + """Create two new VLANs via aggregate; neither is in have.""" + set_module_args( + dict( + aggregate=[ + dict(vlan_id=301, interfaces=["eth2"], name="vlan-301"), + dict(vlan_id=302, interfaces=["eth2"], name="vlan-302"), + ], + ) + ) + commands = [ + "set interfaces ethernet eth2 vif 301 description vlan-301", + "set interfaces ethernet eth2 vif 302 description vlan-302", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_vlan_purge(self): + """Purge VLANs not in want. Want only VLAN 100; VLAN 200 should be removed. + + The fixed parser only maps ethX.Y sub-interfaces, so bare ethX interfaces + (vlan_id=None) are not in have. Only real VLANs (eth1.200) are purged. + """ + set_module_args( + dict( + vlan_id=100, + interfaces=["eth0"], + state="present", + purge=True, + ) + ) + commands = [ + "delete interfaces ethernet eth1 vif 200", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_vlan_with_address(self): + """Create a VLAN with an IP address and no description.""" + set_module_args( + dict( + vlan_id=400, + address="10.10.40.1/24", + interfaces=["eth1"], + state="present", + ) + ) + commands = ["set interfaces ethernet eth1 vif 400 address 10.10.40.1/24"] + self.execute_module(changed=True, commands=commands, filename="empty") |
