summaryrefslogtreecommitdiff
path: root/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit')
-rw-r--r--tests/unit/mock/loader.py1
-rw-r--r--tests/unit/mock/path.py2
-rw-r--r--tests/unit/mock/procenv.py20
-rw-r--r--tests/unit/mock/vault_helper.py1
-rw-r--r--tests/unit/mock/yaml_helper.py46
-rw-r--r--tests/unit/modules/conftest.py4
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config.cfg2
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg12
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg11
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config.cfg6
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config_v14.cfg7
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_ha_config.cfg64
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config_vrf.cfg1
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_l3_interfaces_config.cfg8
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_lldp_interfaces_config.cfg5
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg61
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_route_maps_config_v14.cfg18
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces.cfg10
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_vlan_show_interfaces_empty.cfg7
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_config.cfg30
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_s2s_config.cfg26
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_vrf_config.cfg15
-rw-r--r--tests/unit/modules/network/vyos/test_rm_templates_perf.py163
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_banner.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_bgp_address_family.py11
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_bgp_address_family14.py63
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_bgp_global.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_bgp_global14.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_command.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_config.py39
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_facts.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_file.py524
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_firewall_global.py20
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_firewall_global14.py555
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_firewall_global15.py132
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_firewall_interfaces.py19
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py229
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_firewall_rules14.py256
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_ha.py1405
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_hostname.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_interfaces.py186
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_l3_interfaces.py278
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_lag_interfaces.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_lldp_global.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_lldp_interfaces.py258
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_logging_global.py19
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_logging_global15.py353
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_nat.py839
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_ntp_global.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_ospf_interfaces.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_ospf_interfaces14.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_ospfv2.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_ospfv2v14.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_ospfv3.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_ping.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_prefix_lists.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_route_maps.py136
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_route_maps14.py716
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_snmp_server.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_static_routes.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_static_routes14.py1
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_system.py80
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_user.py103
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_vlan.py158
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_vpn_ipsec.py568
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_vpn_ipsec_s2s.py245
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_vrf.py688
-rw-r--r--tests/unit/modules/network/vyos/vyos_module.py2
-rw-r--r--tests/unit/modules/utils.py1
69 files changed, 8124 insertions, 298 deletions
diff --git a/tests/unit/mock/loader.py b/tests/unit/mock/loader.py
index 67b84c62..2828f38e 100644
--- a/tests/unit/mock/loader.py
+++ b/tests/unit/mock/loader.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
import os
diff --git a/tests/unit/mock/path.py b/tests/unit/mock/path.py
index 1f1b7f0a..3962f24f 100644
--- a/tests/unit/mock/path.py
+++ b/tests/unit/mock/path.py
@@ -1,10 +1,8 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from ansible.utils.path import unfrackpath
from ansible_collections.vyos.vyos.tests.unit.compat.mock import MagicMock
-
mock_unfrackpath_noop = MagicMock(spec_set=unfrackpath, side_effect=lambda x, *args, **kwargs: x)
diff --git a/tests/unit/mock/procenv.py b/tests/unit/mock/procenv.py
index d7f3dc95..2ae0c173 100644
--- a/tests/unit/mock/procenv.py
+++ b/tests/unit/mock/procenv.py
@@ -19,7 +19,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
import json
@@ -29,7 +28,6 @@ from contextlib import contextmanager
from io import BytesIO, StringIO
from ansible.module_utils._text import to_bytes
-from ansible.module_utils.six import PY3
from ansible_collections.vyos.vyos.tests.unit.compat import unittest
@@ -42,11 +40,11 @@ def swap_stdin_and_argv(stdin_data="", argv_data=tuple()):
real_stdin = sys.stdin
real_argv = sys.argv
- if PY3:
- fake_stream = StringIO(stdin_data)
- fake_stream.buffer = BytesIO(to_bytes(stdin_data))
- else:
- fake_stream = BytesIO(to_bytes(stdin_data))
+ # if PY3:
+ fake_stream = StringIO(stdin_data)
+ fake_stream.buffer = BytesIO(to_bytes(stdin_data))
+ # else:
+ # fake_stream = BytesIO(to_bytes(stdin_data))
try:
sys.stdin = fake_stream
@@ -65,10 +63,10 @@ def swap_stdout():
"""
old_stdout = sys.stdout
- if PY3:
- fake_stream = StringIO()
- else:
- fake_stream = BytesIO()
+ # if PY3:
+ fake_stream = StringIO()
+ # else:
+ # fake_stream = BytesIO()
try:
sys.stdout = fake_stream
diff --git a/tests/unit/mock/vault_helper.py b/tests/unit/mock/vault_helper.py
index 6006ba96..4c6c890c 100644
--- a/tests/unit/mock/vault_helper.py
+++ b/tests/unit/mock/vault_helper.py
@@ -14,7 +14,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from ansible.module_utils._text import to_bytes
diff --git a/tests/unit/mock/yaml_helper.py b/tests/unit/mock/yaml_helper.py
index 2e857592..65d4f7bc 100644
--- a/tests/unit/mock/yaml_helper.py
+++ b/tests/unit/mock/yaml_helper.py
@@ -1,12 +1,10 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
import io
import yaml
-from ansible.module_utils.six import PY3
from ansible.parsing.yaml.dumper import AnsibleDumper
from ansible.parsing.yaml.loader import AnsibleLoader
@@ -22,17 +20,11 @@ class YamlTestUtils(object):
def _dump_stream(self, obj, stream, dumper=None):
"""Dump to a py2-unicode or py3-string stream."""
- if PY3:
- return yaml.dump(obj, stream, Dumper=dumper)
- else:
- return yaml.dump(obj, stream, Dumper=dumper, encoding=None)
+ return yaml.dump(obj, stream, Dumper=dumper)
def _dump_string(self, obj, dumper=None):
"""Dump to a py2-unicode or py3-string"""
- if PY3:
- return yaml.dump(obj, Dumper=dumper)
- else:
- return yaml.dump(obj, Dumper=dumper, encoding=None)
+ return yaml.dump(obj, Dumper=dumper)
def _dump_load_cycle(self, obj):
# Each pass though a dump or load revs the 'generation'
@@ -89,22 +81,8 @@ class YamlTestUtils(object):
stream_obj_from_stream = io.StringIO()
stream_obj_from_string = io.StringIO()
- if PY3:
- yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper)
- yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper)
- else:
- yaml.dump(
- obj_from_stream,
- stream_obj_from_stream,
- Dumper=AnsibleDumper,
- encoding=None,
- )
- yaml.dump(
- obj_from_stream,
- stream_obj_from_string,
- Dumper=AnsibleDumper,
- encoding=None,
- )
+ yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper)
+ yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper)
yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue()
yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue()
@@ -112,20 +90,8 @@ class YamlTestUtils(object):
stream_obj_from_stream.seek(0)
stream_obj_from_string.seek(0)
- if PY3:
- yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper)
- yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper)
- else:
- yaml_string_obj_from_stream = yaml.dump(
- obj_from_stream,
- Dumper=AnsibleDumper,
- encoding=None,
- )
- yaml_string_obj_from_string = yaml.dump(
- obj_from_string,
- Dumper=AnsibleDumper,
- encoding=None,
- )
+ yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper)
+ yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper)
assert yaml_string == yaml_string_obj_from_stream
assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string
diff --git a/tests/unit/modules/conftest.py b/tests/unit/modules/conftest.py
index 41465c30..b084d2f0 100644
--- a/tests/unit/modules/conftest.py
+++ b/tests/unit/modules/conftest.py
@@ -2,7 +2,6 @@
# 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
import json
@@ -11,12 +10,11 @@ import pytest
from ansible.module_utils._text import to_bytes
from ansible.module_utils.common._collections_compat import MutableMapping
-from ansible.module_utils.six import string_types
@pytest.fixture
def patch_ansible_module(request, mocker):
- if isinstance(request.param, string_types):
+ if isinstance(request.param, str):
args = request.param
elif isinstance(request.param, MutableMapping):
if "ANSIBLE_MODULE_ARGS" not in request.param:
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config.cfg
index f54a03dc..464f132f 100644
--- a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config.cfg
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config.cfg
@@ -2,6 +2,8 @@ set firewall group address-group RND-HOSTS address 192.0.2.1
set firewall group address-group RND-HOSTS address 192.0.2.3
set firewall group address-group RND-HOSTS address 192.0.2.5
set firewall group address-group RND-HOSTS description 'This group has the Management hosts address lists'
+set firewall group address-group DELETE-HOSTS address 1.2.3.4
+set firewall group address-group DELETE-HOSTS description 'The (single) last address from this group will be deleted in the tests'
set firewall group ipv6-address-group LOCAL-v6 address ::1
set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1
set firewall group ipv6-address-group LOCAL-v6 description 'This group has the hosts address lists of this machine'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg
index 0a1247dd..f58f166c 100644
--- a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg
@@ -1,7 +1,10 @@
+set firewall group address-group A-EMPTY
set firewall group address-group RND-HOSTS address 192.0.2.1
set firewall group address-group RND-HOSTS address 192.0.2.3
set firewall group address-group RND-HOSTS address 192.0.2.5
set firewall group address-group RND-HOSTS description 'This group has the Management hosts address lists'
+set firewall group address-group DELETE-HOSTS address 1.2.3.4
+set firewall group address-group DELETE-HOSTS description 'The (single) last address from this group will be deleted in the tests'
set firewall group ipv6-address-group LOCAL-v6 address ::1
set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1
set firewall group ipv6-address-group LOCAL-v6 description 'This group has the hosts address lists of this machine'
@@ -16,3 +19,12 @@ set firewall global-options state-policy related action 'accept'
set firewall global-options state-policy related log-level 'alert'
set firewall global-options ipv6-src-route 'enable'
set firewall global-options send-redirects 'enable'
+set firewall zone ZONE-TEST interface 'eth0.1234'
+set firewall zone ZONE-TEST description 'zone-test test description'
+set firewall group address-group ZONE-TEST address '1.2.3.4'
+set firewall zone ZONE-IZF description 'zone for izf and sources delete path testing'
+set firewall zone ZONE-IZF intra-zone-filtering action 'drop'
+set firewall zone ZONE-IZF from zone-src firewall name 'existing-ruleset'
+set firewall name TESTRULESET-V4 default-action drop
+set firewall ipv6-name TESTRULESET-V6-LEGACY default-action drop
+set firewall ipv6 name TESTRULESET-V6-1_4PLUS default-action drop
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg
new file mode 100644
index 00000000..23dd9777
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg
@@ -0,0 +1,11 @@
+# Intentionally minimal: represents a VyOS 1.5.0 device with one existing
+# zone already configured using the 1.5.0 'member interface' syntax. Used
+# to test parse_zone's unwrap logic in isolation. TestVyosFirewallRulesModule15's
+# other tests (e.g. test_01_merged) create a differently-named zone
+# (ZONE-15), so under 'merged' state this existing zone is never touched
+# and doesn't interfere.
+set firewall zone ZONE-15-EXISTING member interface eth2
+set firewall zone ZONE-15-EXISTING description 'existing 1.5.0 zone for facts parsing test'
+set firewall name TESTRULESET-V4 default-action drop
+set firewall ipv6-name TESTRULESET-V6-LEGACY default-action drop
+set firewall ipv6 name TESTRULESET-V6-1_4PLUS default-action drop
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_rules_config.cfg
index 3ad6ec97..9665f86b 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
@@ -1,3 +1,5 @@
+set firewall flowtable test interface 'eth1'
+set firewall flowtable test offload 'software'
set firewall name V4-INGRESS default-action 'accept'
set firewall ipv6-name V6-INGRESS default-action 'accept'
set firewall name V4-INGRESS description 'This is IPv4 V4-INGRESS rule set'
@@ -18,4 +20,6 @@ set firewall name MULTIPLE-RULE default-action 'drop'
set firewall name MULTIPLE-RULE rule 1 action 'accept'
set firewall name MULTIPLE-RULE rule 1 protocol 'all'
set firewall name MULTIPLE-RULE rule 2 action 'drop'
-set firewall name MULTIPLE-RULE rule 2 protocol 'all' \ No newline at end of file
+set firewall name MULTIPLE-RULE rule 2 protocol 'all'
+set firewall name V4-OFFLOAD rule 1 action 'offload'
+set firewall name V4-OFFLOAD rule 1 offload-target 'test'
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
index 7f63dd78..6f69211a 100644
--- 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
@@ -1,3 +1,5 @@
+set firewall flowtable test interface 'eth1'
+set firewall flowtable test offload 'software'
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'
@@ -36,4 +38,7 @@ set firewall ipv4 name MULTIPLE-RULE default-action 'drop'
set firewall ipv4 name MULTIPLE-RULE rule 1 action 'accept'
set firewall ipv4 name MULTIPLE-RULE rule 1 protocol 'all'
set firewall ipv4 name MULTIPLE-RULE rule 2 action 'drop'
-set firewall ipv4 name MULTIPLE-RULE rule 2 protocol 'all' \ No newline at end of file
+set firewall ipv4 name MULTIPLE-RULE rule 2 protocol 'all'
+set firewall ipv4 name V4-OFFLOAD rule 105
+set firewall ipv4 name V4-OFFLOAD rule 105 action 'offload'
+set firewall ipv4 name V4-OFFLOAD rule 105 offload-target 'test'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_ha_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_ha_config.cfg
new file mode 100644
index 00000000..ee69c65d
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_ha_config.cfg
@@ -0,0 +1,64 @@
+set high-availability disable
+set high-availability virtual-server s1 address 10.10.10.1
+set high-availability virtual-server s1 algorithm round-robin
+set high-availability virtual-server s1 delay-loop 60
+set high-availability virtual-server s1 forward-method direct
+set high-availability virtual-server s1 fwmark 10
+set high-availability virtual-server s1 persistence-timeout 30
+set high-availability virtual-server s1 protocol tcp
+set high-availability virtual-server s1 real-server 10.10.50.1 health-check script '/var/tmp/script.sh'
+set high-availability virtual-server s1 real-server 10.10.50.1 port 443
+set high-availability virtual-server s2 address 10.10.10.2
+set high-availability virtual-server s2 port 81
+set high-availability virtual-server s2 real-server real1 connection-timeout 5
+set high-availability virtual-server s2 real-server real1 port 8081
+set high-availability virtual-server s2 real-server real2 port 8080
+set high-availability vrrp global-parameters garp interval 30
+set high-availability vrrp global-parameters garp master-delay 10
+set high-availability vrrp global-parameters garp master-refresh 100
+set high-availability vrrp global-parameters garp master-refresh-repeat 200
+set high-availability vrrp global-parameters garp master-repeat 5
+set high-availability vrrp global-parameters startup-delay 30
+set high-availability vrrp global-parameters version 3
+set high-availability vrrp group g1 address '1.1.1.1'
+set high-availability vrrp group g1 address '3.3.3.3'
+set high-availability vrrp group g1 address '5.5.5.5' interface 'eth2'
+set high-availability vrrp group g1 advertise-interval 10
+set high-availability vrrp group g1 authentication password 'testpass'
+set high-availability vrrp group g1 authentication type 'plaintext-password'
+set high-availability vrrp group g1 description 'Group_1'
+set high-availability vrrp group g1 disable
+set high-availability vrrp group g1 excluded-address '192.168.1.7' interface 'eth3'
+set high-availability vrrp group g1 excluded-address '192.168.1.8'
+set high-availability vrrp group g1 garp interval 20
+set high-availability vrrp group g1 garp master-delay 5
+set high-availability vrrp group g1 garp master-refresh 50
+set high-availability vrrp group g1 garp master-refresh-repeat 100
+set high-availability vrrp group g1 garp master-repeat 3
+set high-availability vrrp group g1 health-check failure-count 3
+set high-availability vrrp group g1 health-check interval 10
+set high-availability vrrp group g1 health-check ping '192.168.1.5'
+set high-availability vrrp group g1 health-check script 'script.sh'
+set high-availability vrrp group g1 hello-source-address '192.168.1.2'
+set high-availability vrrp group g1 interface 'eth2'
+set high-availability vrrp group g1 no-preempt
+set high-availability vrrp group g1 peer-address '192.168.1.3'
+set high-availability vrrp group g1 priority 100
+set high-availability vrrp group g1 rfc3768-compatibility
+set high-availability vrrp group g1 track exclude-vrrp-interface
+set high-availability vrrp group g1 track interface 'eth1'
+set high-availability vrrp group g1 transition-script backup '/var/tmp/script.sh'
+set high-availability vrrp group g1 transition-script fault '/var/tmp/script.sh'
+set high-availability vrrp group g1 transition-script master '/var/tmp/script.sh'
+set high-availability vrrp group g1 transition-script stop '/var/tmp/script.sh'
+set high-availability vrrp group g1 vrid 20
+set high-availability vrrp snmp
+set high-availability vrrp sync-group sg1 health-check failure-count 3
+set high-availability vrrp sync-group sg1 health-check interval 10
+set high-availability vrrp sync-group sg1 health-check ping '192.168.1.1'
+set high-availability vrrp sync-group sg1 health-check script '/var/tmp/script.sh'
+set high-availability vrrp sync-group sg1 member 'g1'
+set high-availability vrrp sync-group sg1 transition-script backup '/var/tmp/script.sh'
+set high-availability vrrp sync-group sg1 transition-script fault '/var/tmp/script.sh'
+set high-availability vrrp sync-group sg1 transition-script master '/var/tmp/script.sh'
+set high-availability vrrp sync-group sg1 transition-script stop '/var/tmp/script.sh'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config_vrf.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config_vrf.cfg
new file mode 100644
index 00000000..39afd31b
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config_vrf.cfg
@@ -0,0 +1 @@
+set interfaces ethernet eth2 vrf 'green'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_l3_interfaces_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_l3_interfaces_config.cfg
new file mode 100644
index 00000000..8f9609cd
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_l3_interfaces_config.cfg
@@ -0,0 +1,8 @@
+set interfaces ethernet eth0 address 'dhcp'
+set interfaces ethernet eth1 address '192.0.2.14/24'
+set interfaces ethernet eth2 address '192.0.2.10/24'
+set interfaces ethernet eth2 address '2001:db8::10/32'
+set interfaces ethernet eth3 address '198.51.100.10/24'
+set interfaces ethernet eth3 vif 101 address '198.51.100.130/25'
+set interfaces ethernet eth3 vif 102 address '2001:db8:4000::3/34'
+set interfaces loopback 'lo'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_lldp_interfaces_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_lldp_interfaces_config.cfg
new file mode 100644
index 00000000..767daca8
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_lldp_interfaces_config.cfg
@@ -0,0 +1,5 @@
+set service lldp interface eth1 location elin '0000000911'
+set service lldp interface eth2 location coordinate-based altitude '2200'
+set service lldp interface eth2 location coordinate-based datum 'WGS84'
+set service lldp interface eth2 location coordinate-based latitude '33.524449N'
+set service lldp interface eth2 location coordinate-based longitude '222.267255W'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg
new file mode 100644
index 00000000..505f5e57
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg
@@ -0,0 +1,61 @@
+set nat cgnat log-allocation
+set nat cgnat pool external ext-pool-1 external-port-range '10000-20000'
+set nat cgnat pool external ext-pool-1 per-user-limit port '200'
+set nat cgnat pool external ext-pool-1 range 203.0.113.0/24
+set nat cgnat pool external ext-pool-1 range 203.1.113.1-203.1.113.60 seq '10'
+set nat cgnat pool internal int-pool-1 range '10.0.0.0/24'
+set nat cgnat pool internal int-pool-1 range '10.1.0.0/24'
+set nat cgnat rule 1 source pool 'int-pool-1'
+set nat cgnat rule 1 translation pool 'ext-pool-1'
+set nat destination rule 100 description 'Web server NAT'
+set nat destination rule 100 destination address '198.51.100.10'
+set nat destination rule 100 destination port '80'
+set nat destination rule 100 inbound-interface name 'eth2'
+set nat destination rule 100 log
+set nat destination rule 100 protocol 'tcp'
+set nat destination rule 100 translation address '192.168.1.10'
+set nat destination rule 100 translation options address-mapping 'persistent'
+set nat destination rule 100 translation options port-mapping 'random'
+set nat destination rule 100 translation port '8080'
+set nat destination rule 105 protocol tcp
+set nat destination rule 105 destination port '80'
+set nat destination rule 105 inbound-interface name 'eth0'
+set nat destination rule 105 load-balance hash 'source-address'
+set nat destination rule 105 load-balance hash 'destination-address'
+set nat destination rule 105 load-balance backend 10.10.10.1 weight '60'
+set nat destination rule 105 load-balance backend 10.10.10.2 weight '40'
+set nat source rule 200 description 'Outbound NAT'
+set nat source rule 200 destination address '192.168.10.100'
+set nat source rule 200 destination port '8083'
+set nat source rule 200 disable
+set nat source rule 200 exclude
+set nat source rule 200 log
+set nat source rule 200 protocol 'tcp'
+set nat source rule 200 translation address 'masquerade'
+set nat source rule 200 translation port '443'
+set nat static rule 300 description 'Static mapping'
+set nat static rule 300 destination address '192.168.100.20'
+set nat static rule 300 inbound-interface 'eth2'
+set nat static rule 300 log
+set nat static rule 300 translation address '192.168.1.20'
+set nat64 source rule 10 description 'NAT64 example'
+set nat64 source rule 10 disable
+set nat64 source rule 10 match mark '100'
+set nat64 source rule 10 source prefix '2001:db8::/96'
+set nat64 source rule 10 translation pool 1 address '192.168.100.10'
+set nat64 source rule 10 translation pool 1 description 'NAT64 translation pool'
+set nat64 source rule 10 translation pool 1 disable
+set nat64 source rule 10 translation pool 1 port '1-65535'
+set nat64 source rule 10 translation pool 1 protocol udp
+set nat66 destination rule 20 description 'NAT66 DNAT'
+set nat66 destination rule 20 destination address '2001:db8::1'
+set nat66 destination rule 20 inbound-interface name 'eth1'
+set nat66 destination rule 20 protocol 'tcp'
+set nat66 destination rule 20 translation address '2001:db8:1::10'
+set nat66 destination rule 20 translation port '8443'
+set nat66 source rule 30 description 'NAT66 SNAT'
+set nat66 source rule 30 destination prefix '2001:db8::/96'
+set nat66 source rule 30 outbound-interface name 'eth2'
+set nat66 source rule 30 protocol 'tcp'
+set nat66 source rule 30 source prefix '2001:db8:2::/64'
+set nat66 source rule 30 translation address 'masquerade'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_route_maps_config_v14.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_route_maps_config_v14.cfg
new file mode 100644
index 00000000..1e84e5eb
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_route_maps_config_v14.cfg
@@ -0,0 +1,18 @@
+set policy route-map test3 rule 1 action 'permit'
+set policy route-map test3 rule 1 match interface 'eth2'
+set policy route-map test3 rule 1 match ipv6 nexthop 'fdda:5cc1:23:4::1f'
+set policy route-map test3 rule 1 match metric '1'
+set policy route-map test3 rule 1 match peer '1.1.1.2'
+set policy route-map test3 rule 1 match rpki 'invalid'
+set policy route-map test3 rule 1 set bgp-extcommunity-rt '22:11'
+set policy route-map test3 rule 1 set community replace 'internet'
+set policy route-map test3 rule 1 set ipv6-next-hop global 'fdda:5cc1:23:4::1f'
+set policy route-map test3 rule 1 set ip-next-hop '10.20.10.20'
+set policy route-map test3 rule 1 set local-preference '4'
+set policy route-map test3 rule 1 set metric '5'
+set policy route-map test3 rule 1 set metric-type 'type-1'
+set policy route-map test3 rule 1 set origin 'egp'
+set policy route-map test3 rule 1 set originator-id '10.0.2.3'
+set policy route-map test3 rule 1 set src '10.0.2.15'
+set policy route-map test3 rule 1 set tag '5'
+set policy route-map test3 rule 1 set weight '4'
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/fixtures/vyos_vpn_ipsec_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_config.cfg
new file mode 100644
index 00000000..d0d0e19e
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_config.cfg
@@ -0,0 +1,30 @@
+set vpn ipsec authentication ppk PPK-TEST id 'ppk-id-1'
+set vpn ipsec authentication ppk PPK-TEST secret 'test-ppk-secret'
+set vpn ipsec authentication psk PSK-TEST id 'local@example.com'
+set vpn ipsec authentication psk PSK-TEST id 'remote@example.com'
+set vpn ipsec authentication psk PSK-TEST secret 'test-not-real-secret'
+set vpn ipsec disable-uniqreqids
+set vpn ipsec esp-group ESP-TEST compression
+set vpn ipsec esp-group ESP-TEST proposal 1 encryption 'aes256'
+set vpn ipsec esp-group ESP-TEST proposal 1 hash 'sha256'
+set vpn ipsec ike-group IKE-TEST key-exchange 'ikev2'
+set vpn ipsec ike-group IKE-TEST dead-peer-detection action 'restart'
+set vpn ipsec ike-group IKE-TEST dead-peer-detection interval '15'
+set vpn ipsec ike-group IKE-TEST dead-peer-detection timeout '60'
+set vpn ipsec ike-group IKE-TEST disable-mobike
+set vpn ipsec ike-group IKE-TEST proposal 1 dh-group '14'
+set vpn ipsec ike-group IKE-TEST proposal 1 encryption 'aes256'
+set vpn ipsec ike-group IKE-TEST proposal 1 hash 'sha256'
+set vpn ipsec ike-group IKE-EXTRA key-exchange 'ikev1'
+set vpn ipsec interface 'eth0'
+set vpn ipsec interface 'eth1'
+set vpn ipsec log level '1'
+set vpn ipsec log subsystem 'ike'
+set vpn ipsec log subsystem 'chd'
+set vpn ipsec options flexvpn
+set vpn ipsec options retransmission attempts '3'
+set vpn ipsec profile testprofile authentication mode 'pre-shared-secret'
+set vpn ipsec profile testprofile authentication pre-shared-secret 'test-not-real-secret'
+set vpn ipsec profile testprofile bind tunnel 'tun0'
+set vpn ipsec profile testprofile esp-group 'ESP-TEST'
+set vpn ipsec profile testprofile ike-group 'IKE-TEST'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_s2s_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_s2s_config.cfg
new file mode 100644
index 00000000..0d674b8f
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_s2s_config.cfg
@@ -0,0 +1,26 @@
+set vpn ipsec site-to-site peer PEER-TEST authentication local-id 'local@example.com'
+set vpn ipsec site-to-site peer PEER-TEST authentication mode 'pre-shared-secret'
+set vpn ipsec site-to-site peer PEER-TEST authentication remote-id 'remote@example.com'
+set vpn ipsec site-to-site peer PEER-TEST childless 'prefer'
+set vpn ipsec site-to-site peer PEER-TEST connection-type 'initiate'
+set vpn ipsec site-to-site peer PEER-TEST default-esp-group 'ESP-TEST'
+set vpn ipsec site-to-site peer PEER-TEST description 'test peer for site-to-site module'
+set vpn ipsec site-to-site peer PEER-TEST force-udp-encapsulation
+set vpn ipsec site-to-site peer PEER-TEST ike-group 'IKE-TEST'
+set vpn ipsec site-to-site peer PEER-TEST ikev2-reauth 'inherit'
+set vpn ipsec site-to-site peer PEER-TEST local-address 'any'
+set vpn ipsec site-to-site peer PEER-TEST remote-address '203.0.113.1'
+set vpn ipsec site-to-site peer PEER-TEST replay-window '32'
+set vpn ipsec site-to-site peer PEER-TEST tunnel 1 esp-group 'ESP-TEST'
+set vpn ipsec site-to-site peer PEER-TEST tunnel 1 local port '443'
+set vpn ipsec site-to-site peer PEER-TEST tunnel 1 local prefix '10.0.0.0/24'
+set vpn ipsec site-to-site peer PEER-TEST tunnel 1 priority '10'
+set vpn ipsec site-to-site peer PEER-TEST tunnel 1 protocol 'tcp'
+set vpn ipsec site-to-site peer PEER-TEST tunnel 1 remote port '443'
+set vpn ipsec site-to-site peer PEER-TEST tunnel 1 remote prefix '10.1.0.0/24'
+set vpn ipsec site-to-site peer PEER-TEST virtual-address '0.0.0.0'
+set vpn ipsec site-to-site peer PEER-TEST vti bind 'vti0'
+set vpn ipsec site-to-site peer PEER-TEST vti esp-group 'ESP-TEST'
+set vpn ipsec site-to-site peer PEER-TEST vti traffic-selector local prefix '10.2.0.0/24'
+set vpn ipsec site-to-site peer PEER-TEST vti traffic-selector remote prefix '10.3.0.0/24'
+set vpn ipsec site-to-site peer PEER-EXTRA ike-group 'IKE-TEST'
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_vrf_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vrf_config.cfg
new file mode 100644
index 00000000..094b7cf3
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_vrf_config.cfg
@@ -0,0 +1,15 @@
+set vrf bind-to-all
+set vrf name vrf-blue description 'blue-vrf'
+set vrf name vrf-blue table '100'
+set vrf name vrf-blue vni '1000'
+set vrf name vrf-red description 'red-vrf'
+set vrf name vrf-red disable
+set vrf name vrf-red ip disable-forwarding
+set vrf name vrf-red ip protocol rip route-map 'rm1'
+set vrf name vrf-red ipv6 nht no-resolve-via-default
+set vrf name vrf-red table '101'
+set vrf name vrf-red vni '1001'
+set vrf name vrf-red protocols bgp system-as 65000
+set vrf name vrf-red protocols bgp neighbor 192.0.2.1 remote-as 65002
+set vrf name vrf-red protocols bgp neighbor 1.1.1.3 passive
+set vrf name vrf-red protocols bgp neighbor 1.1.1.3 remote-as 400
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_banner.py b/tests/unit/modules/network/vyos/test_vyos_banner.py
index d418a27c..c15ed8af 100644
--- a/tests/unit/modules/network/vyos/test_vyos_banner.py
+++ b/tests/unit/modules/network/vyos/test_vyos_banner.py
@@ -16,7 +16,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_bgp_address_family.py b/tests/unit/modules/network/vyos/test_vyos_bgp_address_family.py
index 7ea9b2cd..9d6f6297 100644
--- a/tests/unit/modules/network/vyos/test_vyos_bgp_address_family.py
+++ b/tests/unit/modules/network/vyos/test_vyos_bgp_address_family.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -701,13 +700,13 @@ class TestVyosBgpafOpsModule13(TestVyosModule):
],
redistribute=[
dict(protocol="ospf", metric=25),
- ]
+ ],
),
dict(
afi="ipv6",
redistribute=[
dict(protocol="ospfv3", metric=20),
- dict(protocol="ripng")
+ dict(protocol="ripng"),
],
),
],
@@ -738,13 +737,13 @@ class TestVyosBgpafOpsModule13(TestVyosModule):
],
redistribute=[
dict(protocol="ospf", metric=25),
- ]
+ ],
),
dict(
afi="ipv6",
redistribute=[
dict(protocol="ospfv3", metric=20),
- dict(protocol="ripng")
+ dict(protocol="ripng"),
],
),
],
@@ -772,7 +771,7 @@ class TestVyosBgpafOpsModule13(TestVyosModule):
afi="ipv4",
networks=[
dict(prefix="192.2.13.0/24"),
- ]
+ ],
),
dict(
afi="ipv6",
diff --git a/tests/unit/modules/network/vyos/test_vyos_bgp_address_family14.py b/tests/unit/modules/network/vyos/test_vyos_bgp_address_family14.py
index 9e615ff0..447e324f 100644
--- a/tests/unit/modules/network/vyos/test_vyos_bgp_address_family14.py
+++ b/tests/unit/modules/network/vyos/test_vyos_bgp_address_family14.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -45,13 +44,13 @@ class TestVyosBgpafModule14(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.bgp_address_family.bgp_address_family.get_os_version"
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_address_family.bgp_address_family.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.bgp_address_family.bgp_address_family.get_os_version"
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.bgp_address_family.bgp_address_family.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
@@ -425,46 +424,6 @@ class TestVyosBgpafModule14(TestVyosModule):
self.execute_module(changed=True, commands=commands)
- # def test_vyos_bgp_address_family_incorrect_instance(self):
- # set_module_args(
- # dict(
- # state="overridden",
- # config=dict(
- # as_number=100,
- # address_family=[
- # dict(
- # afi="ipv4",
- # networks=[
- # dict(prefix="192.1.13.0/24", route_map="map01"),
- # ],
- # ),
- # dict(
- # afi="ipv6",
- # redistribute=[dict(protocol="ospfv3", metric=20)],
- # ),
- # ],
- # neighbors=[
- # dict(
- # neighbor_address="192.10.21.25",
- # address_family=[
- # dict(
- # afi="ipv4",
- # route_map=[dict(action="import", route_map="map01")],
- # ),
- # dict(
- # afi="ipv6",
- # distribute_list=[dict(action="export", acl=10)],
- # route_server_client=True,
- # ),
- # ],
- # ),
- # ],
- # ),
- # ),
- # )
- # result = self.execute_module(failed=True)
- # self.assertIn("Only one bgp instance is allowed per device", result["msg"])
-
def test_vyos_bgp_address_family_rendered(self):
set_module_args(
dict(
@@ -708,7 +667,7 @@ class TestVyosBgpafModule14(TestVyosModule):
)
self.execute_module(changed=False, commands=[])
- def test_vyos_bgp_address_family_replaced_asn(self):
+ def test_vyos_bgp_address_family_replaced_asn2(self):
set_module_args(
dict(
state="replaced",
@@ -764,7 +723,7 @@ class TestVyosBgpafModule14(TestVyosModule):
]
self.execute_module(changed=True, commands=commands)
- def test_vyos_bgp_address_family_overridden_asn(self):
+ def test_vyos_bgp_address_family_overridden_asn2(self):
set_module_args(
dict(
state="overridden",
@@ -842,13 +801,13 @@ class TestVyosBgpafOpsModule14(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.bgp_address_family.bgp_address_family.get_os_version"
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_address_family.bgp_address_family.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.bgp_address_family.bgp_address_family.get_os_version"
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.bgp_address_family.bgp_address_family.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
@@ -911,13 +870,13 @@ class TestVyosBgpafOpsModule14(TestVyosModule):
],
redistribute=[
dict(protocol="ospf", metric=25),
- ]
+ ],
),
dict(
afi="ipv6",
redistribute=[
dict(protocol="ospfv3", metric=20),
- dict(protocol="ripng")
+ dict(protocol="ripng"),
],
),
],
@@ -948,13 +907,13 @@ class TestVyosBgpafOpsModule14(TestVyosModule):
],
redistribute=[
dict(protocol="ospf", metric=25),
- ]
+ ],
),
dict(
afi="ipv6",
redistribute=[
dict(protocol="ospfv3", metric=20),
- dict(protocol="ripng")
+ dict(protocol="ripng"),
],
),
],
@@ -982,7 +941,7 @@ class TestVyosBgpafOpsModule14(TestVyosModule):
afi="ipv4",
networks=[
dict(prefix="192.2.13.0/24"),
- ]
+ ],
),
dict(
afi="ipv6",
diff --git a/tests/unit/modules/network/vyos/test_vyos_bgp_global.py b/tests/unit/modules/network/vyos/test_vyos_bgp_global.py
index e363f77b..c3680492 100644
--- a/tests/unit/modules/network/vyos/test_vyos_bgp_global.py
+++ b/tests/unit/modules/network/vyos/test_vyos_bgp_global.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_bgp_global14.py b/tests/unit/modules/network/vyos/test_vyos_bgp_global14.py
index ee647616..cf4a377b 100644
--- a/tests/unit/modules/network/vyos/test_vyos_bgp_global14.py
+++ b/tests/unit/modules/network/vyos/test_vyos_bgp_global14.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_command.py b/tests/unit/modules/network/vyos/test_vyos_command.py
index 2c7cd328..a0ad0942 100644
--- a/tests/unit/modules/network/vyos/test_vyos_command.py
+++ b/tests/unit/modules/network/vyos/test_vyos_command.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_config.py b/tests/unit/modules/network/vyos/test_vyos_config.py
index 4f1cac6a..e732ca60 100644
--- a/tests/unit/modules/network/vyos/test_vyos_config.py
+++ b/tests/unit/modules/network/vyos/test_vyos_config.py
@@ -19,7 +19,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import MagicMock, patch
@@ -140,3 +139,41 @@ class TestVyosConfigModule(TestVyosModule):
return_value=self.cliconf_obj.get_diff(candidate, None, diff_match="none"),
)
self.execute_module(changed=True, commands=lines, sort=False)
+
+ def test_vyos_config_confirm_automatic(self):
+ src = load_fixture("vyos_config_src.cfg")
+ confirm_timeout = 7
+ set_module_args(dict(src=src, confirm="automatic", confirm_timeout=confirm_timeout))
+ candidate = "\n".join(self.module.format_commands(src.splitlines()))
+ commands = [
+ "set system host-name foo",
+ "delete interfaces ethernet eth0 address",
+ ]
+ self.conn.get_diff = MagicMock(
+ return_value=self.cliconf_obj.get_diff(candidate, self.running_config),
+ )
+
+ self.execute_module(changed=True, commands=commands)
+
+ self.assertEqual(self.load_config.call_args[1]["confirm"], confirm_timeout)
+ self.run_commands.assert_called_once()
+ self.assertEqual(
+ ["configure", "confirm", "exit"],
+ self.run_commands.call_args[0][1],
+ )
+
+ def test_vyos_config_confirm_manual(self):
+ lines = [
+ "set system host-name foo",
+ ]
+ confirm_timeout = 12
+ set_module_args(dict(lines=lines, confirm="manual", confirm_timeout=confirm_timeout))
+ candidate = "\n".join(lines)
+ self.conn.get_diff = MagicMock(
+ return_value=self.cliconf_obj.get_diff(candidate, self.running_config),
+ )
+
+ self.execute_module(changed=True, commands=lines)
+
+ self.assertEqual(self.load_config.call_args[1]["confirm"], confirm_timeout)
+ self.run_commands.assert_not_called()
diff --git a/tests/unit/modules/network/vyos/test_vyos_facts.py b/tests/unit/modules/network/vyos/test_vyos_facts.py
index 7e192e3c..3f85bf7d 100644
--- a/tests/unit/modules/network/vyos/test_vyos_facts.py
+++ b/tests/unit/modules/network/vyos/test_vyos_facts.py
@@ -17,7 +17,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
import json
diff --git a/tests/unit/modules/network/vyos/test_vyos_file.py b/tests/unit/modules/network/vyos/test_vyos_file.py
new file mode 100644
index 00000000..3ab5fbe5
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_file.py
@@ -0,0 +1,524 @@
+# tests/unit/modules/network/vyos/test_vyos_file.py
+#
+# Mocks run_commands() directly — the real call path this module uses via
+# get_connection()/run_commands() in module_utils/network/vyos/vyos.py.
+# This replaces an earlier draft that mocked a bespoke ActionModule; that
+# design was abandoned once it turned out every module in this collection
+# (vyos_command, vyos_config, etc.) shares one generic action plugin and
+# puts real logic inside main() instead.
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import hashlib
+import json
+import os
+import tempfile
+
+from unittest.mock import MagicMock, patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_file
+from ansible_collections.vyos.vyos.tests.unit.modules.network.vyos.vyos_module import (
+ TestVyosModule,
+)
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ set_module_args,
+)
+
+
+class TestVyosFileModule(TestVyosModule):
+
+ module = vyos_file
+
+ def setUp(self):
+ super(TestVyosFileModule, self).setUp()
+ self.mock_run_commands = patch(
+ "ansible_collections.vyos.vyos.plugins.modules.vyos_file.run_commands",
+ )
+ self.run_commands = self.mock_run_commands.start()
+
+ # content/src transfer now goes through a real SCP call via
+ # get_connection(module).copy_file(...) — never through
+ # run_commands() — so it needs its own mock, separate from the
+ # command-based stat/chown/chmod/rm path above.
+ self.mock_get_connection = patch(
+ "ansible_collections.vyos.vyos.plugins.modules.vyos_file.get_connection",
+ )
+ self.get_connection = self.mock_get_connection.start()
+ self.mock_connection = MagicMock()
+ self.mock_connection.get_option.return_value = 30
+ self.get_connection.return_value = self.mock_connection
+
+ def tearDown(self):
+ super(TestVyosFileModule, self).tearDown()
+ self.mock_run_commands.stop()
+ self.mock_get_connection.stop()
+
+ # ---- helpers -----------------------------------------------------
+
+ def _queue(self, *responses):
+ """Queue successive return values, one per run_commands() call."""
+ self.run_commands.side_effect = list(responses)
+
+ def _run(self, args, expect_fail=False):
+ set_module_args(args)
+ exc = AnsibleFailJson if expect_fail else AnsibleExitJson
+ with self.assertRaises(exc) as ctx:
+ vyos_file.main()
+ return ctx.exception.args[0]
+
+ # ---- idempotency core ---------------------------------------------
+
+ def test_creates_when_absent(self):
+ # get_have() issues ONE stat call; converge() batches mkdir+chown+
+ # chmod into a SINGLE run_commands() call (not one call per
+ # command); the post-check issues one more stat call. Three total
+ # run_commands() invocations, matching the module's actual batching.
+ self._queue(
+ ["stat: cannot statx '/config/auth/x': No such file or directory"],
+ ["", "", ""], # mkdir, chown, chmod — one batched call
+ ["750 vyos vyattacfg 4096"], # post-check stat
+ )
+ result = self._run(
+ {"dest": "/config/auth/x", "owner": "vyos", "group": "vyattacfg", "mode": "0750"},
+ )
+ self.assertTrue(result["changed"])
+ self.assertIn("state", result["diff_fields"])
+ self.assertIn("owner", result["diff_fields"])
+
+ def test_noop_when_converged(self):
+ self._queue(["750 vyos vyattacfg 4096"])
+ result = self._run(
+ {"dest": "/config/auth/x", "owner": "vyos", "group": "vyattacfg", "mode": "0750"},
+ )
+ self.assertFalse(result["changed"])
+ self.assertEqual(result["diff_fields"], [])
+
+ def test_setgid_ignored_when_mode_leading_digit_is_zero(self):
+ # /config/auth is deliberately setgid vyattacfg (vyos.dev T2713).
+ # Requesting mode '0750' (leading digit 0) must NOT be reported as
+ # different from an actual mode of 2750.
+ self._queue(["2750 vyos vyattacfg 4096"])
+ result = self._run({"dest": "/config/auth/x", "mode": "0750"})
+ self.assertFalse(result["changed"], result.get("diff_fields"))
+
+ def test_setgid_respected_when_explicitly_requested(self):
+ # Explicit non-zero leading digit means the caller does care about
+ # the special bits — since 2750 is requested and 2750 is already
+ # there, this should be a no-op (only the initial stat call fires).
+ self._queue(["2750 vyos vyattacfg 4096"])
+ result = self._run({"dest": "/config/auth/x", "mode": "2750"})
+ self.assertFalse(result["changed"])
+
+ def test_mode_change_detected(self):
+ # Only mode differs, so converge() batches a single chmod command
+ # (one run_commands() call), then the post-check stat is a second.
+ self._queue(
+ ["600 vyos vyattacfg 10"],
+ [""], # chmod — the only mutating command needed
+ ["640 vyos vyattacfg 10"],
+ )
+ result = self._run({"dest": "/config/auth/x/hello.txt", "mode": "0640"})
+ self.assertTrue(result["changed"])
+ self.assertEqual(result["diff_fields"], ["mode"])
+
+ def test_mode_string_normalization(self):
+ # "00640" (5 digits) is deliberately excluded here — it's now
+ # correctly rejected by the strict [0-7]{3,4} validation (see
+ # test_rejects_mode_with_extra_leading_digit), even though its
+ # value is harmless. Only genuinely valid 3-4 digit forms of the
+ # same value are expected to normalize equivalently.
+ for requested in ("640", "0640"):
+ with self.subTest(requested=requested):
+ self._queue(["640 vyos vyattacfg 10"])
+ result = self._run({"dest": "/config/auth/x/hello.txt", "mode": requested})
+ self.assertFalse(
+ result["changed"],
+ "mode {0!r} incorrectly compared unequal to stat's '640'".format(requested),
+ )
+
+ def test_implicit_mode_uses_symbolic_chmod_preserving_special_bits(self):
+ # Real bug found in review: a plain numeric chmod ALWAYS explicitly
+ # sets the special-bits digit (even a bare 3-digit form implies a
+ # leading 0), so it would silently clear an existing setgid/setuid
+ # bit the moment any rwx change is needed — directly contradicting
+ # the "special bits are unmanaged for implicit mode" guarantee this
+ # module's own diff comparison already promises. Symbolic chmod
+ # (u=,g=,o=) is the only form that genuinely leaves them untouched.
+ self._queue(
+ ["2770 vyos vyattacfg 10"], # existing: setgid + rwxrwx---
+ [""], # the single batched chmod command
+ ["2750 vyos vyattacfg 10"], # post-check: rwx fixed, setgid survived
+ )
+ result = self._run({"dest": "/x", "mode": "0750"})
+ self.assertTrue(result["changed"])
+ self.assertEqual(result["diff_fields"], ["mode"])
+
+ converge_call = self.run_commands.call_args_list[1]
+ chmod_cmd = converge_call.args[1][0]
+ self.assertIn("u=", chmod_cmd, "expected symbolic chmod for an implicit mode request")
+ self.assertNotRegex(
+ chmod_cmd,
+ r"chmod\s+0?750\b",
+ "must not use a numeric chmod for an implicit mode request — it would "
+ "clear the existing setgid bit",
+ )
+
+ def test_explicit_mode_uses_numeric_chmod(self):
+ # A non-zero leading digit means the caller explicitly wants control
+ # over special bits too — numeric chmod is correct here, unlike the
+ # implicit case above.
+ self._queue(
+ ["0750 vyos vyattacfg 10"], # existing: no special bits
+ [""],
+ ["2750 vyos vyattacfg 10"], # post-check: matches the explicit request
+ )
+ result = self._run({"dest": "/x", "mode": "2750"})
+ self.assertTrue(result["changed"])
+
+ converge_call = self.run_commands.call_args_list[1]
+ chmod_cmd = converge_call.args[1][0]
+ self.assertIn("2750", chmod_cmd)
+ self.assertNotIn("u=", chmod_cmd, "explicit mode should use numeric chmod, not symbolic")
+
+ # ---- content ---------------------------------------------------------
+
+ def test_content_push_detected_and_verified(self):
+ # Content transfer now stages to a /tmp path via copy_file(), then
+ # relocates into `dest` via a sudo-prefixed mv (run_commands call).
+ # That mv is now a separate run_commands() call inserted between
+ # the initial stat and the chown+chmod batch.
+ real_hash = "98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4"
+ self._queue(
+ ["stat: cannot statx '/config/auth/x/hello.txt': No such file or directory"],
+ [""], # mv staging path -> dest
+ ["", ""], # chown, chmod — batched
+ ["600 vyos vyattacfg 10"], # post-check stat
+ ["{0} /config/auth/x/hello.txt".format(real_hash)], # post-check sha256sum
+ )
+ result = self._run(
+ {
+ "dest": "/config/auth/x/hello.txt",
+ "content": "hi\n",
+ "owner": "vyos",
+ "mode": "0600",
+ },
+ )
+ self.assertTrue(result["changed"])
+ self.assertIn("content", result["diff_fields"])
+ self.mock_connection.copy_file.assert_called_once()
+ # copy_file's destination is now the /tmp staging path, NOT the
+ # final dest — the mv (with become applied) does the real placement.
+ staged_dest = self.mock_connection.copy_file.call_args.kwargs["destination"]
+ self.assertTrue(staged_dest.startswith("/tmp/.vyos_file_staging_"))
+ mv_call = self.run_commands.call_args_list[1]
+ mv_cmd = mv_call.args[1][0]
+ self.assertIn("mv", mv_cmd)
+ self.assertIn(staged_dest, mv_cmd)
+ self.assertIn("/config/auth/x/hello.txt", mv_cmd)
+
+ def test_src_upload_reads_local_file_and_pushes_content(self):
+ # src takes a different code path from content (read_local_bytes()
+ # opens the local path rather than encoding an inline string), and
+ # had no direct test coverage — this exercises that path explicitly
+ # using a real temporary file, since local_content_hash()/
+ # read_local_bytes() do plain open() calls that aren't mockable
+ # through run_commands.
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False) as f:
+ f.write("-----BEGIN CERTIFICATE-----\nfakecertdata\n-----END CERTIFICATE-----\n")
+ local_path = f.name
+ try:
+ real_hash = hashlib.sha256(
+ b"-----BEGIN CERTIFICATE-----\nfakecertdata\n-----END CERTIFICATE-----\n",
+ ).hexdigest()
+ self._queue(
+ ["stat: cannot statx '/config/auth/x/client.pem': No such file or directory"],
+ [""], # mv staging path -> dest
+ ["", ""], # chown, chmod — batched
+ ["600 vyos vyattacfg 10"],
+ ["{0} /config/auth/x/client.pem".format(real_hash)],
+ )
+ result = self._run(
+ {
+ "dest": "/config/auth/x/client.pem",
+ "src": local_path,
+ "owner": "vyos",
+ "mode": "0600",
+ },
+ )
+ self.assertTrue(result["changed"])
+ self.assertIn("content", result["diff_fields"])
+ self.mock_connection.copy_file.assert_called_once()
+ self.assertEqual(
+ self.mock_connection.copy_file.call_args.kwargs["source"],
+ local_path,
+ )
+ finally:
+ os.unlink(local_path)
+
+ def test_src_upload_idempotent_on_matching_remote_content(self):
+ # have already matches want entirely -> diff is empty -> converge()
+ # (and therefore push_content_via_scp/mv) never runs at all, so
+ # this stays at 1 run_commands call, unaffected by the staging change.
+ data = b"identical content\n"
+ with tempfile.NamedTemporaryFile(mode="wb", suffix=".txt", delete=False) as f:
+ f.write(data)
+ local_path = f.name
+ try:
+ real_hash = hashlib.sha256(data).hexdigest()
+ self._queue(
+ ["600 vyos vyattacfg 10"],
+ ["{0} /x".format(real_hash)],
+ )
+ result = self._run({"dest": "/x", "src": local_path, "owner": "vyos", "mode": "0600"})
+ self.assertFalse(result["changed"], result.get("diff_fields"))
+ self.mock_connection.copy_file.assert_not_called()
+ finally:
+ os.unlink(local_path)
+
+ def test_content_hash_looked_up_only_when_relevant(self):
+ # plain ownership/mode management on an existing path should never
+ # trigger a sha256sum call — that's the whole point of the
+ # need_content_hash gate.
+ self._queue(["750 vyos vyattacfg 4096"])
+ self._run({"dest": "/config/auth/x", "mode": "0750"})
+ called_commands = [c.args[1] for c in self.run_commands.call_args_list]
+ joined = " ".join(str(c) for c in called_commands)
+ self.assertNotIn("sha256sum", joined)
+
+ # ---- absent state ----------------------------------------------------
+
+ def test_absent_on_existing_removes(self):
+ self._queue(
+ ["600 vyos vyattacfg 10"],
+ [""], # rm -rf
+ ["stat: cannot statx '/config/auth/x/hello.txt': No such file or directory"],
+ )
+ result = self._run({"dest": "/config/auth/x/hello.txt", "state": "absent"})
+ self.assertTrue(result["changed"])
+ self.assertEqual(result["diff_fields"], ["state"])
+
+ def test_absent_noop_when_already_gone(self):
+ self._queue(["stat: cannot statx '/x': No such file or directory"])
+ result = self._run({"dest": "/x", "state": "absent"})
+ self.assertFalse(result["changed"])
+
+ def test_real_stat_error_fails_loudly_instead_of_treated_as_missing(self):
+ # Permission denied (or any other real stat failure) must NOT be
+ # silently treated the same as "doesn't exist" — that could lead
+ # the module to attempt mkdir/chown/chmod against a path it
+ # actually has no real visibility into.
+ self._queue(["stat: cannot statx '/x': Permission denied"])
+ result = self._run({"dest": "/x", "mode": "0750"}, expect_fail=True)
+ self.assertIn("unexpected stat output", result["msg"])
+
+ # ---- destination path validation ---------------------------------
+
+ def test_rejects_relative_path(self):
+ # No run_commands() calls should even be attempted for an invalid
+ # dest — validation must happen before any stat/converge logic.
+ result = self._run({"dest": "relative/path"}, expect_fail=True)
+ self.assertIn("absolute path", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_rejects_root_path(self):
+ result = self._run({"dest": "/", "state": "absent"}, expect_fail=True)
+ self.assertIn("root filesystem", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_rejects_double_slash_root_bypass(self):
+ # os.path.normpath preserves "//" as-is (a POSIX quirk for exactly
+ # two leading slashes) rather than collapsing it to "/" — a naive
+ # `normalized == "/"` check would miss this and let it through.
+ result = self._run({"dest": "//", "state": "absent"}, expect_fail=True)
+ self.assertIn("root filesystem", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_rejects_dot_path_that_normalizes_to_root(self):
+ result = self._run({"dest": "/.", "state": "absent"}, expect_fail=True)
+ self.assertIn("root filesystem", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ # ---- mode validation -----------------------------------------------
+
+ def test_rejects_mode_with_extra_leading_digit(self):
+ # The exact real bug found in review: _normalize_mode()'s
+ # zfill(4)[-4:] would silently truncate "10640" into "0640" rather
+ # than rejecting an obviously malformed 5-digit value — applying
+ # permissions the caller never actually asked for.
+ result = self._run({"dest": "/x", "mode": "10640"}, expect_fail=True)
+ self.assertIn("octal string", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_rejects_non_octal_digits(self):
+ result = self._run({"dest": "/x", "mode": "0890"}, expect_fail=True)
+ self.assertIn("octal string", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_rejects_non_numeric_mode(self):
+ result = self._run({"dest": "/x", "mode": "abcd"}, expect_fail=True)
+ self.assertIn("octal string", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_rejects_too_short_mode(self):
+ result = self._run({"dest": "/x", "mode": "07"}, expect_fail=True)
+ self.assertIn("octal string", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_accepts_valid_3_and_4_digit_modes(self):
+ # Sanity check that the new strict validation doesn't reject
+ # legitimate input alongside the malformed cases above.
+ for valid_mode in ("750", "0750", "2750", "0000", "7777"):
+ with self.subTest(valid_mode=valid_mode):
+ self._queue([" ".join([valid_mode.zfill(4), "vyos", "vyattacfg", "10"])])
+ result = self._run({"dest": "/x", "mode": valid_mode})
+ self.assertFalse(result["changed"])
+
+ # ---- src validation --------------------------------------------------
+
+ def test_rejects_missing_src_file(self):
+ # Without this check, open() inside local_content_hash() would
+ # raise an unhandled FileNotFoundError instead of a clean module
+ # error — and this happens even under check_mode, since content
+ # hashing runs before the check-mode short-circuit.
+ result = self._run(
+ {"dest": "/x", "src": "/definitely/does/not/exist/x.pem"},
+ expect_fail=True,
+ )
+ self.assertIn("src not found", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_rejects_src_that_is_a_directory(self):
+ with tempfile.TemporaryDirectory() as tmpdir:
+ result = self._run({"dest": "/x", "src": tmpdir}, expect_fail=True)
+ self.assertIn("directory", result["msg"])
+ self.assertEqual(self.run_commands.call_count, 0)
+
+ def test_malformed_sha256sum_output_does_not_get_recorded_as_a_hash(self):
+ # If sha256sum itself errors (e.g. a race where the file vanished
+ # between stat and sha256sum), the garbage output must not be
+ # silently trusted as a real content hash — that would corrupt the
+ # comparison instead of surfacing as a real, visible diff.
+ # check_mode=True keeps this isolated to have/diff computation only,
+ # without needing to model a full converge cycle.
+ self._queue(
+ ["600 vyos vyattacfg 10"],
+ ["sha256sum: /x: No such file or directory"],
+ )
+ set_module_args({"dest": "/x", "content": "hi\n", "_ansible_check_mode": True})
+ with self.assertRaises(AnsibleExitJson) as ctx:
+ vyos_file.main()
+ result = ctx.exception.args[0]
+ # have.content_hash stays unset -> compared against a real want hash
+ # -> reported as a genuine diff, not silently accepted as converged.
+ self.assertIn("content", result.get("diff_fields", []))
+
+ # ---- silent-failure detection (the real bug this caught on hardware) --
+
+ def test_post_check_fails_module_when_chown_silently_no_ops(self):
+ # Reproduces the real failure found on hardware: chown to a
+ # nonexistent group prints an error but the CLI still reports the
+ # line as "executed" with rc 0 — have must be re-verified.
+ # converge() batches mkdir+chown into one call (mode wasn't
+ # requested, so no chmod); the second queued item represents that
+ # single batched call's two responses.
+ self._queue(
+ ["stat: cannot statx '/x': No such file or directory"],
+ ["", "chown: invalid group: 'x:bogus'"], # mkdir ok, chown failed
+ ["644 root nogroup 4096"], # post-check: neither owner nor group took
+ )
+ result = self._run({"dest": "/x", "owner": "vyos", "group": "bogus"}, expect_fail=True)
+ self.assertIn("post-check", result["msg"])
+
+ # ---- check_mode --------------------------------------------------------
+
+ def test_check_mode_reports_diff_without_converging(self):
+ self._queue(["600 vyos vyattacfg 10"])
+ set_module_args({"dest": "/x", "mode": "0640", "_ansible_check_mode": True})
+ with self.assertRaises(AnsibleExitJson) as ctx:
+ vyos_file.main()
+ result = ctx.exception.args[0]
+ self.assertTrue(result["changed"])
+ self.assertEqual(result["diff_fields"], ["mode"])
+ # only the initial stat call should have happened — no chmod
+ self.assertEqual(self.run_commands.call_count, 1)
+
+ # ---- secrets discipline ------------------------------------------------
+
+ def test_content_push_via_scp_creates_and_cleans_up_temp_file_for_inline_content(self):
+ # For `content` (no src), a real local temp file must be created to
+ # hand to copy_file() (SCP needs a real source path — it can't
+ # stream an in-memory string), and that temp file must be removed
+ # again afterward regardless of outcome, since it briefly holds
+ # secret material on the controller's local disk.
+ captured_path = {}
+
+ def fake_copy_file(source, destination, proto, timeout):
+ captured_path["source"] = source
+ # the temp file must exist at the moment copy_file is invoked
+ self.assertTrue(os.path.exists(source))
+ with open(source, "rb") as f:
+ self.assertEqual(f.read(), b"hi\n")
+
+ self.mock_connection.copy_file.side_effect = fake_copy_file
+ self._queue(
+ ["stat: cannot statx '/x': No such file or directory"],
+ [""], # mv staging path -> dest (now unconditional, even with no owner/mode)
+ ["600 vyos vyattacfg 10"],
+ [hashlib.sha256(b"hi\n").hexdigest() + " /x"],
+ )
+ self._run({"dest": "/x", "content": "hi\n"})
+ # cleaned up after the transfer completes — nothing sensitive left
+ # sitting on the controller's local disk
+ self.assertFalse(os.path.exists(captured_path["source"]))
+
+ def test_content_push_via_scp_uses_src_path_directly_without_a_temp_file(self):
+ # When src is given, the provided path IS the source — no temp
+ # file should be created or deleted for it.
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
+ f.write("real file content\n")
+ local_path = f.name
+ try:
+ self._queue(
+ ["stat: cannot statx '/x': No such file or directory"],
+ [""], # mv staging path -> dest
+ ["600 vyos vyattacfg 10"],
+ [hashlib.sha256(b"real file content\n").hexdigest() + " /x"],
+ )
+ self._run({"dest": "/x", "src": local_path})
+ self.assertEqual(
+ self.mock_connection.copy_file.call_args.kwargs["source"],
+ local_path,
+ )
+ # the caller's own file must still exist — this module must
+ # never delete a user-provided src path
+ self.assertTrue(os.path.exists(local_path))
+ finally:
+ os.unlink(local_path)
+
+ def test_content_not_echoed_in_result(self):
+ # No owner/group/mode requested here, so converge()'s cmds list
+ # stays empty — but push_content_via_scp's mv is unconditional
+ # regardless, so it's still 4 run_commands calls total: initial
+ # stat, mv, post-check stat, post-check sha256sum.
+ real_hash = "03767fbe485736bb40cc5d85e4c9bb10b12a415674b46faf005aa22188a39a10"
+ self._queue(
+ ["stat: cannot statx '/x': No such file or directory"],
+ [""], # mv staging path -> dest
+ ["600 root root 4"], # post-check stat
+ ["{0} /x".format(real_hash)], # post-check sha256sum
+ )
+ result = self._run({"dest": "/x", "content": "super-secret-value"})
+ self.assertNotIn("super-secret-value", json.dumps(result))
+ # the secret must not leak into the copy_file() call args either —
+ # only a real local temp-file path should appear there
+ for call in self.mock_connection.copy_file.call_args_list:
+ self.assertNotIn("super-secret-value", str(call))
+ # nor into any run_commands() call — the mv only ever references
+ # paths (staging path, dest), never file content
+ for call in self.run_commands.call_args_list:
+ self.assertNotIn("super-secret-value", str(call))
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 481cc1dd..f62ed4b8 100644
--- a/tests/unit/modules/network/vyos/test_vyos_firewall_global.py
+++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -109,7 +108,7 @@ class TestVyosFirewallGlobalModule(TestVyosModule):
),
dict(
afi="ipv6",
- ip_src_route=True,
+ ip_src_route=False,
icmp_redirects=dict(receive=False),
),
],
@@ -183,6 +182,7 @@ class TestVyosFirewallGlobalModule(TestVyosModule):
"set firewall group port-group TELNET description 'This group has the telnet ports'",
"set firewall group port-group TELNET",
"set firewall ip-src-route 'enable'",
+ "set firewall ipv6-src-route 'disable'",
"set firewall receive-redirects 'disable'",
"set firewall config-trap 'enable'",
"set firewall ipv6-receive-redirects 'disable'",
@@ -269,6 +269,12 @@ class TestVyosFirewallGlobalModule(TestVyosModule):
],
),
dict(
+ afi="ipv4",
+ name="DELETE-HOSTS",
+ description="The (single) last address from this group will be deleted in the tests",
+ # No members here
+ ),
+ dict(
afi="ipv6",
name="LOCAL-v6",
description="This group has the hosts address lists of this machine",
@@ -309,6 +315,7 @@ class TestVyosFirewallGlobalModule(TestVyosModule):
"delete firewall send-redirects",
"delete firewall group address-group RND-HOSTS address 192.0.2.3",
"delete firewall group address-group RND-HOSTS address 192.0.2.5",
+ "delete firewall group address-group DELETE-HOSTS address",
"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 network-group RND description",
@@ -376,6 +383,7 @@ class TestVyosFirewallGlobalModule(TestVyosModule):
),
)
commands = [
+ "delete firewall group address-group DELETE-HOSTS",
"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 ipv6-src-route",
@@ -412,6 +420,14 @@ class TestVyosFirewallGlobalModule(TestVyosModule):
],
),
dict(
+ afi="ipv4",
+ name="DELETE-HOSTS",
+ description="The (single) last address from this group will be deleted in the tests",
+ members=[
+ dict(address="1.2.3.4"),
+ ],
+ ),
+ dict(
afi="ipv6",
name="LOCAL-v6",
description="This group has the hosts address lists of this machine",
diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py
index aae4aa83..397ccfbd 100644
--- a/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py
+++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -109,7 +108,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
),
dict(
afi="ipv6",
- ip_src_route=True,
+ ip_src_route=False,
icmp_redirects=dict(receive=False),
),
],
@@ -159,6 +158,31 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
),
],
),
+ zone=[
+ dict(
+ name="FZP-2",
+ default_action="reject",
+ default_log=True,
+ description="This is the Firewall zone fzp2",
+ interfaces=[
+ "eth2",
+ "lo0",
+ ],
+ local_zone=True,
+ sources=[
+ dict(
+ zone="fzp5",
+ firewall=dict(
+ name="fzp1",
+ ipv6_name="fzp1-v6",
+ ),
+ ),
+ ],
+ intra_zone_filtering=dict(
+ action="accept",
+ ),
+ ),
+ ],
),
state="merged",
),
@@ -185,6 +209,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
"set firewall global-options ip-src-route 'enable'",
"set firewall global-options receive-redirects 'disable'",
"set firewall global-options config-trap 'enable'",
+ "set firewall global-options ipv6-src-route 'disable'",
"set firewall global-options ipv6-receive-redirects 'disable'",
"set firewall global-options state-policy established action 'accept'",
"set firewall global-options state-policy established log",
@@ -195,6 +220,15 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
"set firewall global-options twa-hazards-protection 'enable'",
"set firewall global-options syn-cookies 'enable'",
"set firewall global-options source-validation 'strict'",
+ "set firewall zone FZP-2 default-action 'reject'",
+ "set firewall zone FZP-2 default-log",
+ "set firewall zone FZP-2 description 'This is the Firewall zone fzp2'",
+ "set firewall zone FZP-2 from fzp5 firewall ipv6-name fzp1-v6",
+ "set firewall zone FZP-2 from fzp5 firewall name fzp1",
+ "set firewall zone FZP-2 interface eth2",
+ "set firewall zone FZP-2 interface lo0",
+ "set firewall zone FZP-2 intra-zone-filtering action accept",
+ "set firewall zone FZP-2 local-zone",
]
self.execute_module(changed=True, commands=commands)
@@ -206,6 +240,10 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
address_group=[
dict(
afi="ipv4",
+ name="A-EMPTY",
+ ),
+ dict(
+ afi="ipv4",
name="RND-HOSTS",
description="This group has the Management hosts address lists",
members=[
@@ -272,6 +310,12 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
],
),
dict(
+ afi="ipv4",
+ name="DELETE-HOSTS",
+ description="The (single) last address from this group will be deleted in the tests",
+ # No members here
+ ),
+ dict(
afi="ipv6",
name="LOCAL-v6",
description="This group has the hosts address lists of this machine",
@@ -280,6 +324,14 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
dict(address="fdec:2503:89d6:59b3::2"),
],
),
+ dict(
+ afi="ipv4",
+ name="ZONE-TEST",
+ members=[
+ dict(address="1.2.3.4"),
+ ],
+ description="This is a new description for a address group name that is also in a zone",
+ ),
],
network_group=[
dict(
@@ -303,25 +355,64 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
),
],
),
+ zone=[
+ dict(
+ name="FZP-2",
+ default_action="drop",
+ default_log=True,
+ description="This is the Firewall zone fzp2",
+ interfaces=[
+ "eth2",
+ "lo0",
+ ],
+ local_zone=True,
+ sources=[
+ dict(
+ zone="fzp5",
+ firewall=dict(
+ name="fzp1",
+ ipv6_name="fzp1-v6",
+ ),
+ ),
+ ],
+ intra_zone_filtering=dict(
+ action="accept",
+ ),
+ ),
+ ],
),
state="replaced",
),
)
commands = [
+ "delete firewall group address-group A-EMPTY",
"delete firewall group address-group RND-HOSTS address 192.0.2.3",
"delete firewall group address-group RND-HOSTS address 192.0.2.5",
+ "delete firewall group address-group DELETE-HOSTS address",
"delete firewall global-options all-ping",
"delete firewall global-options state-policy related",
"delete firewall global-options ipv6-src-route",
"delete firewall global-options send-redirects",
+ "delete firewall zone ZONE-IZF",
"set firewall global-options state-policy invalid action 'reject'",
"set firewall group address-group RND-HOSTS address 192.0.2.7",
"set firewall group address-group RND-HOSTS address 192.0.2.9",
+ "set firewall group address-group ZONE-TEST description 'This is a new description for a address group name that is also in a zone'",
+ "delete firewall zone ZONE-TEST",
"delete firewall group network-group RND description",
"delete firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1",
"set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::2",
"delete firewall group port-group SSH port 22",
"set firewall group port-group SSH port 2222",
+ "set firewall zone FZP-2 default-action 'drop'",
+ "set firewall zone FZP-2 default-log",
+ "set firewall zone FZP-2 description 'This is the Firewall zone fzp2'",
+ "set firewall zone FZP-2 from fzp5 firewall ipv6-name fzp1-v6",
+ "set firewall zone FZP-2 from fzp5 firewall name fzp1",
+ "set firewall zone FZP-2 interface eth2",
+ "set firewall zone FZP-2 interface lo0",
+ "set firewall zone FZP-2 intra-zone-filtering action accept",
+ "set firewall zone FZP-2 local-zone",
]
self.execute_module(changed=True, commands=commands)
@@ -341,6 +432,10 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
address_group=[
dict(
afi="ipv4",
+ name="A-EMPTY",
+ ),
+ dict(
+ afi="ipv4",
name="RND-HOSTS",
description="This group has the Management hosts address lists",
members=[
@@ -350,6 +445,21 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
],
),
dict(
+ afi="ipv4",
+ name="DELETE-HOSTS",
+ description="The (single) last address from this group will be deleted in the tests",
+ members=[
+ dict(address="1.2.3.4"),
+ ],
+ ),
+ dict(
+ afi="ipv4",
+ name="ZONE-TEST",
+ members=[
+ dict(address="1.2.3.4"),
+ ],
+ ),
+ dict(
afi="ipv6",
name="LOCAL-v6",
description="This group has the hosts address lists of this machine",
@@ -381,6 +491,24 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
),
],
),
+ zone=[
+ dict(
+ name="ZONE-TEST",
+ description="zone-test test description",
+ interfaces=["eth0.1234"],
+ ),
+ dict(
+ name="ZONE-IZF",
+ description="zone for izf and sources delete path testing",
+ intra_zone_filtering=dict(action="drop"),
+ sources=[
+ dict(
+ zone="zone-src",
+ firewall=dict(name="existing-ruleset"),
+ ),
+ ],
+ ),
+ ],
),
state="replaced",
),
@@ -416,6 +544,14 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
dict(address="fdec:2503:89d6:59b3::2"),
],
),
+ dict(
+ afi="ipv4",
+ name="ZONE-TEST",
+ members=[
+ dict(address="4.3.2.1"),
+ ],
+ description="This is a new description for a address group name that is also in a zone",
+ ),
],
network_group=[
dict(
@@ -439,21 +575,38 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
),
],
),
+ zone=[
+ dict(
+ name="ZONE-TEST",
+ description="zone-test test description",
+ interfaces=[
+ "lo",
+ ],
+ ),
+ ],
),
state="replaced",
),
)
commands = [
+ "delete firewall group address-group A-EMPTY",
"delete firewall group address-group RND-HOSTS address 192.0.2.3",
"delete firewall group address-group RND-HOSTS address 192.0.2.5",
+ "delete firewall group address-group ZONE-TEST address 1.2.3.4",
+ "delete firewall zone ZONE-IZF",
"delete firewall global-options all-ping",
"delete firewall global-options ipv6-src-route",
"delete firewall global-options send-redirects",
"set firewall global-options state-policy related action 'drop'",
"delete firewall global-options state-policy related log-level",
+ "delete firewall group address-group DELETE-HOSTS",
"set firewall global-options state-policy invalid action 'reject'",
"set firewall group address-group RND-HOSTS address 192.0.2.7",
"set firewall group address-group RND-HOSTS address 192.0.2.9",
+ "set firewall group address-group ZONE-TEST address 4.3.2.1",
+ "delete firewall zone ZONE-TEST interface eth0.1234",
+ "set firewall zone ZONE-TEST interface lo",
+ "set firewall group address-group ZONE-TEST description 'This is a new description for a address group name that is also in a zone'",
"delete firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1",
"set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::2",
"delete firewall group port-group SSH port 22",
@@ -465,3 +618,401 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
set_module_args(dict(config=dict(), state="deleted"))
commands = ["delete firewall"]
self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_firewall_global_set_03_replaced_izf_action_change(self):
+ set_module_args(
+ dict(
+ config=dict(
+ ping=dict(all=True),
+ route_redirects=[
+ dict(ip_src_route=True, afi="ipv6"),
+ dict(icmp_redirects=dict(send=True), afi="ipv4"),
+ ],
+ state_policy=[
+ dict(connection_type="related", action="accept", log_level="alert"),
+ ],
+ group=dict(
+ address_group=[
+ dict(afi="ipv4", name="A-EMPTY"),
+ dict(
+ afi="ipv4",
+ name="RND-HOSTS",
+ description="This group has the Management hosts address lists",
+ members=[
+ dict(address="192.0.2.1"),
+ dict(address="192.0.2.3"),
+ dict(address="192.0.2.5"),
+ ],
+ ),
+ dict(
+ afi="ipv4",
+ name="DELETE-HOSTS",
+ description="The (single) last address from this group will be deleted in the tests",
+ members=[dict(address="1.2.3.4")],
+ ),
+ dict(
+ afi="ipv4",
+ name="ZONE-TEST",
+ members=[dict(address="1.2.3.4")],
+ ),
+ dict(
+ afi="ipv6",
+ name="LOCAL-v6",
+ description="This group has the hosts address lists of this machine",
+ members=[
+ dict(address="::1"),
+ dict(address="fdec:2503:89d6:59b3::1"),
+ ],
+ ),
+ ],
+ network_group=[
+ dict(
+ afi="ipv4",
+ name="RND",
+ description="This group has the Management network addresses",
+ members=[dict(address="192.0.2.0/24")],
+ ),
+ dict(
+ afi="ipv6",
+ name="UNIQUE-LOCAL-v6",
+ description="This group encompasses the ULA address space in IPv6",
+ members=[dict(address="fc00::/7")],
+ ),
+ ],
+ port_group=[
+ dict(
+ name="SSH",
+ description="This group has the ssh ports",
+ members=[dict(port="22")],
+ ),
+ ],
+ ),
+ zone=[
+ dict(
+ name="ZONE-TEST",
+ description="zone-test test description",
+ interfaces=["eth0.1234"],
+ ),
+ dict(
+ name="ZONE-IZF",
+ description="zone for izf and sources delete path testing",
+ intra_zone_filtering=dict(action="accept"), # was 'drop'
+ sources=[
+ dict(
+ zone="zone-src",
+ firewall=dict(name="existing-ruleset"),
+ ),
+ ],
+ ),
+ ],
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete firewall zone ZONE-IZF intra-zone-filtering action",
+ "set firewall zone ZONE-IZF intra-zone-filtering action accept",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_firewall_global_set_04_replaced_sources_value_change(self):
+ # ZONE-IZF.sources[0].firewall.name changes from 'existing-ruleset'
+ # (fixture) to 'new-ruleset'. Exercises _render_sources' `not opr
+ # and hfw` (delete) branch. Pre-fix this leaked the old value into
+ # the delete command.
+ set_module_args(
+ dict(
+ config=dict(
+ ping=dict(all=True),
+ route_redirects=[
+ dict(ip_src_route=True, afi="ipv6"),
+ dict(icmp_redirects=dict(send=True), afi="ipv4"),
+ ],
+ state_policy=[
+ dict(connection_type="related", action="accept", log_level="alert"),
+ ],
+ group=dict(
+ address_group=[
+ dict(afi="ipv4", name="A-EMPTY"),
+ dict(
+ afi="ipv4",
+ name="RND-HOSTS",
+ description="This group has the Management hosts address lists",
+ members=[
+ dict(address="192.0.2.1"),
+ dict(address="192.0.2.3"),
+ dict(address="192.0.2.5"),
+ ],
+ ),
+ dict(
+ afi="ipv4",
+ name="DELETE-HOSTS",
+ description="The (single) last address from this group will be deleted in the tests",
+ members=[dict(address="1.2.3.4")],
+ ),
+ dict(
+ afi="ipv4",
+ name="ZONE-TEST",
+ members=[dict(address="1.2.3.4")],
+ ),
+ dict(
+ afi="ipv6",
+ name="LOCAL-v6",
+ description="This group has the hosts address lists of this machine",
+ members=[
+ dict(address="::1"),
+ dict(address="fdec:2503:89d6:59b3::1"),
+ ],
+ ),
+ ],
+ network_group=[
+ dict(
+ afi="ipv4",
+ name="RND",
+ description="This group has the Management network addresses",
+ members=[dict(address="192.0.2.0/24")],
+ ),
+ dict(
+ afi="ipv6",
+ name="UNIQUE-LOCAL-v6",
+ description="This group encompasses the ULA address space in IPv6",
+ members=[dict(address="fc00::/7")],
+ ),
+ ],
+ port_group=[
+ dict(
+ name="SSH",
+ description="This group has the ssh ports",
+ members=[dict(port="22")],
+ ),
+ ],
+ ),
+ zone=[
+ dict(
+ name="ZONE-TEST",
+ description="zone-test test description",
+ interfaces=["eth0.1234"],
+ ),
+ dict(
+ name="ZONE-IZF",
+ description="zone for izf and sources delete path testing",
+ intra_zone_filtering=dict(action="drop"),
+ sources=[
+ dict(
+ zone="zone-src",
+ firewall=dict(name="new-ruleset"), # was 'existing-ruleset'
+ ),
+ ],
+ ),
+ ],
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete firewall zone ZONE-IZF from zone-src firewall name",
+ "set firewall zone ZONE-IZF from zone-src firewall name new-ruleset",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_firewall_global_set_05_replaced_sources_firewall_cleared(self):
+ # ZONE-IZF.sources[0] keeps the 'zone-src' key but drops the
+ # 'firewall' sub-dict entirely. Exercises _render_sources' `not opr
+ # and not hfw` branch -- unreachable before the fix, since it was
+ # guarded by `not have` instead of `not hfw`, and `have` can never
+ # be empty here (we're already inside `if zone in have_index`).
+ set_module_args(
+ dict(
+ config=dict(
+ ping=dict(all=True),
+ route_redirects=[
+ dict(ip_src_route=True, afi="ipv6"),
+ dict(icmp_redirects=dict(send=True), afi="ipv4"),
+ ],
+ state_policy=[
+ dict(connection_type="related", action="accept", log_level="alert"),
+ ],
+ group=dict(
+ address_group=[
+ dict(afi="ipv4", name="A-EMPTY"),
+ dict(
+ afi="ipv4",
+ name="RND-HOSTS",
+ description="This group has the Management hosts address lists",
+ members=[
+ dict(address="192.0.2.1"),
+ dict(address="192.0.2.3"),
+ dict(address="192.0.2.5"),
+ ],
+ ),
+ dict(
+ afi="ipv4",
+ name="DELETE-HOSTS",
+ description="The (single) last address from this group will be deleted in the tests",
+ members=[dict(address="1.2.3.4")],
+ ),
+ dict(
+ afi="ipv4",
+ name="ZONE-TEST",
+ members=[dict(address="1.2.3.4")],
+ ),
+ dict(
+ afi="ipv6",
+ name="LOCAL-v6",
+ description="This group has the hosts address lists of this machine",
+ members=[
+ dict(address="::1"),
+ dict(address="fdec:2503:89d6:59b3::1"),
+ ],
+ ),
+ ],
+ network_group=[
+ dict(
+ afi="ipv4",
+ name="RND",
+ description="This group has the Management network addresses",
+ members=[dict(address="192.0.2.0/24")],
+ ),
+ dict(
+ afi="ipv6",
+ name="UNIQUE-LOCAL-v6",
+ description="This group encompasses the ULA address space in IPv6",
+ members=[dict(address="fc00::/7")],
+ ),
+ ],
+ port_group=[
+ dict(
+ name="SSH",
+ description="This group has the ssh ports",
+ members=[dict(port="22")],
+ ),
+ ],
+ ),
+ zone=[
+ dict(
+ name="ZONE-TEST",
+ description="zone-test test description",
+ interfaces=["eth0.1234"],
+ ),
+ dict(
+ name="ZONE-IZF",
+ description="zone for izf and sources delete path testing",
+ intra_zone_filtering=dict(action="drop"),
+ sources=[
+ dict(zone="zone-src"), # firewall dropped entirely
+ ],
+ ),
+ ],
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete firewall zone ZONE-IZF from zone-src",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_firewall_global_set_06_replaced_sources_entry_removed(self):
+ # ZONE-IZF's sources list drops the 'zone-src' entry entirely (not
+ # just its firewall sub-dict). Exercises the new `elif not opr:`
+ # branch -- previously this case emitted nothing at all.
+ set_module_args(
+ dict(
+ config=dict(
+ ping=dict(all=True),
+ route_redirects=[
+ dict(ip_src_route=True, afi="ipv6"),
+ dict(icmp_redirects=dict(send=True), afi="ipv4"),
+ ],
+ state_policy=[
+ dict(connection_type="related", action="accept", log_level="alert"),
+ ],
+ group=dict(
+ address_group=[
+ dict(afi="ipv4", name="A-EMPTY"),
+ dict(
+ afi="ipv4",
+ name="RND-HOSTS",
+ description="This group has the Management hosts address lists",
+ members=[
+ dict(address="192.0.2.1"),
+ dict(address="192.0.2.3"),
+ dict(address="192.0.2.5"),
+ ],
+ ),
+ dict(
+ afi="ipv4",
+ name="DELETE-HOSTS",
+ description="The (single) last address from this group will be deleted in the tests",
+ members=[dict(address="1.2.3.4")],
+ ),
+ dict(
+ afi="ipv4",
+ name="ZONE-TEST",
+ members=[dict(address="1.2.3.4")],
+ ),
+ dict(
+ afi="ipv6",
+ name="LOCAL-v6",
+ description="This group has the hosts address lists of this machine",
+ members=[
+ dict(address="::1"),
+ dict(address="fdec:2503:89d6:59b3::1"),
+ ],
+ ),
+ ],
+ network_group=[
+ dict(
+ afi="ipv4",
+ name="RND",
+ description="This group has the Management network addresses",
+ members=[dict(address="192.0.2.0/24")],
+ ),
+ dict(
+ afi="ipv6",
+ name="UNIQUE-LOCAL-v6",
+ description="This group encompasses the ULA address space in IPv6",
+ members=[dict(address="fc00::/7")],
+ ),
+ ],
+ port_group=[
+ dict(
+ name="SSH",
+ description="This group has the ssh ports",
+ members=[dict(port="22")],
+ ),
+ ],
+ ),
+ zone=[
+ dict(
+ name="ZONE-TEST",
+ description="zone-test test description",
+ interfaces=["eth0.1234"],
+ ),
+ dict(
+ name="ZONE-IZF",
+ description="zone for izf and sources delete path testing",
+ intra_zone_filtering=dict(action="drop"),
+ # sources omitted entirely
+ ),
+ ],
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete firewall zone ZONE-IZF from zone-src",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_firewall_global_ruleset_lines_filtered_from_facts(self):
+ # Confirms render_config's pre-filter strips all three ruleset-line
+ # prefixes (IPv4 name, legacy hyphenated ipv6-name, and 1.4+
+ # space-separated "ipv6 name") before zone/global-options parsing
+ # runs, on a real 1.4.x-shaped fixture alongside existing zone data.
+ set_module_args(dict(config=dict(), state="gathered"))
+ result = self.execute_module(changed=False)
+ facts = result["gathered"]
+ self.assertNotIn("TESTRULESET-V4", str(facts))
+ self.assertNotIn("TESTRULESET-V6-LEGACY", str(facts))
+ self.assertNotIn("TESTRULESET-V6-1_4PLUS", str(facts))
diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py b/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py
new file mode 100644
index 00000000..22ab5364
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py
@@ -0,0 +1,132 @@
+# (c) 2016 Red Hat Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_firewall_global
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosFirewallRulesModule15(TestVyosModule):
+ module = vyos_firewall_global
+
+ def setUp(self):
+ super(TestVyosFirewallRulesModule15, self).setUp()
+ self.mock_get_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.get_config",
+ )
+ self.get_config = self.mock_get_config.start()
+
+ self.mock_load_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.load_config",
+ )
+ self.load_config = self.mock_load_config.start()
+
+ self.mock_get_resource_connection_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base.get_resource_connection",
+ )
+ self.get_resource_connection_config = self.mock_get_resource_connection_config.start()
+
+ self.mock_get_resource_connection_facts = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection",
+ )
+ self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start()
+
+ self.mock_execute_show_command = patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_global.firewall_global.Firewall_globalFacts.get_device_data",
+ )
+
+ self.mock_get_os_version = patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.firewall_global.firewall_global.get_os_version",
+ )
+ self.get_os_version = self.mock_get_os_version.start()
+ self.get_os_version.return_value = "1.5"
+
+ self.execute_show_command = self.mock_execute_show_command.start()
+ self.maxDiff = None
+
+ def tearDown(self):
+ super(TestVyosFirewallRulesModule15, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_get_config.stop()
+ self.mock_load_config.stop()
+ self.mock_execute_show_command.stop()
+ self.mock_get_os_version.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ def load_from_file(*args, **kwargs):
+ return load_fixture("vyos_firewall_global_config_v15.cfg")
+
+ self.execute_show_command.side_effect = load_from_file
+
+ def test_vyos_firewall_global_set_01_merged_interface_uses_member_keyword(self):
+ # On a 1.5.0 device (empty fixture, no existing zone), merging a
+ # zone with an interface should render "member interface", not the
+ # bare "interface" used on 1.4.x / 1.5-rolling. This is the sole
+ # thing this class exists to prove right now -- the version-gate
+ # added to _render_interfaces.
+ set_module_args(
+ dict(
+ config=dict(
+ zone=[
+ dict(
+ name="ZONE-15",
+ interfaces=["eth1"],
+ ),
+ ],
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set firewall zone ZONE-15 default-action 'drop'",
+ "set firewall zone ZONE-15 member interface eth1",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_firewall_global_set_02_gathered_member_interface_parsed(self):
+ # ZONE-15-EXISTING in the fixture uses 1.5.0's "member interface"
+ # syntax. Before the parse_zone fix, this misparsed into a bogus
+ # zone['member'] = "interface eth2" string field, and 'interfaces'
+ # was missing entirely. After the fix, it should land in
+ # 'interfaces' exactly like the pre-1.5.0 bare "interface" form.
+ set_module_args(dict(config=dict(), state="gathered"))
+ result = self.execute_module(changed=False)
+ zones = result["gathered"]["zone"]
+ zone = next(z for z in zones if z["name"] == "ZONE-15-EXISTING")
+ self.assertEqual(zone["interfaces"], ["eth2"])
+ self.assertEqual(zone["description"], "existing 1.5.0 zone for facts parsing test")
+ self.assertNotIn("member", zone)
+
+ def test_vyos_firewall_global_ruleset_lines_filtered_from_facts(self):
+ # Same coverage as the 1.4 version, confirmed independently on the
+ # 1.5.0 fixture/version path.
+ set_module_args(dict(config=dict(), state="gathered"))
+ result = self.execute_module(changed=False)
+ facts = result["gathered"]
+ self.assertNotIn("TESTRULESET-V4", str(facts))
+ self.assertNotIn("TESTRULESET-V6-LEGACY", str(facts))
+ self.assertNotIn("TESTRULESET-V6-1_4PLUS", str(facts))
diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_interfaces.py b/tests/unit/modules/network/vyos/test_vyos_firewall_interfaces.py
index f921c508..2b995652 100644
--- a/tests/unit/modules/network/vyos/test_vyos_firewall_interfaces.py
+++ b/tests/unit/modules/network/vyos/test_vyos_firewall_interfaces.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -437,19 +436,19 @@ class TestVyosFirewallInterfacesModule(TestVyosModule):
),
)
commands = [
- 'delete interfaces ethernet eth0 firewall in name',
- 'delete interfaces ethernet eth0 firewall local name',
- 'delete interfaces ethernet eth0 firewall out name',
- 'delete interfaces ethernet eth0 firewall local ipv6-name',
- 'delete interfaces ethernet eth2 firewall in name',
- 'delete interfaces ethernet eth2 firewall local name',
- 'delete interfaces ethernet eth2 firewall out name',
- 'delete interfaces ethernet eth2 firewall local ipv6-name',
+ "delete interfaces ethernet eth0 firewall in name",
+ "delete interfaces ethernet eth0 firewall local name",
+ "delete interfaces ethernet eth0 firewall out name",
+ "delete interfaces ethernet eth0 firewall local ipv6-name",
+ "delete interfaces ethernet eth2 firewall in name",
+ "delete interfaces ethernet eth2 firewall local name",
+ "delete interfaces ethernet eth2 firewall out name",
+ "delete interfaces ethernet eth2 firewall local ipv6-name",
"set interfaces ethernet eth0 vif 100 firewall in name 'INBOUND'",
"set interfaces ethernet eth0 vif 100 firewall local ipv6-name 'V6-LOCAL'",
"set interfaces bonding bond2 firewall local name 'LOCAL'",
"set interfaces bonding bond2 firewall local ipv6-name 'V6-LOCAL'",
"set interfaces wireguard wg4 firewall local name 'LOCAL'",
- "set interfaces wireguard wg4 firewall local ipv6-name 'V6-LOCAL'"
+ "set interfaces wireguard wg4 firewall local ipv6-name 'V6-LOCAL'",
]
self.execute_module(changed=True, commands=commands)
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 9a25198f..457fc76b 100644
--- a/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py
+++ b/tests/unit/modules/network/vyos/test_vyos_firewall_rules13.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -167,6 +166,13 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
fragment="match-frag",
disable=True,
),
+ dict(
+ number="105",
+ action="offload",
+ offload_target="test",
+ description="Rule 105 is configured by Ansible",
+ protocol="udp",
+ ),
],
),
],
@@ -187,12 +193,17 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
"set firewall name INBOUND rule 101 action 'accept'",
"set firewall name INBOUND rule 101 ipsec 'match-ipsec'",
"set firewall name INBOUND rule 101 log 'disable'",
+ "set firewall name INBOUND rule 105",
+ "set firewall name INBOUND rule 105 action 'offload'",
+ "set firewall name INBOUND rule 105 offload-target 'test'",
+ "set firewall name INBOUND rule 105 description 'Rule 105 is configured by Ansible'",
+ "set firewall name INBOUND rule 105 protocol 'udp'",
]
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4_rule_sets_rule_merged_02(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -253,7 +264,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_03(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -302,7 +313,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_04(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -327,9 +338,8 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
tcp=dict(
flags=[
dict(flag="all"),
- ]
+ ],
),
-
),
],
),
@@ -399,7 +409,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v6_rule_sets_rule_merged_02(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing ipv6 rule set
+ within existing ipv6 rule set
"""
set_module_args(
dict(
@@ -460,7 +470,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v6_rule_sets_rule_merged_03(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing ipv6 rule set
+ within existing ipv6 rule set
"""
set_module_args(
dict(
@@ -509,7 +519,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v6_rule_sets_rule_merged_04(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing ipv6 rule set
+ within existing ipv6 rule set
"""
set_module_args(
dict(
@@ -534,7 +544,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
tcp=dict(
flags=[
dict(flag="all"),
- ]
+ ],
),
),
dict(
@@ -545,8 +555,8 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
dict(flag="syn"),
dict(flag="fin", invert=True),
],
- )
- )
+ ),
+ ),
],
),
],
@@ -572,7 +582,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v6_rule_sets_rule_merged_icmp_01(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing ipv6 rule set
+ within existing ipv6 rule set
"""
set_module_args(
dict(
@@ -605,7 +615,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_icmp_01(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -639,7 +649,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_icmp_02(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -671,8 +681,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4_rule_sets_del_01(self):
- """Test if plugin correctly removes existing rule set
- """
+ """Test if plugin correctly removes existing rule set"""
set_module_args(
dict(
config=[dict(afi="ipv4", rule_sets=[dict(name="V4-INGRESS")])],
@@ -683,33 +692,32 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4v6_rule_sets_del_02(self):
- """Test if plugin correctly removes existing rule sets, both ipv4 and ipv6
- """
+ """Test if plugin correctly removes existing rule sets, both ipv4 and ipv6"""
set_module_args(
dict(
config=[
dict(afi="ipv4", rule_sets=[dict(name="V4-INGRESS")]),
dict(afi="ipv6", rule_sets=[dict(name="V6-INGRESS")]),
+ dict(afi="ipv4", rule_sets=[dict(name="V4-OFFLOAD")]),
],
state="deleted",
),
)
commands = [
+ "delete firewall name V4-OFFLOAD",
"delete firewall name V4-INGRESS",
"delete firewall ipv6-name V6-INGRESS",
]
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4v6_rule_sets_del_03(self):
- """Test if plugin correctly removes existing AFIs, both ipv4 and ipv6
- """
+ """Test if plugin correctly removes existing AFIs, both ipv4 and ipv6"""
set_module_args(dict(config=[], state="deleted"))
commands = ["delete firewall name", "delete firewall ipv6-name"]
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4v6_rule_sets_del_04(self):
- """Test if plugin has no effect on non-existent rule sets
- """
+ """Test if plugin has no effect on non-existent rule sets"""
set_module_args(
dict(
config=[
@@ -723,7 +731,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v4v6_rule_sets_rule_rep_01(self):
"""Test if plugin correctly replaces a particular rule set(s)
- without affecting the others
+ without affecting the others
"""
set_module_args(
dict(
@@ -803,7 +811,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v4v6_rule_sets_rule_rep_02(self):
"""Test if plugin correctly replaces a particular rule(s) and rule set attribute(s)
- without affecting the others
+ without affecting the others
"""
set_module_args(
dict(
@@ -867,8 +875,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4v6_rule_sets_rule_rep_idem_01(self):
- """Test if plugin correctly has no effect if there is no change in the configuration
- """
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
set_module_args(
dict(
config=[
@@ -890,7 +897,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
fragment="match-frag",
disable=True,
log="enable",
- )
+ ),
],
),
dict(
@@ -925,8 +932,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
self.execute_module(changed=False, commands=[])
def test_vyos_firewall_v4v6_rule_sets_rule_rep_idem_02(self):
- """Test if plugin correctly has no effect if there is no change in the configuration
- """
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
set_module_args(
dict(
config=[
@@ -947,7 +953,33 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
protocol="icmp",
fragment="match-frag",
disable=True,
- log="enable"
+ log="enable",
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ state="replaced",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_firewall_v4v6_rule_sets_rule_rep_idem_03(self):
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ afi="ipv4",
+ rule_sets=[
+ dict(
+ name="V4-OFFLOAD",
+ rules=[
+ dict(
+ number="1",
+ action="offload",
+ offload_target="test",
),
],
),
@@ -960,8 +992,63 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
self.execute_module(changed=False, commands=[])
def test_vyos_firewall_v4v6_rule_sets_rule_mer_idem_01(self):
- """Test if plugin correctly has no effect if there is no change in the configuration
- """
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
+ 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",
+ ipsec="match-ipsec",
+ protocol="icmp",
+ fragment="match-frag",
+ disable=True,
+ ),
+ ],
+ ),
+ dict(
+ name="EGRESS",
+ default_action="reject",
+ ),
+ ],
+ ),
+ dict(
+ afi="ipv6",
+ rule_sets=[
+ dict(
+ name="V6-INGRESS",
+ default_action="accept",
+ ),
+ dict(
+ name="EGRESS",
+ default_action="reject",
+ rules=[
+ dict(
+ icmp=dict(type_name="echo-request"),
+ number=20,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_firewall_v4v6_rule_sets_rule_mer_idem_02(self):
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
set_module_args(
dict(
config=[
@@ -982,13 +1069,23 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
protocol="icmp",
fragment="match-frag",
disable=True,
- )
+ ),
],
),
dict(
name="EGRESS",
default_action="reject",
),
+ dict(
+ name="V4-OFFLOAD",
+ rules=[
+ dict(
+ number="1",
+ action="offload",
+ offload_target="test",
+ ),
+ ],
+ ),
],
),
dict(
@@ -1017,8 +1114,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
self.execute_module(changed=False, commands=[])
def test_vyos_firewall_v4v6_rule_sets_rule_ovr_01(self):
- """Test if plugin correctly resets the entire rule set if there is a change in the configuration
- """
+ """Test if plugin correctly resets the entire rule set if there is a change in the configuration"""
set_module_args(
dict(
config=[
@@ -1095,6 +1191,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
"delete firewall name V4-INGRESS",
"delete firewall name EGRESS",
"delete firewall name MULTIPLE-RULE",
+ "delete firewall name V4-OFFLOAD",
"set firewall name MULTIPLE-RULE default-action 'drop'",
"set firewall name MULTIPLE-RULE rule 1",
"set firewall name MULTIPLE-RULE rule 1 action 'accept'",
@@ -1126,7 +1223,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_v4v6_rule_sets_rule_ovr_02(self):
"""Test if plugin correctly resets the entire rule set
- while removing the absent ones if there is a change in the configuration
+ while removing the absent ones if there is a change in the configuration
"""
set_module_args(
dict(
@@ -1176,6 +1273,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
"delete firewall name V4-INGRESS",
"delete firewall name EGRESS",
"delete firewall name MULTIPLE-RULE",
+ "delete firewall name V4-OFFLOAD",
"set firewall name V4-INGRESS rule 101",
"set firewall name V4-INGRESS description 'This is IPv4 INGRESS rule set'",
"set firewall name V4-INGRESS default-action 'accept'",
@@ -1186,13 +1284,12 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
"set firewall ipv6-name EGRESS default-action 'reject'",
"set firewall ipv6-name EGRESS rule 20",
"set firewall ipv6-name EGRESS rule 20 protocol 'udp'",
- "set firewall ipv6-name EGRESS rule 20 action 'accept'"
+ "set firewall ipv6-name EGRESS rule 20 action 'accept'",
]
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4v6_rule_sets_rule_ovr_idem_01(self):
- """Test if plugin correctly has no effect if there is no change in the configuration
- """
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
set_module_args(
dict(
config=[
@@ -1214,7 +1311,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
fragment="match-frag",
disable=True,
log="enable",
- )
+ ),
],
),
dict(
@@ -1237,6 +1334,16 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
),
],
),
+ dict(
+ name="V4-OFFLOAD",
+ rules=[
+ dict(
+ number="1",
+ action="offload",
+ offload_target="test",
+ ),
+ ],
+ ),
],
),
dict(
@@ -1265,8 +1372,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
self.execute_module(changed=False, commands=[])
def test_vyos_firewall_v6_rule_sets_rule_merged_01_version(self):
- """Test if plugin correctly adds ipv6 rule set with rules
- """
+ """Test if plugin correctly adds ipv6 rule set with rules"""
self.get_os_version.return_value = "1.3"
set_module_args(
dict(
@@ -1321,13 +1427,12 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
"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',
+ "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):
- """Test if plugin correctly adds rule set with a jump action
- """
+ """Test if plugin correctly adds rule set with a jump action"""
set_module_args(
dict(
config=[
@@ -1348,7 +1453,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
protocol="icmp",
icmp=dict(type_name="echo-request"),
jump_target="PROTECT-RE",
- packet_length_exclude=[dict(length=100), dict(length=200)]
+ packet_length_exclude=[dict(length=100), dict(length=200)],
),
dict(
number="102",
@@ -1360,10 +1465,10 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
],
),
],
- )
+ ),
],
state="merged",
- )
+ ),
)
commands = [
"set firewall ipv6-name INBOUND default-action 'accept'",
@@ -1382,7 +1487,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
"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',
+ "set firewall ipv6-name INBOUND rule 102 icmpv6 type 7",
]
self.execute_module(changed=True, commands=commands)
@@ -1409,10 +1514,10 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
],
),
],
- )
+ ),
],
state="merged",
- )
+ ),
)
commands = [
"set firewall ipv6-name INBOUND default-action 'accept'",
@@ -1428,7 +1533,7 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
def test_vyos_firewall_log_replace_01(self):
"""Test that stanza is correctly replaced
- without touching the other stanzas
+ without touching the other stanzas
"""
set_module_args(
dict(
@@ -1453,10 +1558,10 @@ class TestVyosFirewallRulesModule13(TestVyosModule):
],
),
],
- )
+ ),
],
state="replaced",
- )
+ ),
)
commands = [
"delete firewall name V4-INGRESS rule 101",
@@ -1470,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_firewall_rules14.py b/tests/unit/modules/network/vyos/test_vyos_firewall_rules14.py
index 64884282..67ed216e 100644
--- a/tests/unit/modules/network/vyos/test_vyos_firewall_rules14.py
+++ b/tests/unit/modules/network/vyos/test_vyos_firewall_rules14.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -35,35 +34,35 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
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"
+ "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"
+ "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"
+ "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"
+ "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"
+ "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"
+ "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"
+ "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"
@@ -164,15 +163,15 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
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)]
+ packet_length=[dict(length=22)],
),
],
),
],
- )
+ ),
],
state="merged",
- )
+ ),
)
commands = [
"set firewall ipv6 name INBOUND default-action 'accept'",
@@ -191,7 +190,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_packet_length_replace_01(self):
"""Test that stanza is correctly replaced
- without touching the other stanzas
+ without touching the other stanzas
"""
set_module_args(
dict(
@@ -210,15 +209,15 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
action="accept",
description="Rule 101 is configured by Ansible",
packet_length_exclude=[dict(length=100), dict(length=200)],
- packet_length=[dict(length=22)]
+ packet_length=[dict(length=22)],
),
],
),
],
- )
+ ),
],
state="replaced",
- )
+ ),
)
commands = [
"delete firewall ipv4 name V4-INGRESS rule 101",
@@ -252,15 +251,15 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
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)]
+ packet_length=[dict(length=22)],
),
],
),
],
- )
+ ),
],
state="merged",
- )
+ ),
)
commands = [
"set firewall ipv6 input filter default-action 'accept'",
@@ -298,13 +297,20 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
inbound_interface=dict(name="eth0"),
outbound_interface=dict(group="eth1"),
),
+ dict(
+ number="105",
+ action="offload",
+ offload_target="test",
+ description="Rule 105 is configured by Ansible",
+ protocol="udp",
+ ),
],
),
],
- )
+ ),
],
state="merged",
- )
+ ),
)
commands = [
"set firewall ipv6 name V6-INGRESS description 'This is IPv6 INBOUND rule set with a jump action'",
@@ -314,13 +320,18 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
"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'",
+ "set firewall ipv6 name V6-INGRESS rule 105",
+ "set firewall ipv6 name V6-INGRESS rule 105 action 'offload'",
+ "set firewall ipv6 name V6-INGRESS rule 105 offload-target 'test'",
+ "set firewall ipv6 name V6-INGRESS rule 105 description 'Rule 105 is configured by Ansible'",
+ "set firewall ipv6 name V6-INGRESS rule 105 protocol 'udp'",
]
self.maxDiff = None
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_interface_replace_02(self):
"""Test that new stanza is correctly replaced
- without touching the other stanzas
+ without touching the other stanzas
"""
set_module_args(
dict(
@@ -341,17 +352,17 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
],
),
],
- )
+ ),
],
state="replaced",
- )
+ ),
)
commands = [
"delete firewall ipv4 name IF-TEST rule 10",
"set firewall ipv4 name IF-TEST rule 10",
"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',
+ "set firewall ipv4 name IF-TEST rule 10 inbound-interface name eth1",
"set firewall ipv4 name IF-TEST rule 10 action 'accept'",
]
self.maxDiff = None
@@ -405,7 +416,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_02(self):
"""Test that a rule set is correctly applied
- including variant attributes such as state
+ including variant attributes such as state
"""
set_module_args(
dict(
@@ -466,7 +477,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_03(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -515,7 +526,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_04(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -540,9 +551,8 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
tcp=dict(
flags=[
dict(flag="all"),
- ]
+ ],
),
-
),
],
),
@@ -612,7 +622,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v6_rule_sets_rule_merged_02(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing ipv6 rule set
+ within existing ipv6 rule set
"""
set_module_args(
dict(
@@ -673,7 +683,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v6_rule_sets_rule_merged_03(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing ipv6 rule set
+ within existing ipv6 rule set
"""
set_module_args(
dict(
@@ -722,7 +732,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v6_rule_sets_rule_merged_04(self):
"""Test that the plugin correctly applies configuration
- within exsiting rule set
+ within exsiting rule set
"""
set_module_args(
dict(
@@ -747,7 +757,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
tcp=dict(
flags=[
dict(flag="all"),
- ]
+ ],
),
),
dict(
@@ -758,8 +768,8 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
dict(flag="syn"),
dict(flag="fin", invert=True),
],
- )
- )
+ ),
+ ),
],
),
],
@@ -787,7 +797,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_change_state_01(self):
"""Test that a rule set is replaced applied without touching the other stanzas
- in particular variant attributes such as state
+ in particular variant attributes such as state
"""
set_module_args(
dict(
@@ -826,7 +836,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v6_rule_sets_rule_merged_icmp_01(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing ipv6 rule set
+ within existing ipv6 rule set
"""
set_module_args(
dict(
@@ -859,7 +869,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_icmp_01(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -893,7 +903,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4_rule_sets_rule_merged_icmp_02(self):
"""Test if plugin correctly adds new rules with variant attributes
- within existing rule set
+ within existing rule set
"""
set_module_args(
dict(
@@ -925,8 +935,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4_rule_sets_del_01(self):
- """Test if plugin correctly removes existing rule set
- """
+ """Test if plugin correctly removes existing rule set"""
set_module_args(
dict(
config=[dict(afi="ipv4", rule_sets=[dict(name="V4-INGRESS")])],
@@ -937,8 +946,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4v6_rule_sets_del_02(self):
- """Test if plugin correctly removes existing rule sets, both ipv4 and ipv6
- """
+ """Test if plugin correctly removes existing rule sets, both ipv4 and ipv6"""
set_module_args(
dict(
config=[
@@ -956,15 +964,14 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4v6_rule_sets_del_03(self):
"""Test that the plugin correctly deprovisions
- variant configuration
+ variant configuration
"""
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_v4v6_rule_sets_del_04(self):
- """Test if plugin has no effect on non-existent rule sets
- """
+ """Test if plugin has no effect on non-existent rule sets"""
set_module_args(
dict(
config=[
@@ -976,9 +983,20 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
)
self.execute_module(changed=False, commands=[])
+ def test_vyos_firewall_v4_rule_sets_del_05(self):
+ """Test if plugin correctly removes existing rule set"""
+ set_module_args(
+ dict(
+ config=[dict(afi="ipv4", rule_sets=[dict(name="V4-OFFLOAD")])],
+ state="deleted",
+ ),
+ )
+ commands = ["delete firewall ipv4 name V4-OFFLOAD"]
+ self.execute_module(changed=True, commands=commands)
+
def test_vyos_firewall_v4v6_rule_sets_rule_rep_01(self):
"""Test if plugin correctly replaces a particular rule set(s)
- without affecting the others
+ without affecting the others
"""
set_module_args(
dict(
@@ -1058,7 +1076,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4v6_rule_sets_rule_rep_02(self):
"""Test if plugin correctly replaces a particular rule(s) and rule set attribute(s)
- without affecting the others
+ without affecting the others
"""
set_module_args(
dict(
@@ -1122,8 +1140,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4v6_rule_sets_rule_rep_idem_01(self):
- """Test if plugin correctly has no effect if there is no change in the configuration
- """
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
set_module_args(
dict(
config=[
@@ -1144,7 +1161,17 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
protocol="icmp",
disable=True,
log="enable",
- )
+ ),
+ ],
+ ),
+ dict(
+ name="V4-OFFLOAD",
+ rules=[
+ dict(
+ number="105",
+ action="offload",
+ offload_target="test",
+ ),
],
),
dict(
@@ -1178,7 +1205,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
inbound_interface=dict(name="eth0"),
outbound_interface=dict(group="the-ethers"),
disable=True,
- )
+ ),
],
),
dict(
@@ -1233,8 +1260,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
self.execute_module(changed=False, commands=[])
def test_vyos_firewall_v4v6_rule_sets_rule_rep_idem_02(self):
- """Test if plugin correctly has no effect if there is no change in the configuration
- """
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
set_module_args(
dict(
config=[
@@ -1255,7 +1281,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
protocol="icmp",
disable=True,
log="enable",
- )
+ ),
],
),
],
@@ -1267,8 +1293,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
self.execute_module(changed=False, commands=[])
def test_vyos_firewall_v4v6_rule_sets_rule_mer_idem_01(self):
- """Test if plugin correctly has no effect if there is no change in the configuration
- """
+ """Test if plugin correctly has no effect if there is no change in the configuration"""
set_module_args(
dict(
config=[
@@ -1289,7 +1314,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
protocol="icmp",
disable=True,
log="enable",
- )
+ ),
],
),
dict(
@@ -1323,7 +1348,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
inbound_interface=dict(name="eth0"),
outbound_interface=dict(group="the-ethers"),
disable=True,
- )
+ ),
],
),
dict(
@@ -1378,8 +1403,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
self.execute_module(changed=False, commands=[])
def test_vyos_firewall_v4v6_rule_sets_rule_ovr_01(self):
- """Test if plugin correctly resets the entire rule set if there is a change in the configuration
- """
+ """Test if plugin correctly resets the entire rule set if there is a change in the configuration"""
set_module_args(
dict(
config=[
@@ -1461,6 +1485,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
"delete firewall ipv6 input filter",
"delete firewall ipv6 output filter",
"delete firewall ipv4 name IF-TEST",
+ "delete firewall ipv4 name V4-OFFLOAD",
"set firewall ipv4 name MULTIPLE-RULE default-action 'drop'",
"set firewall ipv4 name MULTIPLE-RULE rule 1",
"set firewall ipv4 name MULTIPLE-RULE rule 1 action 'accept'",
@@ -1492,7 +1517,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_v4v6_rule_sets_rule_ovr_02(self):
"""Test that the plugin correctly resets the entire
- rule sets configuration if changes are detected
+ rule sets configuration if changes are detected
"""
set_module_args(
dict(
@@ -1547,6 +1572,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
"delete firewall ipv6 output filter",
"delete firewall ipv4 name IF-TEST",
"delete firewall ipv4 name MULTIPLE-RULE",
+ "delete firewall ipv4 name V4-OFFLOAD",
"set firewall ipv4 name V4-INGRESS rule 101",
"set firewall ipv4 name V4-INGRESS default-log",
"set firewall ipv4 name V4-INGRESS description 'This is IPv4 INGRESS rule set'",
@@ -1557,13 +1583,13 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
"set firewall ipv6 name EGRESS default-action 'reject'",
"set firewall ipv6 name EGRESS rule 20",
"set firewall ipv6 name EGRESS rule 20 protocol 'udp'",
- "set firewall ipv6 name EGRESS rule 20 action 'accept'"
+ "set firewall ipv6 name EGRESS rule 20 action 'accept'",
]
self.execute_module(changed=True, commands=commands)
def test_vyos_firewall_v4v6_rule_sets_rule_ovr_idem_01(self):
"""Test that the plugin is idempotent in overridden state
- if there are no changes to the rule sets
+ if there are no changes to the rule sets
"""
set_module_args(
dict(
@@ -1585,7 +1611,17 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
protocol="icmp",
disable=True,
log="enable",
- )
+ ),
+ ],
+ ),
+ dict(
+ name="V4-OFFLOAD",
+ rules=[
+ dict(
+ number="105",
+ action="offload",
+ offload_target="test",
+ ),
],
),
dict(
@@ -1619,7 +1655,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
inbound_interface=dict(name="eth0"),
outbound_interface=dict(group="the-ethers"),
disable=True,
- )
+ ),
],
),
dict(
@@ -1690,8 +1726,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
self.execute_module(changed=False, commands=[])
def test_vyos_firewall_v6_rule_sets_rule_merged_01_version(self):
- """Test if plugin correctly adds ipv6 rule set with rules
- """
+ """Test if plugin correctly adds ipv6 rule set with rules"""
set_module_args(
dict(
config=[
@@ -1745,13 +1780,12 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
"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',
+ "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):
- """Test if plugin correctly adds rule set with a jump action
- """
+ """Test if plugin correctly adds rule set with a jump action"""
set_module_args(
dict(
config=[
@@ -1772,7 +1806,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
protocol="icmp",
icmp=dict(type_name="echo-request"),
jump_target="PROTECT-RE",
- packet_length_exclude=[dict(length=100), dict(length=200)]
+ packet_length_exclude=[dict(length=100), dict(length=200)],
),
dict(
number="102",
@@ -1784,10 +1818,10 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
],
),
],
- )
+ ),
],
state="merged",
- )
+ ),
)
commands = [
"set firewall ipv6 name INBOUND default-action 'accept'",
@@ -1806,7 +1840,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
"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',
+ "set firewall ipv6 name INBOUND rule 102 icmpv6 type 7",
]
self.execute_module(changed=True, commands=commands)
@@ -1833,10 +1867,10 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
],
),
],
- )
+ ),
],
state="merged",
- )
+ ),
)
commands = [
"set firewall ipv6 name INBOUND default-action 'accept'",
@@ -1852,7 +1886,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
def test_vyos_firewall_log_replace_01(self):
"""Test that stanza is correctly replaced
- without touching the other stanzas
+ without touching the other stanzas
"""
set_module_args(
dict(
@@ -1877,10 +1911,10 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
],
),
],
- )
+ ),
],
state="replaced",
- )
+ ),
)
commands = [
"delete firewall ipv4 name V4-INGRESS rule 101",
@@ -1894,3 +1928,65 @@ class TestVyosFirewallRulesModule14(TestVyosModule):
]
self.maxDiff = None
self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_firewall_default_action_merged_01(self):
+ """Test that every default-action value is accepted and rendered"""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ afi="ipv4",
+ rule_sets=[
+ dict(
+ name="V4-DROP",
+ description="Rule set configured with default_action drop",
+ default_action="drop",
+ ),
+ dict(
+ name="V4-JUMP",
+ description="Rule set configured with default_action jump",
+ default_action="jump",
+ default_jump_target="V4-ACCEPT",
+ ),
+ dict(
+ name="V4-REJECT",
+ description="Rule set configured with default_action reject",
+ default_action="reject",
+ ),
+ dict(
+ name="V4-RETURN",
+ description="Rule set configured with default_action return",
+ default_action="return",
+ ),
+ dict(
+ name="V4-ACCEPT",
+ description="Rule set configured with default_action accept",
+ default_action="accept",
+ ),
+ dict(
+ name="V4-CONTINUE",
+ description="Rule set configured with default_action continue",
+ default_action="continue",
+ ),
+ ],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set firewall ipv4 name V4-DROP default-action 'drop'",
+ "set firewall ipv4 name V4-DROP description 'Rule set configured with default_action drop'",
+ "set firewall ipv4 name V4-JUMP default-action 'jump'",
+ "set firewall ipv4 name V4-JUMP default-jump-target 'V4-ACCEPT'",
+ "set firewall ipv4 name V4-JUMP description 'Rule set configured with default_action jump'",
+ "set firewall ipv4 name V4-REJECT default-action 'reject'",
+ "set firewall ipv4 name V4-REJECT description 'Rule set configured with default_action reject'",
+ "set firewall ipv4 name V4-RETURN default-action 'return'",
+ "set firewall ipv4 name V4-RETURN description 'Rule set configured with default_action return'",
+ "set firewall ipv4 name V4-ACCEPT default-action 'accept'",
+ "set firewall ipv4 name V4-ACCEPT description 'Rule set configured with default_action accept'",
+ "set firewall ipv4 name V4-CONTINUE default-action 'continue'",
+ "set firewall ipv4 name V4-CONTINUE description 'Rule set configured with default_action continue'",
+ ]
+ self.execute_module(changed=True, commands=commands)
diff --git a/tests/unit/modules/network/vyos/test_vyos_ha.py b/tests/unit/modules/network/vyos/test_vyos_ha.py
new file mode 100644
index 00000000..54833a36
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_ha.py
@@ -0,0 +1,1405 @@
+# (c) 2021 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_ha
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosHaModule(TestVyosModule):
+ module = vyos_ha
+
+ def setUp(self):
+ super(TestVyosHaModule, 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_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.ha.ha.HaFacts.get_config",
+ )
+
+ 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.ha.ha.get_os_version",
+ )
+ self.get_os_version = self.mock_get_os_version.start()
+ self.get_os_version.return_value = "1.5"
+ self.maxDiff = None
+
+ def tearDown(self):
+ super(TestVyosHaModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.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_ha_config.cfg"
+
+ def load_from_file(*args, **kwargs):
+ output = load_fixture(filename)
+ return output
+
+ self.execute_show_command.side_effect = load_from_file
+
+ def test_vrrp_merged_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ disable=True,
+ virtual_servers=[
+ dict(
+ address="10.10.10.1",
+ algorithm="round-robin",
+ delay_loop=60,
+ forward_method="direct",
+ fwmark=10,
+ name="s1",
+ persistence_timeout="30",
+ protocol="tcp",
+ real_server=[
+ dict(
+ address="10.10.50.1",
+ port=443,
+ health_check_script="/var/tmp/script.sh",
+ ),
+ ],
+ ),
+ dict(
+ address="10.10.10.2",
+ name="s2",
+ port=81,
+ real_server=[
+ dict(
+ address="real1",
+ port=8081,
+ connection_timeout=5,
+ ),
+ dict(
+ address="real2",
+ port=8080,
+ ),
+ ],
+ ),
+ ],
+ vrrp=dict(
+ global_parameters=dict(
+ garp=dict(
+ master_refresh=100,
+ ),
+ version="3",
+ ),
+ groups=[
+ dict(
+ authentication=dict(
+ password="testpass",
+ type="plaintext-password",
+ ),
+ address=[
+ "1.1.1.1",
+ "3.3.3.3",
+ "5.5.5.5 interface eth2",
+ ],
+ advertise_interval=10,
+ description="Group_1",
+ disable=True,
+ excluded_address=[
+ "192.168.1.8",
+ "192.168.1.7 interface eth3",
+ ],
+ garp=dict(
+ interval=20,
+ master_delay=5,
+ master_refresh_repeat=100,
+ master_repeat=3,
+ ),
+ health_check=dict(
+ failure_count=3,
+ interval=10,
+ ping="192.168.1.5",
+ script="script.sh",
+ ),
+ hello_source_address="192.168.1.2",
+ interface="eth2",
+ name="g1",
+ no_preempt=True,
+ peer_address="192.168.1.3",
+ priority=100,
+ rfc3768_compatibility=True,
+ track=dict(
+ exclude_vrrp_interface=True,
+ interface=["eth1"],
+ ),
+ transition_script=dict(
+ backup="/var/tmp/script.sh",
+ master="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ vrid=20,
+ ),
+ ],
+ snmp="enabled",
+ sync_groups=[
+ dict(
+ health_check=dict(
+ failure_count=3,
+ interval=10,
+ ),
+ member=["g1"],
+ name="sg1",
+ transition_script=dict(
+ backup="/var/tmp/script.sh",
+ master="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ ),
+ ],
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vrrp_merged(self):
+ set_module_args(
+ dict(
+ config=dict(
+ disable=True,
+ virtual_servers=[
+ dict(
+ name="s1",
+ address="10.10.10.5",
+ algorithm="round-robin",
+ real_server=[
+ dict(
+ address="10.10.50.2",
+ port=8443,
+ ),
+ ],
+ ),
+ dict(
+ name="s2",
+ address="10.10.10.2",
+ persistence_timeout=30,
+ port=81,
+ protocol="tcp",
+ ),
+ dict(
+ name="s3",
+ address="10.10.10.3",
+ port=88,
+ protocol="udp",
+ ),
+ ],
+ vrrp=dict(
+ snmp="disabled",
+ global_parameters=dict(
+ startup_delay=35,
+ garp=dict(
+ master_repeat=6,
+ ),
+ ),
+ groups=[
+ dict(
+ name="g1",
+ peer_address="192.168.1.3",
+ priority=100,
+ disable=False,
+ no_preempt=False,
+ ),
+ dict(
+ name="g2",
+ address=[
+ "192.168.3.3",
+ "192.168.4.4 interface eth3",
+ ],
+ preempt_delay=15,
+ ),
+ ],
+ sync_groups=[
+ dict(
+ name="sg1",
+ health_check=dict(
+ failure_count=5,
+ ),
+ ),
+ ],
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "delete high-availability vrrp group g1 no-preempt",
+ "delete high-availability vrrp group g1 disable",
+ "delete high-availability vrrp group g1 rfc3768-compatibility",
+ "set high-availability vrrp group g2 address 192.168.3.3",
+ "set high-availability vrrp group g2 address 192.168.4.4 interface eth3",
+ "set high-availability vrrp group g2 preempt-delay 15",
+ "delete high-availability vrrp snmp",
+ "set high-availability virtual-server s1 address 10.10.10.5",
+ "set high-availability virtual-server s1 real-server 10.10.50.2 port 8443",
+ "set high-availability virtual-server s2 persistence-timeout 30",
+ "set high-availability virtual-server s2 protocol tcp",
+ "set high-availability virtual-server s3 address 10.10.10.3",
+ "set high-availability virtual-server s3 port 88",
+ "set high-availability virtual-server s3 protocol udp",
+ "set high-availability vrrp global-parameters garp master-repeat 6",
+ "set high-availability vrrp global-parameters startup-delay 35",
+ "set high-availability vrrp sync-group sg1 health-check failure-count 5",
+ ]
+
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrrp_replaced(self):
+ set_module_args(
+ dict(
+ config=dict(
+ disable=False,
+ virtual_servers=[
+ dict(
+ name="s1",
+ address="10.10.10.5",
+ algorithm="round-robin",
+ real_server=[
+ dict(
+ address="10.10.50.2",
+ port=8443,
+ ),
+ ],
+ ),
+ dict(
+ name="s2",
+ address="10.10.10.2",
+ persistence_timeout=30,
+ port=81,
+ protocol="tcp",
+ ),
+ dict(
+ name="s3",
+ address="10.10.10.3",
+ port=88,
+ protocol="udp",
+ ),
+ ],
+ vrrp=dict(
+ snmp="disabled",
+ global_parameters=dict(
+ startup_delay=32,
+ garp=dict(
+ master_repeat=6,
+ ),
+ ),
+ groups=[
+ dict(
+ name="g1",
+ peer_address="192.168.1.3",
+ priority=100,
+ disable=False,
+ no_preempt=False,
+ vrid=20,
+ ),
+ dict(
+ name="g2",
+ address=[
+ "192.168.3.3",
+ "192.168.4.4 interface eth3",
+ ],
+ ),
+ ],
+ sync_groups=[
+ dict(
+ name="sg1",
+ health_check=dict(
+ failure_count=5,
+ ),
+ ),
+ ],
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete high-availability vrrp global-parameters garp master-repeat 5",
+ "delete high-availability vrrp global-parameters startup-delay 30",
+ "delete high-availability vrrp group g1 disable",
+ "delete high-availability vrrp group g1 no-preempt",
+ "delete high-availability vrrp group g1 rfc3768-compatibility",
+ "delete high-availability vrrp sync-group sg1 health-check failure-count 3",
+ "set high-availability vrrp group g2 address 192.168.3.3",
+ "set high-availability vrrp group g2 address 192.168.4.4 interface eth3",
+ "delete high-availability disable",
+ "delete high-availability vrrp snmp",
+ "set high-availability virtual-server s1 address 10.10.10.5",
+ "set high-availability virtual-server s1 real-server 10.10.50.2 port 8443",
+ "set high-availability virtual-server s2 persistence-timeout 30",
+ "set high-availability virtual-server s2 protocol tcp",
+ "set high-availability virtual-server s3 address 10.10.10.3",
+ "set high-availability virtual-server s3 port 88",
+ "set high-availability virtual-server s3 protocol udp",
+ "set high-availability vrrp global-parameters garp master-repeat 6",
+ "set high-availability vrrp global-parameters startup-delay 32",
+ "set high-availability vrrp sync-group sg1 health-check failure-count 5",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrrp_replaced_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ disable=True,
+ virtual_servers=[
+ dict(
+ address="10.10.10.1",
+ algorithm="round-robin",
+ delay_loop=60,
+ forward_method="direct",
+ fwmark=10,
+ name="s1",
+ persistence_timeout="30",
+ protocol="tcp",
+ real_server=[
+ dict(
+ address="10.10.50.1",
+ port=443,
+ health_check_script="/var/tmp/script.sh",
+ ),
+ ],
+ ),
+ dict(
+ address="10.10.10.2",
+ name="s2",
+ port=81,
+ real_server=[
+ dict(
+ address="real1",
+ port=8081,
+ connection_timeout=5,
+ ),
+ dict(
+ address="real2",
+ port=8080,
+ ),
+ ],
+ ),
+ ],
+ vrrp=dict(
+ global_parameters=dict(
+ garp=dict(
+ interval=30,
+ master_delay=10,
+ master_refresh=100,
+ master_refresh_repeat=200,
+ master_repeat=5,
+ ),
+ version="3",
+ startup_delay=30,
+ ),
+ groups=[
+ dict(
+ authentication=dict(
+ password="testpass",
+ type="plaintext-password",
+ ),
+ address=[
+ "1.1.1.1",
+ "3.3.3.3",
+ "5.5.5.5 interface eth2",
+ ],
+ advertise_interval=10,
+ description="Group_1",
+ disable=True,
+ excluded_address=[
+ "192.168.1.8",
+ "192.168.1.7 interface eth3",
+ ],
+ garp=dict(
+ interval=20,
+ master_delay=5,
+ master_refresh=50,
+ master_refresh_repeat=100,
+ master_repeat=3,
+ ),
+ health_check=dict(
+ failure_count=3,
+ interval=10,
+ ping="192.168.1.5",
+ script="script.sh",
+ ),
+ hello_source_address="192.168.1.2",
+ interface="eth2",
+ name="g1",
+ no_preempt=True,
+ peer_address="192.168.1.3",
+ priority=100,
+ rfc3768_compatibility=True,
+ track=dict(
+ exclude_vrrp_interface=True,
+ interface=["eth1"],
+ ),
+ transition_script=dict(
+ backup="/var/tmp/script.sh",
+ fault="/var/tmp/script.sh",
+ master="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ vrid=20,
+ ),
+ ],
+ snmp="enabled",
+ sync_groups=[
+ dict(
+ health_check=dict(
+ failure_count=3,
+ interval=10,
+ ping="192.168.1.1",
+ script="/var/tmp/script.sh",
+ ),
+ member=["g1"],
+ name="sg1",
+ transition_script=dict(
+ backup="/var/tmp/script.sh",
+ fault="/var/tmp/script.sh",
+ master="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ ),
+ ],
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vrrp_overridden(self):
+ set_module_args(
+ dict(
+ config=dict(
+ disable=False,
+ vrrp=dict(
+ snmp="disabled",
+ global_parameters=dict(
+ startup_delay=32,
+ version=3,
+ garp=dict(
+ interval=30,
+ master_delay=11,
+ master_refresh=100,
+ master_refresh_repeat=200,
+ master_repeat=6,
+ ),
+ ),
+ groups=[
+ dict(
+ name="g1",
+ description="Group_1",
+ interface="eth2",
+ address="1.1.1.1",
+ disable=True,
+ no_preempt=True,
+ rfc3768_compatibility=True,
+ vrid=20,
+ peer_address="192.168.1.3",
+ advertise_interval=10,
+ priority=100,
+ garp=dict(
+ interval=20,
+ master_delay=5,
+ master_refresh=50,
+ master_refresh_repeat=100,
+ master_repeat=3,
+ ),
+ authentication=dict(
+ type="plaintext-password",
+ password="testpass",
+ ),
+ transition_script=dict(
+ master="/var/tmp/script.sh",
+ backup="/var/tmp/script.sh",
+ fault="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ track=dict(
+ exclude_vrrp_interface=True,
+ ),
+ ),
+ dict(
+ name="g2",
+ address=[
+ "192.168.3.3",
+ "192.168.4.4 interface eth3",
+ ],
+ ),
+ ],
+ sync_groups=[
+ dict(
+ name="sg1",
+ member=["g1", "g2"],
+ transition_script=dict(
+ master="/var/tmp/script.sh",
+ backup="/var/tmp/script.sh",
+ fault="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ health_check=dict(
+ failure_count=4,
+ interval=10,
+ ping="192.168.1.100",
+ script="/var/tmp/script.sh",
+ ),
+ ),
+ ],
+ ),
+ virtual_servers=[
+ dict(
+ name="s1",
+ address="10.10.10.15",
+ algorithm="round-robin",
+ delay_loop=60,
+ fwmark=12,
+ forward_method="direct",
+ persistence_timeout=30,
+ protocol="tcp",
+ real_server=[
+ dict(
+ address="10.10.10.1",
+ connection_timeout=61,
+ port=443,
+ ),
+ ],
+ ),
+ ],
+ ),
+ state="overridden",
+ ),
+ )
+ commands = [
+ "delete high-availability disable",
+ "delete high-availability virtual-server s1",
+ "delete high-availability vrrp global-parameters garp",
+ "delete high-availability vrrp group g1",
+ "delete high-availability vrrp sync-group sg1",
+ "set high-availability virtual-server s1 address 10.10.10.15",
+ "set high-availability virtual-server s1 algorithm round-robin",
+ "set high-availability virtual-server s1 delay-loop 60",
+ "set high-availability virtual-server s1 forward-method direct",
+ "set high-availability virtual-server s1 fwmark 12",
+ "set high-availability virtual-server s1 persistence-timeout 30",
+ "set high-availability virtual-server s1 protocol tcp",
+ "set high-availability virtual-server s1 real-server 10.10.10.1 connection-timeout 61",
+ "set high-availability virtual-server s1 real-server 10.10.10.1 port 443",
+ "set high-availability vrrp global-parameters garp interval 30",
+ "set high-availability vrrp global-parameters garp master-delay 11",
+ "set high-availability vrrp global-parameters garp master-refresh 100",
+ "set high-availability vrrp global-parameters garp master-refresh-repeat 200",
+ "set high-availability vrrp global-parameters garp master-repeat 6",
+ "set high-availability vrrp global-parameters startup-delay 32",
+ "set high-availability vrrp group g1 address 1.1.1.1",
+ "set high-availability vrrp group g1 advertise-interval 10",
+ "set high-availability vrrp group g1 authentication password testpass",
+ "set high-availability vrrp group g1 authentication type plaintext-password",
+ "set high-availability vrrp group g1 description 'Group_1'",
+ "set high-availability vrrp group g1 disable",
+ "set high-availability vrrp group g1 garp interval 20",
+ "set high-availability vrrp group g1 garp master-delay 5",
+ "set high-availability vrrp group g1 garp master-refresh 50",
+ "set high-availability vrrp group g1 garp master-refresh-repeat 100",
+ "set high-availability vrrp group g1 garp master-repeat 3",
+ "set high-availability vrrp group g1 interface eth2",
+ "set high-availability vrrp group g1 no-preempt",
+ "set high-availability vrrp group g1 peer-address 192.168.1.3",
+ "set high-availability vrrp group g1 priority 100",
+ "set high-availability vrrp group g1 rfc3768-compatibility",
+ "set high-availability vrrp group g1 track exclude-vrrp-interface",
+ "set high-availability vrrp group g1 transition-script backup /var/tmp/script.sh",
+ "set high-availability vrrp group g1 transition-script fault /var/tmp/script.sh",
+ "set high-availability vrrp group g1 transition-script master /var/tmp/script.sh",
+ "set high-availability vrrp group g1 transition-script stop /var/tmp/script.sh",
+ "set high-availability vrrp group g1 vrid 20",
+ "set high-availability vrrp group g2 address 192.168.3.3",
+ "set high-availability vrrp group g2 address 192.168.4.4 interface eth3",
+ "set high-availability vrrp sync-group sg1 health-check failure-count 4",
+ "set high-availability vrrp sync-group sg1 health-check interval 10",
+ "set high-availability vrrp sync-group sg1 health-check ping 192.168.1.100",
+ "set high-availability vrrp sync-group sg1 health-check script /var/tmp/script.sh",
+ "set high-availability vrrp sync-group sg1 member g1",
+ "set high-availability vrrp sync-group sg1 member g2",
+ "set high-availability vrrp sync-group sg1 transition-script backup /var/tmp/script.sh",
+ "set high-availability vrrp sync-group sg1 transition-script fault /var/tmp/script.sh",
+ "set high-availability vrrp sync-group sg1 transition-script master /var/tmp/script.sh",
+ "set high-availability vrrp sync-group sg1 transition-script stop /var/tmp/script.sh",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrrp_overridden_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ disable=True,
+ virtual_servers=[
+ dict(
+ address="10.10.10.1",
+ algorithm="round-robin",
+ delay_loop=60,
+ forward_method="direct",
+ fwmark=10,
+ name="s1",
+ persistence_timeout="30",
+ protocol="tcp",
+ real_server=[
+ dict(
+ address="10.10.50.1",
+ port=443,
+ health_check_script="/var/tmp/script.sh",
+ ),
+ ],
+ ),
+ dict(
+ address="10.10.10.2",
+ name="s2",
+ port=81,
+ real_server=[
+ dict(
+ address="real1",
+ port=8081,
+ connection_timeout=5,
+ ),
+ dict(
+ address="real2",
+ port=8080,
+ ),
+ ],
+ ),
+ ],
+ vrrp=dict(
+ global_parameters=dict(
+ garp=dict(
+ interval=30,
+ master_delay=10,
+ master_refresh=100,
+ master_refresh_repeat=200,
+ master_repeat=5,
+ ),
+ version="3",
+ startup_delay=30,
+ ),
+ groups=[
+ dict(
+ authentication=dict(
+ password="testpass",
+ type="plaintext-password",
+ ),
+ address=[
+ "1.1.1.1",
+ "3.3.3.3",
+ "5.5.5.5 interface eth2",
+ ],
+ advertise_interval=10,
+ description="Group_1",
+ disable=True,
+ excluded_address=[
+ "192.168.1.8",
+ "192.168.1.7 interface eth3",
+ ],
+ garp=dict(
+ interval=20,
+ master_delay=5,
+ master_refresh=50,
+ master_refresh_repeat=100,
+ master_repeat=3,
+ ),
+ health_check=dict(
+ failure_count=3,
+ interval=10,
+ ping="192.168.1.5",
+ script="script.sh",
+ ),
+ hello_source_address="192.168.1.2",
+ interface="eth2",
+ name="g1",
+ no_preempt=True,
+ peer_address="192.168.1.3",
+ priority=100,
+ rfc3768_compatibility=True,
+ track=dict(
+ exclude_vrrp_interface=True,
+ interface=["eth1"],
+ ),
+ transition_script=dict(
+ backup="/var/tmp/script.sh",
+ fault="/var/tmp/script.sh",
+ master="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ vrid=20,
+ ),
+ ],
+ snmp="enabled",
+ sync_groups=[
+ dict(
+ health_check=dict(
+ failure_count=3,
+ interval=10,
+ ping="192.168.1.1",
+ script="/var/tmp/script.sh",
+ ),
+ member=["g1"],
+ name="sg1",
+ transition_script=dict(
+ backup="/var/tmp/script.sh",
+ fault="/var/tmp/script.sh",
+ master="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ ),
+ ],
+ ),
+ ),
+ state="overridden",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vrrp_rendered(self):
+ set_module_args(
+ dict(
+ config=dict(
+ virtual_servers=[
+ dict(
+ name="s1",
+ address="10.10.10.1",
+ algorithm="round-robin",
+ delay_loop=60,
+ forward_method="direct",
+ fwmark=10,
+ persistence_timeout=30,
+ protocol="tcp",
+ real_server=[
+ dict(
+ address="10.10.50.1",
+ health_check_script="/var/tmp/script.sh",
+ port=443,
+ ),
+ ],
+ ),
+ dict(
+ name="s2",
+ address="10.10.10.2",
+ port=81,
+ real_server=[
+ dict(
+ address="real1",
+ connection_timeout=5,
+ port=8081,
+ ),
+ dict(
+ address="real2",
+ port=8080,
+ ),
+ ],
+ ),
+ ],
+ vrrp=dict(
+ snmp="enabled",
+ global_parameters=dict(
+ startup_delay=31,
+ version="3",
+ garp=dict(
+ interval=30,
+ master_delay=10,
+ master_refresh=100,
+ master_refresh_repeat=200,
+ master_repeat=5,
+ ),
+ ),
+ groups=[
+ dict(
+ name="g1",
+ description="Group_1",
+ interface="eth2",
+ address="1.1.1.1",
+ advertise_interval=10,
+ peer_address="192.168.1.3",
+ priority=100,
+ disable=True,
+ no_preempt=True,
+ rfc3768_compatibility=True,
+ vrid=20,
+ excluded_address=[
+ "192.168.1.7 interface eth3",
+ ],
+ garp=dict(
+ interval=20,
+ master_delay=5,
+ master_refresh=50,
+ master_refresh_repeat=100,
+ master_repeat=3,
+ ),
+ authentication=dict(
+ type="plaintext-password",
+ password="testpass",
+ ),
+ transition_script=dict(
+ master="/var/tmp/script.sh",
+ backup="/var/tmp/script.sh",
+ fault="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ track=dict(
+ exclude_vrrp_interface=True,
+ interface=[
+ "eth0",
+ ],
+ ),
+ ),
+ dict(
+ name="g2",
+ description="Group_2",
+ interface="eth1",
+ address=[
+ "2.2.2.2",
+ "2.2.2.3 interface eth3",
+ ],
+ disable=False,
+ no_preempt=False,
+ rfc3768_compatibility=False,
+ vrid=11,
+ health_check=dict(
+ failure_count=5,
+ interval=15,
+ ping="192.168.1.100",
+ script="/var/tmp/script.sh",
+ ),
+ hello_source_address="2.2.2.2",
+ ),
+ ],
+ sync_groups=[
+ dict(
+ name="sg1",
+ member=[
+ "g1",
+ ],
+ transition_script=dict(
+ master="/var/tmp/script.sh",
+ backup="/var/tmp/script.sh",
+ fault="/var/tmp/script.sh",
+ stop="/var/tmp/script.sh",
+ ),
+ health_check=dict(
+ failure_count=3,
+ interval=10,
+ ping="192.168.2.100",
+ script="/var/tmp/script.sh",
+ ),
+ ),
+ ],
+ ),
+ ),
+ state="rendered",
+ ),
+ )
+ rendered_commands = [
+ "set high-availability virtual-server s1 address 10.10.10.1",
+ "set high-availability virtual-server s1 algorithm round-robin",
+ "set high-availability virtual-server s1 delay-loop 60",
+ "set high-availability virtual-server s1 forward-method direct",
+ "set high-availability virtual-server s1 fwmark 10",
+ "set high-availability virtual-server s1 persistence-timeout 30",
+ "set high-availability virtual-server s1 protocol tcp",
+ "set high-availability virtual-server s1 real-server 10.10.50.1 health-check script /var/tmp/script.sh",
+ "set high-availability virtual-server s1 real-server 10.10.50.1 port 443",
+ "set high-availability virtual-server s2 address 10.10.10.2",
+ "set high-availability virtual-server s2 port 81",
+ "set high-availability virtual-server s2 real-server real1 connection-timeout 5",
+ "set high-availability virtual-server s2 real-server real1 port 8081",
+ "set high-availability virtual-server s2 real-server real2 port 8080",
+ "set high-availability vrrp global-parameters garp interval 30",
+ "set high-availability vrrp global-parameters garp master-delay 10",
+ "set high-availability vrrp global-parameters garp master-refresh 100",
+ "set high-availability vrrp global-parameters garp master-refresh-repeat 200",
+ "set high-availability vrrp global-parameters garp master-repeat 5",
+ "set high-availability vrrp global-parameters startup-delay 31",
+ "set high-availability vrrp global-parameters version 3",
+ "set high-availability vrrp group g1 address 1.1.1.1",
+ "set high-availability vrrp group g1 advertise-interval 10",
+ "set high-availability vrrp group g1 authentication password testpass",
+ "set high-availability vrrp group g1 authentication type plaintext-password",
+ "set high-availability vrrp group g1 description 'Group_1'",
+ "set high-availability vrrp group g1 disable",
+ "set high-availability vrrp group g1 excluded-address 192.168.1.7 interface eth3",
+ "set high-availability vrrp group g1 garp interval 20",
+ "set high-availability vrrp group g1 garp master-delay 5",
+ "set high-availability vrrp group g1 garp master-refresh 50",
+ "set high-availability vrrp group g1 garp master-refresh-repeat 100",
+ "set high-availability vrrp group g1 garp master-repeat 3",
+ "set high-availability vrrp group g1 interface eth2",
+ "set high-availability vrrp group g1 no-preempt",
+ "set high-availability vrrp group g1 peer-address 192.168.1.3",
+ "set high-availability vrrp group g1 priority 100",
+ "set high-availability vrrp group g1 rfc3768-compatibility",
+ "set high-availability vrrp group g1 track exclude-vrrp-interface",
+ "set high-availability vrrp group g1 track interface eth0",
+ "set high-availability vrrp group g1 transition-script backup /var/tmp/script.sh",
+ "set high-availability vrrp group g1 transition-script fault /var/tmp/script.sh",
+ "set high-availability vrrp group g1 transition-script master /var/tmp/script.sh",
+ "set high-availability vrrp group g1 transition-script stop /var/tmp/script.sh",
+ "set high-availability vrrp group g1 vrid 20",
+ "set high-availability vrrp group g2 address 2.2.2.2",
+ "set high-availability vrrp group g2 address 2.2.2.3 interface eth3",
+ "set high-availability vrrp group g2 description 'Group_2'",
+ "set high-availability vrrp group g2 health-check failure-count 5",
+ "set high-availability vrrp group g2 health-check interval 15",
+ "set high-availability vrrp group g2 health-check ping 192.168.1.100",
+ "set high-availability vrrp group g2 health-check script /var/tmp/script.sh",
+ "set high-availability vrrp group g2 hello-source-address 2.2.2.2",
+ "set high-availability vrrp group g2 interface eth1",
+ "set high-availability vrrp group g2 vrid 11",
+ "set high-availability vrrp snmp",
+ "set high-availability vrrp sync-group sg1 health-check failure-count 3",
+ "set high-availability vrrp sync-group sg1 health-check interval 10",
+ "set high-availability vrrp sync-group sg1 health-check ping 192.168.2.100",
+ "set high-availability vrrp sync-group sg1 health-check script /var/tmp/script.sh",
+ "set high-availability vrrp sync-group sg1 member g1",
+ "set high-availability vrrp sync-group sg1 transition-script backup /var/tmp/script.sh",
+ "set high-availability vrrp sync-group sg1 transition-script fault /var/tmp/script.sh",
+ "set high-availability vrrp sync-group sg1 transition-script master /var/tmp/script.sh",
+ "set high-availability vrrp sync-group sg1 transition-script stop /var/tmp/script.sh",
+ ]
+ result = self.execute_module(changed=False)
+ # rendered output order is not significant — use sorted comparison
+ self.assertEqual(
+ sorted(result["rendered"]),
+ sorted(rendered_commands),
+ result["rendered"],
+ )
+
+ def test_vrrp_parsed(self):
+ commands = (
+ "set high-availability disable",
+ "set high-availability virtual-server s1 address 10.10.10.1",
+ "set high-availability virtual-server s1 algorithm round-robin",
+ "set high-availability virtual-server s1 delay-loop 60",
+ "set high-availability virtual-server s1 forward-method direct",
+ "set high-availability virtual-server s1 fwmark 10",
+ "set high-availability virtual-server s1 persistence-timeout 30",
+ "set high-availability virtual-server s1 protocol tcp",
+ "set high-availability virtual-server s1 real-server 10.10.50.1 health-check script '/var/tmp/script.sh'",
+ "set high-availability virtual-server s1 real-server 10.10.50.1 port 443",
+ "set high-availability virtual-server s2 address 10.10.10.2",
+ "set high-availability virtual-server s2 port 81",
+ "set high-availability virtual-server s2 real-server real1 connection-timeout 5",
+ "set high-availability virtual-server s2 real-server real1 port 8081",
+ "set high-availability virtual-server s2 real-server real2 port 8080",
+ "set high-availability vrrp global-parameters garp interval 30",
+ "set high-availability vrrp global-parameters garp master-delay 10",
+ "set high-availability vrrp global-parameters garp master-refresh 100",
+ "set high-availability vrrp global-parameters garp master-refresh-repeat 200",
+ "set high-availability vrrp global-parameters garp master-repeat 5",
+ "set high-availability vrrp global-parameters startup-delay 30",
+ "set high-availability vrrp global-parameters version 3",
+ "set high-availability vrrp group g1 address '1.1.1.1'",
+ "set high-availability vrrp group g1 address '3.3.3.3'",
+ "set high-availability vrrp group g1 address '5.5.5.5' interface 'eth2'",
+ "set high-availability vrrp group g1 advertise-interval 10",
+ "set high-availability vrrp group g1 authentication password 'testpass'",
+ "set high-availability vrrp group g1 authentication type 'plaintext-password'",
+ "set high-availability vrrp group g1 description 'Group_1'",
+ "set high-availability vrrp group g1 disable",
+ "set high-availability vrrp group g1 excluded-address '192.168.1.7' interface 'eth3'",
+ "set high-availability vrrp group g1 excluded-address '192.168.1.8'",
+ "set high-availability vrrp group g1 garp interval 20",
+ "set high-availability vrrp group g1 garp master-delay 5",
+ "set high-availability vrrp group g1 garp master-refresh 50",
+ "set high-availability vrrp group g1 garp master-refresh-repeat 100",
+ "set high-availability vrrp group g1 garp master-repeat 3",
+ "set high-availability vrrp group g1 health-check failure-count 3",
+ "set high-availability vrrp group g1 health-check interval 10",
+ "set high-availability vrrp group g1 health-check ping '192.168.1.5'",
+ "set high-availability vrrp group g1 health-check script 'script.sh'",
+ "set high-availability vrrp group g1 hello-source-address '192.168.1.2'",
+ "set high-availability vrrp group g1 interface 'eth2'",
+ "set high-availability vrrp group g1 no-preempt",
+ "set high-availability vrrp group g1 peer-address '192.168.1.3'",
+ "set high-availability vrrp group g1 priority 100",
+ "set high-availability vrrp group g1 rfc3768-compatibility",
+ "set high-availability vrrp group g1 track exclude-vrrp-interface",
+ "set high-availability vrrp group g1 track interface 'eth1'",
+ "set high-availability vrrp group g1 transition-script backup '/var/tmp/script.sh'",
+ "set high-availability vrrp group g1 transition-script fault '/var/tmp/script.sh'",
+ "set high-availability vrrp group g1 transition-script master '/var/tmp/script.sh'",
+ "set high-availability vrrp group g1 transition-script stop '/var/tmp/script.sh'",
+ "set high-availability vrrp group g1 vrid 20",
+ "set high-availability vrrp group g2 address '192.168.3.3'",
+ "set high-availability vrrp group g2 address '192.168.4.4' interface 'eth3'",
+ "set high-availability vrrp snmp",
+ "set high-availability vrrp sync-group sg1 health-check failure-count 3",
+ "set high-availability vrrp sync-group sg1 health-check interval 10",
+ "set high-availability vrrp sync-group sg1 health-check ping '192.168.1.1'",
+ "set high-availability vrrp sync-group sg1 health-check script '/var/tmp/script.sh'",
+ "set high-availability vrrp sync-group sg1 member 'g1'",
+ "set high-availability vrrp sync-group sg1 transition-script backup '/var/tmp/script.sh'",
+ "set high-availability vrrp sync-group sg1 transition-script fault '/var/tmp/script.sh'",
+ "set high-availability vrrp sync-group sg1 transition-script master '/var/tmp/script.sh'",
+ "set high-availability vrrp sync-group sg1 transition-script stop '/var/tmp/script.sh'",
+ )
+ parsed_str = "\n".join(commands)
+ set_module_args(dict(running_config=parsed_str, state="parsed"))
+ result = self.execute_module(changed=False)
+ parsed_list = {
+ "disable": True,
+ "virtual_servers": [
+ {
+ "name": "s1",
+ "address": "10.10.10.1",
+ "algorithm": "round-robin",
+ "delay_loop": 60,
+ "forward_method": "direct",
+ "fwmark": 10,
+ "persistence_timeout": 30,
+ "protocol": "tcp",
+ "real_server": [
+ {
+ "address": "10.10.50.1",
+ "health_check_script": "/var/tmp/script.sh",
+ "port": 443,
+ },
+ ],
+ },
+ {
+ "name": "s2",
+ "address": "10.10.10.2",
+ "port": 81,
+ "real_server": [
+ {
+ "address": "real1",
+ "connection_timeout": 5,
+ "port": 8081,
+ },
+ {
+ "address": "real2",
+ "port": 8080,
+ },
+ ],
+ },
+ ],
+ "vrrp": {
+ "snmp": "enabled",
+ "global_parameters": {
+ "startup_delay": 30,
+ "version": "3",
+ "garp": {
+ "interval": 30,
+ "master_delay": 10,
+ "master_refresh": 100,
+ "master_refresh_repeat": 200,
+ "master_repeat": 5,
+ },
+ },
+ "groups": [
+ {
+ "name": "g1",
+ "description": "Group_1",
+ "disable": True,
+ "no_preempt": True,
+ "rfc3768_compatibility": True,
+ "interface": "eth2",
+ "address": ["1.1.1.1", "3.3.3.3", "5.5.5.5 interface eth2"],
+ "advertise_interval": 10,
+ "peer_address": "192.168.1.3",
+ "priority": 100,
+ "vrid": 20,
+ "garp": {
+ "interval": 20,
+ "master_delay": 5,
+ "master_refresh": 50,
+ "master_refresh_repeat": 100,
+ "master_repeat": 3,
+ },
+ "authentication": {
+ "type": "plaintext-password",
+ "password": "testpass",
+ },
+ "transition_script": {
+ "master": "/var/tmp/script.sh",
+ "backup": "/var/tmp/script.sh",
+ "fault": "/var/tmp/script.sh",
+ "stop": "/var/tmp/script.sh",
+ },
+ "health_check": {
+ "failure_count": 3,
+ "interval": 10,
+ "ping": "192.168.1.5",
+ "script": "script.sh",
+ },
+ "track": {
+ "exclude_vrrp_interface": True,
+ "interface": ["eth1"],
+ },
+ "excluded_address": [
+ "192.168.1.7 interface eth3",
+ "192.168.1.8",
+ ],
+ "hello_source_address": "192.168.1.2",
+ },
+ {
+ "name": "g2",
+ "address": ["192.168.3.3", "192.168.4.4 interface eth3"],
+ "disable": False,
+ "no_preempt": False,
+ "rfc3768_compatibility": False,
+ },
+ ],
+ "sync_groups": [
+ {
+ "name": "sg1",
+ "member": ["g1"],
+ "transition_script": {
+ "master": "/var/tmp/script.sh",
+ "backup": "/var/tmp/script.sh",
+ "fault": "/var/tmp/script.sh",
+ "stop": "/var/tmp/script.sh",
+ },
+ "health_check": {
+ "failure_count": 3,
+ "interval": 10,
+ "ping": "192.168.1.1",
+ "script": "/var/tmp/script.sh",
+ },
+ },
+ ],
+ },
+ }
+ self.assertEqual(parsed_list, result["parsed"])
+
+ def test_vrrp_gathered(self):
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False)
+ gathered_list = {
+ "disable": True,
+ "virtual_servers": [
+ {
+ "name": "s1",
+ "address": "10.10.10.1",
+ "algorithm": "round-robin",
+ "delay_loop": 60,
+ "forward_method": "direct",
+ "fwmark": 10,
+ "persistence_timeout": 30,
+ "protocol": "tcp",
+ "real_server": [
+ {
+ "address": "10.10.50.1",
+ "health_check_script": "/var/tmp/script.sh",
+ "port": 443,
+ },
+ ],
+ },
+ {
+ "name": "s2",
+ "address": "10.10.10.2",
+ "port": 81,
+ "real_server": [
+ {
+ "address": "real1",
+ "connection_timeout": 5,
+ "port": 8081,
+ },
+ {
+ "address": "real2",
+ "port": 8080,
+ },
+ ],
+ },
+ ],
+ "vrrp": {
+ "snmp": "enabled",
+ "global_parameters": {
+ "startup_delay": 30,
+ "version": "3",
+ "garp": {
+ "interval": 30,
+ "master_delay": 10,
+ "master_refresh": 100,
+ "master_refresh_repeat": 200,
+ "master_repeat": 5,
+ },
+ },
+ "groups": [
+ {
+ "name": "g1",
+ "description": "Group_1",
+ "disable": True,
+ "no_preempt": True,
+ "rfc3768_compatibility": True,
+ "interface": "eth2",
+ "address": ["1.1.1.1", "3.3.3.3", "5.5.5.5 interface eth2"],
+ "advertise_interval": 10,
+ "peer_address": "192.168.1.3",
+ "priority": 100,
+ "vrid": 20,
+ "garp": {
+ "interval": 20,
+ "master_delay": 5,
+ "master_refresh": 50,
+ "master_refresh_repeat": 100,
+ "master_repeat": 3,
+ },
+ "authentication": {
+ "type": "plaintext-password",
+ "password": "testpass",
+ },
+ "transition_script": {
+ "master": "/var/tmp/script.sh",
+ "backup": "/var/tmp/script.sh",
+ "fault": "/var/tmp/script.sh",
+ "stop": "/var/tmp/script.sh",
+ },
+ "health_check": {
+ "failure_count": 3,
+ "interval": 10,
+ "ping": "192.168.1.5",
+ "script": "script.sh",
+ },
+ "track": {
+ "exclude_vrrp_interface": True,
+ "interface": ["eth1"],
+ },
+ "excluded_address": [
+ "192.168.1.7 interface eth3",
+ "192.168.1.8",
+ ],
+ "hello_source_address": "192.168.1.2",
+ },
+ ],
+ "sync_groups": [
+ {
+ "name": "sg1",
+ "member": ["g1"],
+ "transition_script": {
+ "master": "/var/tmp/script.sh",
+ "backup": "/var/tmp/script.sh",
+ "fault": "/var/tmp/script.sh",
+ "stop": "/var/tmp/script.sh",
+ },
+ "health_check": {
+ "failure_count": 3,
+ "interval": 10,
+ "ping": "192.168.1.1",
+ "script": "/var/tmp/script.sh",
+ },
+ },
+ ],
+ },
+ }
+ self.assertEqual(gathered_list, result["gathered"])
+
+ def test_vrrp_groups_deleted(self):
+ set_module_args(
+ dict(
+ config=dict(
+ vrrp=dict(
+ groups=[
+ dict(name="g1"),
+ ],
+ sync_groups=[
+ dict(name="sg1"),
+ ],
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete high-availability vrrp group g1",
+ "delete high-availability vrrp sync-group sg1",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrrp_objs_deleted(self):
+ set_module_args(
+ dict(
+ config=dict(
+ disable=False,
+ vrrp=dict(
+ global_parameters=dict(
+ startup_delay=32,
+ garp=dict(),
+ ),
+ groups=[
+ dict(name="g1"),
+ ],
+ sync_groups=[],
+ ),
+ virtual_servers=[],
+ ),
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete high-availability virtual-server",
+ "delete high-availability vrrp global-parameters garp",
+ "delete high-availability vrrp global-parameters startup-delay",
+ "delete high-availability vrrp group g1",
+ "delete high-availability vrrp sync-group",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrrp_deleted_all(self):
+ set_module_args(
+ dict(
+ config=dict(),
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete high-availability",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrrp_purged(self):
+ set_module_args(
+ dict(
+ config=dict(),
+ state="purged",
+ ),
+ )
+ commands = [
+ "delete high-availability",
+ ]
+ self.execute_module(changed=True, commands=commands)
diff --git a/tests/unit/modules/network/vyos/test_vyos_hostname.py b/tests/unit/modules/network/vyos/test_vyos_hostname.py
index a056f6ba..3101cacc 100644
--- a/tests/unit/modules/network/vyos/test_vyos_hostname.py
+++ b/tests/unit/modules/network/vyos/test_vyos_hostname.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_interfaces.py b/tests/unit/modules/network/vyos/test_vyos_interfaces.py
index f4a5d2a5..a8f812c1 100644
--- a/tests/unit/modules/network/vyos/test_vyos_interfaces.py
+++ b/tests/unit/modules/network/vyos/test_vyos_interfaces.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -236,6 +235,34 @@ class TestVyosInterfacesModule(TestVyosModule):
]
self.execute_module(changed=True, commands=commands)
+ def test_vyos_interfaces_replaced_remove_vrf(self):
+ # we have a vrf in eth2 at this point, so that should be removed
+ self.fixture_path = "vyos_interfaces_config_vrf.cfg"
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth4",
+ description="Ethernet 4",
+ enabled=True,
+ speed="auto",
+ duplex="auto",
+ ),
+ dict(name="eth2", description="Configured by Ansible"),
+ ],
+ state="replaced",
+ ),
+ )
+
+ commands = [
+ "delete interfaces ethernet eth2 vrf",
+ "set interfaces ethernet eth2 description 'Configured by Ansible'",
+ "set interfaces ethernet eth4 description 'Ethernet 4'",
+ "set interfaces ethernet eth4 duplex 'auto'",
+ "set interfaces ethernet eth4 speed 'auto'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
def test_vyos_interfaces_merged_enable_vif(self):
# merge in enabling vif
self.fixture_path = "vyos_interfaces_config_vif.cfg"
@@ -363,6 +390,34 @@ class TestVyosInterfacesModule(TestVyosModule):
]
self.execute_module(changed=True, commands=commands)
+ def test_vyos_overridden_remove_vrf(self):
+ # we have a vrf in eth2 at this point, so that should be removed
+ self.fixture_path = "vyos_interfaces_config_vrf.cfg"
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth4",
+ description="Ethernet 4",
+ enabled=True,
+ speed="auto",
+ duplex="auto",
+ ),
+ dict(name="eth2", description="Configured by Ansible"),
+ ],
+ state="overridden",
+ ),
+ )
+
+ commands = [
+ "set interfaces ethernet eth2 description 'Configured by Ansible'",
+ "set interfaces ethernet eth4 description 'Ethernet 4'",
+ "set interfaces ethernet eth4 duplex 'auto'",
+ "set interfaces ethernet eth4 speed 'auto'",
+ "delete interfaces ethernet eth2 vrf",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
def test_vyos_interfaces_idempotent_disable(self):
set_module_args(
dict(
@@ -416,6 +471,23 @@ class TestVyosInterfacesModule(TestVyosModule):
]
self.execute_module(changed=True, commands=commands)
+ def test_vyos_interfaces_deleted_remove_vrf(self):
+ # we have a vrf in eth2 at this point, so that should be removed
+ self.fixture_path = "vyos_interfaces_config_vrf.cfg"
+ set_module_args(
+ dict(
+ config=[
+ dict(name="eth2"),
+ ],
+ state="deleted",
+ ),
+ )
+
+ commands = [
+ "delete interfaces ethernet eth2 vrf",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
def test_vyos_interfaces_deleted_remove_all(self):
# we have a vif in eth1 at this point, so that should be removed
set_module_args(
@@ -450,3 +522,115 @@ class TestVyosInterfacesModule(TestVyosModule):
)
self.execute_module(failed=True)
+
+ def test_vyos_interfaces_merged_idempotent_vrf(self):
+
+ self.fixture_path = "vyos_interfaces_config_vrf.cfg"
+ set_module_args(
+ dict(
+ config=[
+ dict(name="eth2", vrf="green"),
+ ],
+ state="merged",
+ ),
+ )
+
+ commands = []
+ self.execute_module(changed=False, commands=commands)
+
+ def test_vyos_interfaces_replaced_idempotent_vrf(self):
+
+ self.fixture_path = "vyos_interfaces_config_vrf.cfg"
+ set_module_args(
+ dict(
+ config=[
+ dict(name="eth2", vrf="green"),
+ ],
+ state="replaced",
+ ),
+ )
+
+ commands = []
+ self.execute_module(changed=False, commands=commands)
+
+ def test_vyos_interfaces_overridden_idempotent_vrf(self):
+
+ self.fixture_path = "vyos_interfaces_config_vrf.cfg"
+ set_module_args(
+ dict(
+ config=[
+ dict(name="eth2", vrf="green"),
+ ],
+ state="overridden",
+ ),
+ )
+
+ commands = []
+ self.execute_module(changed=False, commands=commands)
+
+ def test_vyos_interfaces_merged_additional_vrf(self):
+
+ self.fixture_path = "vyos_interfaces_config_vrf.cfg"
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ vrf="pink",
+ ),
+ ],
+ state="merged",
+ ),
+ )
+
+ commands = [
+ "set interfaces ethernet eth1 vrf 'pink'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_interfaces_gathered_vrf(self):
+ # gathered facts should surface the vrf set on eth2
+ self.fixture_path = "vyos_interfaces_config_vrf.cfg"
+ set_module_args(
+ dict(
+ config=[],
+ state="gathered",
+ ),
+ )
+
+ result = self.execute_module(changed=False)
+ eth2_facts = [i for i in result["gathered"] if i["name"] == "eth2"][0]
+ self.assertEqual(eth2_facts["vrf"], "green")
+
+ def test_vyos_interfaces_parsed_vrf(self):
+ # parsing a running_config string should surface vrf on eth2
+ set_module_args(
+ dict(
+ running_config="set interfaces ethernet eth2 vrf 'green'",
+ state="parsed",
+ ),
+ )
+
+ result = self.execute_module(changed=False)
+ eth2_facts = [i for i in result["parsed"] if i["name"] == "eth2"][0]
+ self.assertEqual(eth2_facts["vrf"], "green")
+
+ def test_vyos_interfaces_rendered_vrf(self):
+ # rendered output should include the vrf set command without touching a device
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ vrf="pink",
+ ),
+ ],
+ state="rendered",
+ ),
+ )
+
+ result = self.execute_module(changed=False)
+ commands = [
+ "set interfaces ethernet eth1 vrf 'pink'",
+ ]
+ self.assertEqual(sorted(result["rendered"]), sorted(commands))
diff --git a/tests/unit/modules/network/vyos/test_vyos_l3_interfaces.py b/tests/unit/modules/network/vyos/test_vyos_l3_interfaces.py
new file mode 100644
index 00000000..7932f63d
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_l3_interfaces.py
@@ -0,0 +1,278 @@
+# (c) 2024 VyOS Networks <maintainers@vyos.net>
+#
+# 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/>.
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_l3_interfaces
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosL3InterfacesModule(TestVyosModule):
+ module = vyos_l3_interfaces
+
+ def setUp(self):
+ super(TestVyosL3InterfacesModule, 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()
+
+ def tearDown(self):
+ super(TestVyosL3InterfacesModule, 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()
+
+ def load_fixtures(self, commands=None, filename=None):
+ fixture_data = load_fixture("vyos_l3_interfaces_config.cfg")
+ self.get_resource_connection_facts.return_value.get_config.return_value = fixture_data
+
+ def test_vyos_l3_interfaces_merged(self):
+ """Merge a new IPv4 address onto eth1 (currently has 192.0.2.14/24)."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ ipv4=[dict(address="192.0.2.1/24")],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set interfaces ethernet eth1 address '192.0.2.1/24'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_merged_idempotent(self):
+ """No change when desired config already matches existing config."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ ipv4=[dict(address="192.0.2.14/24")],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_l3_interfaces_merged_ipv6(self):
+ """Merge a new IPv6 address onto eth1."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ ipv6=[dict(address="2001:db8::1/32")],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set interfaces ethernet eth1 address '2001:db8::1/32'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_replaced(self):
+ """Replace eth1 addresses: remove existing, add new."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ ipv4=[dict(address="10.0.0.1/24")],
+ ),
+ ],
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete interfaces ethernet eth1 address '192.0.2.14/24'",
+ "set interfaces ethernet eth1 address '10.0.0.1/24'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_overridden(self):
+ """Override: keep only eth2; delete L3 addresses from all other interfaces."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth2",
+ ipv4=[dict(address="192.0.2.10/24")],
+ ipv6=[dict(address="2001:db8::10/32")],
+ ),
+ ],
+ state="overridden",
+ ),
+ )
+ commands = [
+ "delete interfaces ethernet eth0 address 'dhcp'",
+ "delete interfaces ethernet eth1 address '192.0.2.14/24'",
+ "delete interfaces ethernet eth3 address '198.51.100.10/24'",
+ "delete interfaces ethernet eth3 vif 101 address '198.51.100.130/25'",
+ "delete interfaces ethernet eth3 vif 102 address '2001:db8:4000::3/34'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_deleted(self):
+ """Delete all L3 config from eth1."""
+ set_module_args(
+ dict(
+ config=[
+ dict(name="eth1"),
+ ],
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete interfaces ethernet eth1 address '192.0.2.14/24'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_rendered(self):
+ """Render set commands without connecting to the device."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth0",
+ ipv4=[dict(address="dhcp")],
+ ),
+ dict(
+ name="eth1",
+ ipv4=[dict(address="192.0.2.14/24")],
+ ),
+ ],
+ state="rendered",
+ ),
+ )
+ rendered_cmds = [
+ "set interfaces ethernet eth0 address 'dhcp'",
+ "set interfaces ethernet eth1 address '192.0.2.14/24'",
+ ]
+ result = self.execute_module(changed=False)
+ self.assertEqual(
+ sorted(result["rendered"]),
+ sorted(rendered_cmds),
+ result["rendered"],
+ )
+
+ def test_vyos_l3_interfaces_gathered(self):
+ """Gather L3 interface facts from the device."""
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False)
+ gathered = result["gathered"]
+ # Verify eth1 is present with its IPv4 address
+ eth1 = next((i for i in gathered if i["name"] == "eth1"), None)
+ self.assertIsNotNone(eth1)
+ self.assertIn({"address": "192.0.2.14/24"}, eth1["ipv4"])
+
+ def test_vyos_l3_interfaces_parsed(self):
+ """Parse a raw config string into structured data."""
+ raw_config = (
+ "set interfaces ethernet eth0 address 'dhcp'\n"
+ "set interfaces ethernet eth1 address '192.0.2.14/24'\n"
+ )
+ set_module_args(
+ dict(
+ running_config=raw_config,
+ state="parsed",
+ ),
+ )
+ result = self.execute_module(changed=False)
+ parsed = result["parsed"]
+ eth0 = next((i for i in parsed if i["name"] == "eth0"), None)
+ self.assertIsNotNone(eth0)
+ self.assertIn({"address": "dhcp"}, eth0["ipv4"])
+ eth1 = next((i for i in parsed if i["name"] == "eth1"), None)
+ self.assertIsNotNone(eth1)
+ self.assertIn({"address": "192.0.2.14/24"}, eth1["ipv4"])
+
+ def test_vyos_l3_interfaces_vif_merged(self):
+ """Merge a new address onto a VIF sub-interface."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth3",
+ vifs=[
+ dict(
+ vlan_id=101,
+ ipv4=[dict(address="198.51.100.200/25")],
+ ),
+ ],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set interfaces ethernet eth3 vif 101 address '198.51.100.200/25'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_l3_interfaces_vif_deleted(self):
+ """Delete all L3 addresses from eth3, including VIF sub-interfaces."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth3",
+ vifs=[
+ dict(vlan_id=101),
+ dict(vlan_id=102),
+ ],
+ ),
+ ],
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete interfaces ethernet eth3 vif 101 address '198.51.100.130/25'",
+ "delete interfaces ethernet eth3 vif 102 address '2001:db8:4000::3/34'",
+ "delete interfaces ethernet eth3 address '198.51.100.10/24'",
+ ]
+ self.execute_module(changed=True, commands=commands)
diff --git a/tests/unit/modules/network/vyos/test_vyos_lag_interfaces.py b/tests/unit/modules/network/vyos/test_vyos_lag_interfaces.py
index 406ca598..41ba88a4 100644
--- a/tests/unit/modules/network/vyos/test_vyos_lag_interfaces.py
+++ b/tests/unit/modules/network/vyos/test_vyos_lag_interfaces.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_lldp_global.py b/tests/unit/modules/network/vyos/test_vyos_lldp_global.py
index 0dc1a8d7..ef5233ce 100644
--- a/tests/unit/modules/network/vyos/test_vyos_lldp_global.py
+++ b/tests/unit/modules/network/vyos/test_vyos_lldp_global.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_lldp_interfaces.py b/tests/unit/modules/network/vyos/test_vyos_lldp_interfaces.py
new file mode 100644
index 00000000..fda8f7f4
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_lldp_interfaces.py
@@ -0,0 +1,258 @@
+# (c) 2024 VyOS Networks <maintainers@vyos.net>
+#
+# 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/>.
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_lldp_interfaces
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosLldpInterfacesModule(TestVyosModule):
+ module = vyos_lldp_interfaces
+
+ def setUp(self):
+ super(TestVyosLldpInterfacesModule, self).setUp()
+
+ 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()
+
+ def tearDown(self):
+ super(TestVyosLldpInterfacesModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ self.get_resource_connection_facts.return_value.get_config.return_value = load_fixture(
+ "vyos_lldp_interfaces_config.cfg",
+ )
+
+ # -------------------------------------------------------------------------
+ # merged
+ # -------------------------------------------------------------------------
+
+ def test_vyos_lldp_interfaces_merged_elin(self):
+ """Add ELIN location to a new interface (eth3)."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth3",
+ location=dict(elin="0000000912"),
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set service lldp interface eth3",
+ "set service lldp interface eth3 location elin '0000000912'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_lldp_interfaces_merged_idempotent(self):
+ """Existing ELIN config — no change expected."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ location=dict(elin="0000000911"),
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_lldp_interfaces_merged_coordinate(self):
+ """Add coordinate-based location to a new interface (eth3)."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth3",
+ location=dict(
+ coordinate_based=dict(
+ altitude=1500,
+ datum="WGS84",
+ latitude="33.000000N",
+ longitude="222.000000W",
+ ),
+ ),
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set service lldp interface eth3",
+ "set service lldp interface eth3 location coordinate-based altitude '1500'",
+ "set service lldp interface eth3 location coordinate-based datum 'WGS84'",
+ "set service lldp interface eth3 location coordinate-based latitude '33.000000N'",
+ "set service lldp interface eth3 location coordinate-based longitude '222.000000W'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # replaced
+ # -------------------------------------------------------------------------
+
+ def test_vyos_lldp_interfaces_replaced(self):
+ """Replace eth1 ELIN with a new value."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ location=dict(elin="9999999999"),
+ ),
+ ],
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete service lldp interface eth1 location",
+ "set service lldp interface eth1 location elin '9999999999'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # overridden
+ # -------------------------------------------------------------------------
+
+ def test_vyos_lldp_interfaces_overridden(self):
+ """Override: keep eth1 with same ELIN, remove eth2."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ location=dict(elin="0000000911"),
+ ),
+ ],
+ state="overridden",
+ ),
+ )
+ commands = ["delete service lldp interface eth2"]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # deleted
+ # -------------------------------------------------------------------------
+
+ def test_vyos_lldp_interfaces_deleted(self):
+ """Delete LLDP config for eth1."""
+ set_module_args(
+ dict(
+ config=[dict(name="eth1")],
+ state="deleted",
+ ),
+ )
+ commands = ["delete service lldp interface eth1"]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # gathered
+ # -------------------------------------------------------------------------
+
+ def test_vyos_lldp_interfaces_gathered(self):
+ """Gather current LLDP interface config from device."""
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False)
+ gathered = result["gathered"]
+ names = sorted(entry["name"] for entry in gathered)
+ self.assertEqual(names, ["eth1", "eth2"])
+
+ eth1 = next(e for e in gathered if e["name"] == "eth1")
+ self.assertEqual(eth1["location"]["elin"], "0000000911")
+
+ eth2 = next(e for e in gathered if e["name"] == "eth2")
+ coord = eth2["location"]["coordinate_based"]
+ self.assertEqual(coord["altitude"], 2200)
+ self.assertEqual(coord["datum"], "WGS84")
+ self.assertEqual(coord["latitude"], "33.524449N")
+ self.assertEqual(coord["longitude"], "222.267255W")
+
+ # -------------------------------------------------------------------------
+ # parsed
+ # -------------------------------------------------------------------------
+
+ def test_vyos_lldp_interfaces_parsed(self):
+ """Parse a raw config string into structured facts."""
+ raw_config = (
+ "set service lldp interface eth1 location elin '0000000911'\n"
+ "set service lldp interface eth2 location coordinate-based altitude '2200'\n"
+ "set service lldp interface eth2 location coordinate-based datum 'WGS84'\n"
+ "set service lldp interface eth2 location coordinate-based latitude '33.524449N'\n"
+ "set service lldp interface eth2 location coordinate-based longitude '222.267255W'\n"
+ )
+ set_module_args(dict(running_config=raw_config, state="parsed"))
+ result = self.execute_module(changed=False)
+ parsed = result["parsed"]
+ names = sorted(entry["name"] for entry in parsed)
+ self.assertEqual(names, ["eth1", "eth2"])
+
+ eth1 = next(e for e in parsed if e["name"] == "eth1")
+ self.assertEqual(eth1["location"]["elin"], "0000000911")
+
+ eth2 = next(e for e in parsed if e["name"] == "eth2")
+ coord = eth2["location"]["coordinate_based"]
+ self.assertEqual(coord["altitude"], 2200)
+ self.assertEqual(coord["datum"], "WGS84")
+ self.assertEqual(coord["latitude"], "33.524449N")
+ self.assertEqual(coord["longitude"], "222.267255W")
+
+ # -------------------------------------------------------------------------
+ # rendered
+ # -------------------------------------------------------------------------
+
+ def test_vyos_lldp_interfaces_rendered(self):
+ """Render set commands without connecting to the device."""
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ name="eth1",
+ location=dict(elin="0000000911"),
+ ),
+ ],
+ state="rendered",
+ ),
+ )
+ rendered_cmds = [
+ "set service lldp interface eth1",
+ "set service lldp interface eth1 location elin '0000000911'",
+ ]
+ result = self.execute_module(changed=False)
+ self.assertEqual(
+ sorted(result["rendered"]),
+ sorted(rendered_cmds),
+ )
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 a6751518..a1ab3c6c 100644
--- a/tests/unit/modules/network/vyos/test_vyos_logging_global.py
+++ b/tests/unit/modules/network/vyos/test_vyos_logging_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from textwrap import dedent
@@ -39,11 +38,25 @@ class TestVyosLoggingGlobalModule(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.logging_global.logging_global.get_os_version",
+ )
+ self.get_os_version = self.mock_get_os_version.start()
+ self.get_os_version.return_value = "1.3"
+
+ self.mock_facts_get_os_version = patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.logging_global.logging_global.get_os_version",
+ )
+ self.get_facts_os_version = self.mock_facts_get_os_version.start()
+ self.get_facts_os_version.return_value = "1.3"
+
def tearDown(self):
super(TestVyosLoggingGlobalModule, self).tearDown()
self.mock_get_resource_connection_config.stop()
self.mock_get_resource_connection_facts.stop()
self.mock_execute_show_command.stop()
+ self.mock_get_os_version.stop()
+ self.mock_facts_get_os_version.stop()
def test_vyos_logging_global_merged_idempotent(self):
self.execute_show_command.return_value = dedent(
@@ -122,7 +135,7 @@ class TestVyosLoggingGlobalModule(TestVyosModule):
global_params=dict(
archive=dict(size=111, file_num=2),
marker_interval=111,
- preserve_fqdn="True",
+ preserve_fqdn=True,
facilities=[
dict(facility="cron", severity="debug"),
dict(facility="local7", severity="debug"),
@@ -194,7 +207,7 @@ class TestVyosLoggingGlobalModule(TestVyosModule):
global_params=dict(
archive=dict(size=111, file_num=2),
marker_interval=111,
- preserve_fqdn="True",
+ preserve_fqdn=True,
facilities=[
dict(facility="cron", severity="debug"),
dict(facility="local7", severity="debug"),
diff --git a/tests/unit/modules/network/vyos/test_vyos_logging_global15.py b/tests/unit/modules/network/vyos/test_vyos_logging_global15.py
new file mode 100644
index 00000000..63b3fd31
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_logging_global15.py
@@ -0,0 +1,353 @@
+#
+# (c) 2021, Ansible by Red Hat, inc
+# 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
+
+from textwrap import dedent
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_logging_global
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule
+
+
+class TestVyosLoggingGlobalModule(TestVyosModule):
+ module = vyos_logging_global
+
+ def setUp(self):
+ super(TestVyosLoggingGlobalModule, 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_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.logging_global.logging_global.Logging_globalFacts.get_logging_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.logging_global.logging_global.get_os_version",
+ )
+ self.get_os_version = self.mock_get_os_version.start()
+ self.get_os_version.return_value = "1.5"
+
+ self.mock_facts_get_os_version = patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.logging_global.logging_global.get_os_version",
+ )
+ self.get_facts_os_version = self.mock_facts_get_os_version.start()
+ self.get_facts_os_version.return_value = "1.5"
+
+ def tearDown(self):
+ super(TestVyosLoggingGlobalModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_execute_show_command.stop()
+ self.mock_get_os_version.stop()
+ self.mock_facts_get_os_version.stop()
+
+ def test_vyos_logging_global_merged_idempotent(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ set system syslog console facility all
+ set system syslog console facility local7 level 'err'
+ set system syslog console facility news level 'debug'
+ set system syslog local facility cron level 'debug'
+ set system syslog local facility local7 level 'debug'
+ set system syslog marker interval '111'
+ set system syslog preserve-fqdn
+ set system syslog remote 10.0.2.12 facility all
+ set system syslog remote 10.0.2.15 facility all level 'all'
+ set system syslog remote 10.0.2.15 port '122'
+ """,
+ )
+ playbook = dict(
+ config=dict(
+ console=dict(
+ facilities=[
+ dict(facility="all"),
+ dict(facility="local7", severity="err"),
+ dict(facility="news", severity="debug"),
+ ],
+ ),
+ hosts=[
+ dict(
+ hostname="10.0.2.15",
+ port=122,
+ facilities=[dict(facility="all", severity="all")],
+ ),
+ dict(
+ hostname="10.0.2.12",
+ facilities=[dict(facility="all")],
+ ),
+ ],
+ global_params=dict(
+ marker_interval=111,
+ preserve_fqdn=True,
+ facilities=[
+ dict(facility="cron", severity="debug"),
+ dict(facility="local7", severity="debug"),
+ ],
+ ),
+ ),
+ )
+ compare_cmds = []
+ playbook["state"] = "merged"
+ set_module_args(playbook)
+ result = self.execute_module()
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_logging_global_merged(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ """,
+ )
+ playbook = dict(
+ config=dict(
+ console=dict(
+ facilities=[
+ dict(facility="all"),
+ dict(facility="local7", severity="err"),
+ dict(facility="news", severity="debug"),
+ ],
+ ),
+ hosts=[
+ dict(
+ hostname="10.0.2.15",
+ port=122,
+ facilities=[dict(facility="all", severity="all")],
+ ),
+ dict(
+ hostname="10.0.2.12",
+ facilities=[dict(facility="all", severity="info")],
+ ),
+ ],
+ global_params=dict(
+ marker_interval=111,
+ preserve_fqdn=True,
+ facilities=[
+ dict(facility="cron", severity="debug"),
+ dict(facility="local7", severity="debug"),
+ ],
+ ),
+ ),
+ )
+ compare_cmds = [
+ "set system syslog console facility all",
+ "set system syslog console facility local7 level err",
+ "set system syslog console facility news level debug",
+ "set system syslog local facility cron level debug",
+ "set system syslog local facility local7 level debug",
+ "set system syslog marker interval 111",
+ "set system syslog preserve-fqdn",
+ "set system syslog remote 10.0.2.15 facility all level all",
+ "set system syslog remote 10.0.2.15 port 122",
+ "set system syslog remote 10.0.2.12 facility all level info",
+ ]
+ playbook["state"] = "merged"
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_logging_global_deleted(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ set system syslog console facility all
+ set system syslog console facility local7 level 'err'
+ set system syslog local facility cron level 'debug'
+ set system syslog local facility local7 level 'debug'
+ set system syslog marker interval '111'
+ set system syslog preserve-fqdn
+ set system syslog remote 10.0.2.12 facility all
+ set system syslog remote 10.0.2.15 facility all level 'all'
+ set system syslog remote 10.0.2.15 port '122'
+ """,
+ )
+ playbook = dict(config=dict())
+ compare_cmds = ["delete system syslog"]
+ playbook["state"] = "deleted"
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_logging_global_replaced(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ set system syslog console facility all
+ set system syslog console facility local7 level 'err'
+ set system syslog console facility news level 'debug'
+ set system syslog local facility cron level 'debug'
+ set system syslog local facility local7 level 'debug'
+ set system syslog marker interval '111'
+ set system syslog preserve-fqdn
+ set system syslog remote 10.0.2.12 facility all
+ set system syslog remote 10.0.2.15 facility all level 'all'
+ set system syslog remote 10.0.2.15 port '122'
+ """,
+ )
+ playbook = dict(
+ config=dict(
+ console=dict(
+ facilities=[dict(facility="local7", severity="emerg")],
+ ),
+ hosts=[
+ dict(
+ hostname="10.0.2.15",
+ port=122,
+ facilities=[dict(facility="all", severity="all")],
+ ),
+ ],
+ ),
+ )
+ compare_cmds = [
+ "delete system syslog console facility all",
+ "delete system syslog console facility local7",
+ "delete system syslog console facility news",
+ "delete system syslog local facility cron",
+ "delete system syslog local facility local7",
+ "delete system syslog marker",
+ "delete system syslog preserve-fqdn",
+ "delete system syslog remote 10.0.2.12",
+ "set system syslog console facility local7 level emerg",
+ ]
+ playbook["state"] = "replaced"
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_logging_global_replaced_idempotent(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ set system syslog console facility local6
+ """,
+ )
+ playbook = dict(config=dict(console=dict(facilities=[dict(facility="local6")])))
+ compare_cmds = []
+ playbook["state"] = "replaced"
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_logging_global_overridden(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ set system syslog console
+ set system syslog local
+ """,
+ )
+ playbook = dict(
+ config=dict(
+ console=dict(
+ facilities=[dict(facility="local7", severity="emerg")],
+ ),
+ global_params=dict(
+ facilities=[dict(facility="all", severity="info")],
+ ),
+ ),
+ )
+ compare_cmds = [
+ "set system syslog console facility local7 level emerg",
+ "set system syslog local facility all level info",
+ ]
+ playbook["state"] = "overridden"
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_logging_global_rendered(self):
+ playbook = dict(
+ config=dict(
+ console=dict(facilities=[dict(facility="all")]),
+ hosts=[
+ dict(
+ hostname="10.0.2.16",
+ facilities=[dict(facility="local6")],
+ ),
+ ],
+ global_params=dict(
+ preserve_fqdn=True,
+ facilities=[dict(facility="all", severity="info")],
+ ),
+ ),
+ )
+ compare_cmds = [
+ "set system syslog console facility all",
+ "set system syslog remote 10.0.2.16 facility local6",
+ "set system syslog preserve-fqdn",
+ "set system syslog local facility all level info",
+ ]
+ playbook["state"] = "rendered"
+ set_module_args(playbook)
+ result = self.execute_module()
+ self.maxDiff = None
+ self.assertEqual(sorted(result["rendered"]), sorted(compare_cmds))
+
+ def test_vyos_logging_global_parsed(self):
+ set_module_args(
+ dict(
+ running_config=dedent(
+ """\
+ set system syslog console facility all
+ set system syslog local facility all level 'info'
+ set system syslog remote 10.0.2.1 facility all level 'info'
+ set system syslog remote 10.0.2.1 port '514'
+ """,
+ ),
+ state="parsed",
+ ),
+ )
+ parsed = dict(
+ console=dict(facilities=[dict(facility="all")]),
+ global_params=dict(facilities=[dict(facility="all", severity="info")]),
+ hosts=[
+ dict(
+ hostname="10.0.2.1",
+ port=514,
+ facilities=[dict(facility="all", severity="info")],
+ ),
+ ],
+ )
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(result["parsed"], parsed)
+
+ def test_vyos_logging_global_gathered(self):
+ self.execute_show_command.return_value = dedent(
+ """\
+ set system syslog console facility all
+ set system syslog local facility all level 'info'
+ set system syslog remote 10.0.2.1 facility all level 'info'
+ """,
+ )
+ set_module_args(dict(state="gathered"))
+ gathered = dict(
+ console=dict(facilities=[dict(facility="all")]),
+ global_params=dict(facilities=[dict(facility="all", severity="info")]),
+ hosts=[
+ dict(
+ hostname="10.0.2.1",
+ facilities=[dict(facility="all", severity="info")],
+ ),
+ ],
+ )
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(result["gathered"], gathered)
diff --git a/tests/unit/modules/network/vyos/test_vyos_nat.py b/tests/unit/modules/network/vyos/test_vyos_nat.py
new file mode 100644
index 00000000..4ef76305
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_nat.py
@@ -0,0 +1,839 @@
+# (c) 2024 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/>.
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_nat
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosNatModule(TestVyosModule):
+ module = vyos_nat
+
+ def setUp(self):
+ super(TestVyosNatModule, 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_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.nat.nat.NatFacts.get_config",
+ )
+ self.execute_show_command = self.mock_execute_show_command.start()
+ self.maxDiff = None
+
+ def tearDown(self):
+ super(TestVyosNatModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_execute_show_command.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ if filename is None:
+ filename = "vyos_nat_config.cfg"
+
+ def load_from_file(*args, **kwargs):
+ return load_fixture(filename)
+
+ self.execute_show_command.side_effect = load_from_file
+
+ # -------------------------------------------------------------------------
+ # merged
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_merged_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ log_allocation=True,
+ pool=dict(
+ external=[
+ dict(
+ name="ext-pool-1",
+ external_port_range="10000-20000",
+ per_user_limit=dict(port="200"),
+ range=[
+ dict(value="203.0.113.0/24"),
+ dict(value="203.1.113.1-203.1.113.60", seq="10"),
+ ],
+ ),
+ ],
+ internal=[
+ dict(
+ name="int-pool-1",
+ range=["10.0.0.0/24", "10.1.0.0/24"],
+ ),
+ ],
+ ),
+ rule=[
+ dict(
+ id=1,
+ source=dict(pool="int-pool-1"),
+ translation=dict(pool="ext-pool-1"),
+ ),
+ ],
+ ),
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Web server NAT",
+ protocol="tcp",
+ log=True,
+ inbound_interface=dict(name="eth2"),
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(
+ address="192.168.1.10",
+ port="8080",
+ address_mapping="persistent",
+ port_mapping="random",
+ ),
+ ),
+ dict(
+ id=105,
+ protocol="tcp",
+ destination=dict(port="80"),
+ inbound_interface=dict(name="eth0"),
+ load_balance=dict(
+ hash=["source-address", "destination-address"],
+ backend=[
+ dict(ip="10.10.10.1", weight=60),
+ dict(ip="10.10.10.2", weight=40),
+ ],
+ ),
+ ),
+ ],
+ ),
+ source=dict(
+ rule=[
+ dict(
+ id=200,
+ description="Outbound NAT",
+ protocol="tcp",
+ log=True,
+ exclude=True,
+ disable=True,
+ destination=dict(address="192.168.10.100", port="8083"),
+ translation=dict(address="masquerade", port="443"),
+ ),
+ ],
+ ),
+ static=dict(
+ rule=[
+ dict(
+ id=300,
+ description="Static mapping",
+ inbound_interface="eth2",
+ destination=dict(address="192.168.100.20"),
+ translation=dict(address="192.168.1.20"),
+ log=True,
+ ),
+ ],
+ ),
+ ),
+ nat64=dict(
+ source=dict(
+ rule=[
+ dict(
+ id=10,
+ description="NAT64 example",
+ disable=True,
+ match=dict(mark=100),
+ source=dict(prefix="2001:db8::/96"),
+ translation=dict(
+ pool=[
+ dict(
+ id=1,
+ address="192.168.100.10",
+ description="NAT64 translation pool",
+ disable=True,
+ port="1-65535",
+ protocol="udp",
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ nat66=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=20,
+ description="NAT66 DNAT",
+ protocol="tcp",
+ inbound_interface=dict(name="eth1"),
+ destination=dict(address="2001:db8::1"),
+ translation=dict(address="2001:db8:1::10", port="8443"),
+ ),
+ ],
+ ),
+ source=dict(
+ rule=[
+ dict(
+ id=30,
+ description="NAT66 SNAT",
+ protocol="tcp",
+ destination=dict(prefix="2001:db8::/96"),
+ outbound_interface=dict(name="eth2"),
+ source=dict(prefix="2001:db8:2::/64"),
+ translation=dict(address="masquerade"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_nat_merged_new_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=101,
+ description="New DNAT rule",
+ protocol="tcp",
+ destination=dict(address="198.51.100.11", port="443"),
+ translation=dict(address="192.168.1.11", port="8443"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat destination rule 101 description 'New DNAT rule'",
+ "set nat destination rule 101 protocol tcp",
+ "set nat destination rule 101 destination address 198.51.100.11",
+ "set nat destination rule 101 destination port 443",
+ "set nat destination rule 101 translation address 192.168.1.11",
+ "set nat destination rule 101 translation port 8443",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_update_existing_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Updated DNAT rule",
+ protocol="tcp",
+ inbound_interface=dict(name="eth2"),
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(
+ address="192.168.1.10",
+ port="8080",
+ address_mapping="persistent",
+ port_mapping="random",
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat destination rule 100 description 'Updated DNAT rule'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_cgnat_new_pool(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ pool=dict(
+ external=[
+ dict(
+ name="ext-pool-2",
+ external_port_range="30000-40000",
+ range=[dict(value="203.0.114.0/24")],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat cgnat pool external ext-pool-2 external-port-range 30000-40000",
+ "set nat cgnat pool external ext-pool-2 range 203.0.114.0/24",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_nat66_new_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat66=dict(
+ source=dict(
+ rule=[
+ dict(
+ id=31,
+ description="New NAT66 SNAT",
+ protocol="udp",
+ outbound_interface=dict(name="eth3"),
+ source=dict(prefix="2001:db8:3::/64"),
+ translation=dict(address="masquerade"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat66 source rule 31 description 'New NAT66 SNAT'",
+ "set nat66 source rule 31 protocol udp",
+ "set nat66 source rule 31 outbound-interface name eth3",
+ "set nat66 source rule 31 source prefix 2001:db8:3::/64",
+ "set nat66 source rule 31 translation address masquerade",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_static_inbound_interface_change(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ static=dict(
+ rule=[
+ dict(
+ id=300,
+ description="Static mapping",
+ inbound_interface="eth3",
+ destination=dict(address="192.168.100.20"),
+ translation=dict(address="192.168.1.20"),
+ log=True,
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = ["set nat static rule 300 inbound-interface eth3"]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # deleted
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_deleted_all(self):
+ set_module_args(dict(state="deleted"))
+ commands = [
+ "delete nat",
+ "delete nat64",
+ "delete nat66",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_deleted_specific_rules(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(rule=[dict(id=100)]),
+ source=dict(rule=[dict(id=200)]),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete nat destination rule 100",
+ "delete nat source rule 200",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_deleted_cgnat_pool(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ pool=dict(
+ external=[dict(name="ext-pool-1")],
+ internal=[dict(name="int-pool-1")],
+ ),
+ ),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete nat cgnat pool external ext-pool-1",
+ "delete nat cgnat pool internal int-pool-1",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_deleted_nat64_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat64=dict(
+ source=dict(rule=[dict(id=10)]),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = ["delete nat64 source rule 10"]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_deleted_nonexistent_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(rule=[dict(id=999)]),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_nat_deleted_cgnat_rule_full_match(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ rule=[
+ dict(
+ id=1,
+ source=dict(pool="int-pool-1"),
+ translation=dict(pool="ext-pool-1"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = ["delete nat cgnat rule 1"]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # replaced
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_replaced_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Web server NAT",
+ protocol="tcp",
+ log=True,
+ inbound_interface=dict(name="eth2"),
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(
+ address="192.168.1.10",
+ port="8080",
+ address_mapping="persistent",
+ port_mapping="random",
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_nat_replaced_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Replaced DNAT rule",
+ protocol="udp",
+ destination=dict(address="198.51.100.10", port="53"),
+ translation=dict(address="192.168.1.53", port="53"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete nat destination rule 100",
+ "set nat destination rule 100 description 'Replaced DNAT rule'",
+ "set nat destination rule 100 protocol udp",
+ "set nat destination rule 100 destination address 198.51.100.10",
+ "set nat destination rule 100 destination port 53",
+ "set nat destination rule 100 translation address 192.168.1.53",
+ "set nat destination rule 100 translation port 53",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_replaced_cgnat_pool_partial_field_change(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ pool=dict(
+ external=[
+ dict(
+ name="ext-pool-1",
+ external_port_range="30000-40000",
+ per_user_limit=dict(port="200"),
+ range=[
+ dict(value="203.0.113.0/24"),
+ dict(value="203.1.113.1-203.1.113.60", seq="10"),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete nat cgnat pool external ext-pool-1",
+ "delete nat cgnat log-allocation",
+ "set nat cgnat pool external ext-pool-1 external-port-range 30000-40000",
+ "set nat cgnat pool external ext-pool-1 per-user-limit port 200",
+ "set nat cgnat pool external ext-pool-1 range 203.0.113.0/24",
+ "set nat cgnat pool external ext-pool-1 range 203.1.113.1-203.1.113.60 seq 10",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # overridden
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_overridden_remove_sections(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Overridden web server NAT", # changed
+ protocol="tcp",
+ inbound_interface=dict(name="eth3"), # changed
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(
+ address="192.168.1.10",
+ port="8080",
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="overridden",
+ ),
+ )
+ commands = [
+ "delete nat cgnat",
+ "delete nat source",
+ "delete nat static",
+ "delete nat64",
+ "delete nat66",
+ "delete nat destination rule 100",
+ "delete nat destination rule 105",
+ "set nat destination rule 100 description 'Overridden web server NAT'",
+ "set nat destination rule 100 protocol tcp",
+ "set nat destination rule 100 inbound-interface name eth3",
+ "set nat destination rule 100 destination address 198.51.100.10",
+ "set nat destination rule 100 destination port 80",
+ "set nat destination rule 100 translation address 192.168.1.10",
+ "set nat destination rule 100 translation port 8080",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # rendered
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_rendered(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Rendered rule",
+ protocol="tcp",
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(address="192.168.1.10", port="8080"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="rendered",
+ ),
+ )
+ rendered_cmds = [
+ "set nat destination rule 100 description 'Rendered rule'",
+ "set nat destination rule 100 protocol tcp",
+ "set nat destination rule 100 destination address 198.51.100.10",
+ "set nat destination rule 100 destination port 80",
+ "set nat destination rule 100 translation address 192.168.1.10",
+ "set nat destination rule 100 translation port 8080",
+ ]
+ result = self.execute_module(changed=False)
+ self.assertEqual(sorted(result["rendered"]), sorted(rendered_cmds), result["rendered"])
+
+ # -------------------------------------------------------------------------
+ # parsed
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_parsed(self):
+ parsed_str = (
+ "set nat destination rule 100 description 'Web server NAT'\n"
+ "set nat destination rule 100 destination address '198.51.100.10'\n"
+ "set nat destination rule 100 destination port '80'\n"
+ "set nat destination rule 100 inbound-interface name 'eth2'\n"
+ "set nat destination rule 100 log\n"
+ "set nat destination rule 100 protocol 'tcp'\n"
+ "set nat destination rule 100 translation address '192.168.1.10'\n"
+ "set nat destination rule 100 translation port '8080'"
+ )
+ set_module_args(dict(running_config=parsed_str, state="parsed"))
+ result = self.execute_module(changed=False)
+ parsed_list = {
+ "nat": {
+ "destination": {
+ "rule": [
+ {
+ "id": 100,
+ "description": "Web server NAT",
+ "protocol": "tcp",
+ "log": True,
+ "inbound_interface": {"name": "eth2"},
+ "destination": {"address": "198.51.100.10", "port": "80"},
+ "translation": {"address": "192.168.1.10", "port": "8080"},
+ },
+ ],
+ },
+ },
+ }
+ self.assertEqual(parsed_list, result["parsed"])
+
+ # -------------------------------------------------------------------------
+ # gathered
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_gathered(self):
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False)
+ gathered = result["gathered"]
+ self.assertIn("nat", gathered)
+ self.assertIn("nat64", gathered)
+ self.assertIn("nat66", gathered)
+ self.assertEqual(gathered["nat"]["destination"]["rule"][0]["id"], 100)
+ self.assertEqual(gathered["nat64"]["source"]["rule"][0]["id"], 10)
+ self.assertEqual(gathered["nat66"]["destination"]["rule"][0]["id"], 20)
+
+ def test_vyos_nat_merged_load_balance_new_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=110,
+ protocol="tcp",
+ destination=dict(port="443"),
+ load_balance=dict(
+ hash=["random"],
+ backend=[
+ dict(ip="10.20.20.1", weight=100),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat destination rule 110 protocol tcp",
+ "set nat destination rule 110 destination port 443",
+ "set nat destination rule 110 load-balance hash random",
+ "set nat destination rule 110 load-balance backend 10.20.20.1 weight 100",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_load_balance_hash_add(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=105,
+ protocol="tcp",
+ destination=dict(port="80"),
+ inbound_interface=dict(name="eth0"),
+ load_balance=dict(
+ hash=[
+ "source-address",
+ "destination-address",
+ "source-port",
+ ],
+ backend=[
+ dict(ip="10.10.10.1", weight=60),
+ dict(ip="10.10.10.2", weight=40),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = ["set nat destination rule 105 load-balance hash source-port"]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_load_balance_backend_weight_change(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=105,
+ protocol="tcp",
+ destination=dict(port="80"),
+ inbound_interface=dict(name="eth0"),
+ load_balance=dict(
+ hash=["source-address", "destination-address"],
+ backend=[
+ dict(ip="10.10.10.1", weight=70),
+ dict(ip="10.10.10.2", weight=30),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat destination rule 105 load-balance backend 10.10.10.1 weight 70",
+ "set nat destination rule 105 load-balance backend 10.10.10.2 weight 30",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_replaced_load_balance_removes_omitted(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=105,
+ protocol="tcp",
+ destination=dict(port="80"),
+ inbound_interface=dict(name="eth0"),
+ load_balance=dict(
+ hash=["source-address"],
+ backend=[
+ dict(ip="10.10.10.1", weight=100),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete nat destination rule 105",
+ "set nat destination rule 105 protocol tcp",
+ "set nat destination rule 105 destination port 80",
+ "set nat destination rule 105 inbound-interface name eth0",
+ "set nat destination rule 105 load-balance hash source-address",
+ "set nat destination rule 105 load-balance backend 10.10.10.1 weight 100",
+ ]
+ self.execute_module(changed=True, commands=commands)
diff --git a/tests/unit/modules/network/vyos/test_vyos_ntp_global.py b/tests/unit/modules/network/vyos/test_vyos_ntp_global.py
index b8adeae7..e14eb43f 100644
--- a/tests/unit/modules/network/vyos/test_vyos_ntp_global.py
+++ b/tests/unit/modules/network/vyos/test_vyos_ntp_global.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
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 b0a0f0ff..09fcae61 100644
--- a/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces.py
+++ b/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces14.py b/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces14.py
index d3f8bc38..af1418db 100644
--- a/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces14.py
+++ b/tests/unit/modules/network/vyos/test_vyos_ospf_interfaces14.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_ospfv2.py b/tests/unit/modules/network/vyos/test_vyos_ospfv2.py
index 2620b1cd..51af858e 100644
--- a/tests/unit/modules/network/vyos/test_vyos_ospfv2.py
+++ b/tests/unit/modules/network/vyos/test_vyos_ospfv2.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_ospfv2v14.py b/tests/unit/modules/network/vyos/test_vyos_ospfv2v14.py
index 5c77cb88..58ceb57b 100644
--- a/tests/unit/modules/network/vyos/test_vyos_ospfv2v14.py
+++ b/tests/unit/modules/network/vyos/test_vyos_ospfv2v14.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_ospfv3.py b/tests/unit/modules/network/vyos/test_vyos_ospfv3.py
index bdd93612..0fc83b85 100644
--- a/tests/unit/modules/network/vyos/test_vyos_ospfv3.py
+++ b/tests/unit/modules/network/vyos/test_vyos_ospfv3.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_ping.py b/tests/unit/modules/network/vyos/test_vyos_ping.py
index 28216962..8d2ce595 100644
--- a/tests/unit/modules/network/vyos/test_vyos_ping.py
+++ b/tests/unit/modules/network/vyos/test_vyos_ping.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_prefix_lists.py b/tests/unit/modules/network/vyos/test_vyos_prefix_lists.py
index 3da58f72..bd8ade4b 100644
--- a/tests/unit/modules/network/vyos/test_vyos_prefix_lists.py
+++ b/tests/unit/modules/network/vyos/test_vyos_prefix_lists.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from textwrap import dedent
diff --git a/tests/unit/modules/network/vyos/test_vyos_route_maps.py b/tests/unit/modules/network/vyos/test_vyos_route_maps.py
index ce13dcf2..9ff5e073 100644
--- a/tests/unit/modules/network/vyos/test_vyos_route_maps.py
+++ b/tests/unit/modules/network/vyos/test_vyos_route_maps.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -50,6 +49,18 @@ class TestVyosRouteMapsModule(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.route_maps.route_maps.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.route_maps.route_maps.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(TestVyosRouteMapsModule, self).tearDown()
@@ -176,21 +187,25 @@ class TestVyosRouteMapsModule(TestVyosModule):
self.execute_module(changed=True, commands=commands)
- def test_route_maps_replaced(self):
+ def test_route_maps_extras_merged(self):
set_module_args(
dict(
config=[
dict(
- route_map="test3",
+ route_map="test2",
entries=[
dict(
sequence=1,
action="permit",
+ call="2",
+ continue_sequence=2,
match=dict(
rpki="invalid",
+ interface="eth2",
metric=1,
peer="1.1.1.3",
ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ community=dict(community_list="235"),
),
set=dict(
ipv6_next_hop=dict(
@@ -198,9 +213,17 @@ class TestVyosRouteMapsModule(TestVyosModule):
value="fdda:5cc1:23:4::1f",
),
community=dict(value="internet"),
+ extcommunity_rt="22:11",
+ extcommunity_soo="220:110",
+ extcommunity_bandwidth="100",
+ extcommunity_bandwidth_non_transitive=True,
+ atomic_aggregate=True,
+ aggregator={"ip": "10.20.11.22", "as": "245"},
bgp_extcommunity_rt="22:11",
ip_next_hop="10.20.10.22",
large_community="10:20:21",
+ as_path_prepend="100 200 350",
+ as_path_exclude="150",
local_preference=4,
metric=5,
metric_type="type-2",
@@ -209,6 +232,85 @@ class TestVyosRouteMapsModule(TestVyosModule):
src="10.0.2.15",
tag=4,
weight=4,
+ table=7,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set policy route-map test2 rule 1 action permit",
+ "set policy route-map test2 rule 1 call 2",
+ "set policy route-map test2 rule 1 set bgp-extcommunity-rt 22:11",
+ "set policy route-map test2 rule 1 set ip-next-hop 10.20.10.22",
+ "set policy route-map test2 rule 1 set ipv6-next-hop global fdda:5cc1:23:4::1f",
+ "set policy route-map test2 rule 1 set large-community 10:20:21",
+ "set policy route-map test2 rule 1 set as-path-prepend '100 200 350'",
+ "set policy route-map test2 rule 1 set as-path-exclude 150",
+ "set policy route-map test2 rule 1 set local-preference 4",
+ "set policy route-map test2 rule 1 set metric 5",
+ "set policy route-map test2 rule 1 set metric-type type-2",
+ "set policy route-map test2 rule 1 set origin egp",
+ "set policy route-map test2 rule 1 set originator-id 10.0.2.2",
+ "set policy route-map test2 rule 1 set src 10.0.2.15",
+ "set policy route-map test2 rule 1 set tag 4",
+ "set policy route-map test2 rule 1 set weight 4",
+ "set policy route-map test2 rule 1 set table 7",
+ "set policy route-map test2 rule 1 set community internet",
+ "set policy route-map test2 rule 1 set extcommunity-rt 22:11",
+ "set policy route-map test2 rule 1 set extcommunity-soo 220:110",
+ "set policy route-map test2 rule 1 set extcommunity bandwidth 100",
+ "set policy route-map test2 rule 1 set extcommunity bandwidth-non-transitive",
+ "set policy route-map test2 rule 1 set atomic-aggregate",
+ "set policy route-map test2 rule 1 set aggregator as 245",
+ "set policy route-map test2 rule 1 set aggregator ip 10.20.11.22",
+ "set policy route-map test2 rule 1 match interface eth2",
+ "set policy route-map test2 rule 1 match metric 1",
+ "set policy route-map test2 rule 1 match peer 1.1.1.3",
+ "set policy route-map test2 rule 1 match ipv6 nexthop fdda:5cc1:23:4::1f",
+ "set policy route-map test2 rule 1 match rpki invalid",
+ "set policy route-map test2 rule 1 match community community-list 235",
+ "set policy route-map test2 rule 1 continue 2",
+ ]
+
+ self.execute_module(changed=True, commands=commands)
+
+ def test_route_maps_replaced(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test3",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(
+ rpki="invalid",
+ metric=1,
+ peer="1.1.1.3",
+ ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ ),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="100:100"),
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.22",
+ large_community="10:20:21",
+ local_preference=4,
+ metric=5,
+ metric_type="type-2",
+ origin="egp",
+ originator_id="10.0.2.2",
+ src="fdda:5cc1:23:4::12",
+ tag=4,
+ weight=4,
),
),
],
@@ -220,10 +322,12 @@ class TestVyosRouteMapsModule(TestVyosModule):
commands = [
"delete policy route-map test3 rule 1 match interface eth2",
"set policy route-map test3 rule 1 set ip-next-hop 10.20.10.22",
+ "set policy route-map test3 rule 1 set community 100:100",
"set policy route-map test3 rule 1 set large-community 10:20:21",
"set policy route-map test3 rule 1 set metric-type type-2",
"set policy route-map test3 rule 1 set originator-id 10.0.2.2",
"set policy route-map test3 rule 1 set tag 4",
+ "set policy route-map test3 rule 1 set src fdda:5cc1:23:4::12",
"set policy route-map test3 rule 1 match peer 1.1.1.3",
]
self.execute_module(changed=True, commands=commands)
@@ -328,6 +432,32 @@ class TestVyosRouteMapsModule(TestVyosModule):
]
self.execute_module(changed=True, commands=commands)
+ def test_route_maps__deny_overridden(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test2",
+ entries=[
+ dict(
+ sequence=1,
+ action="deny",
+ match=dict(rpki="invalid", peer="1.1.1.5"),
+ ),
+ ],
+ ),
+ ],
+ state="overridden",
+ ),
+ )
+ commands = [
+ "delete policy route-map test3",
+ "set policy route-map test2 rule 1 action deny",
+ "set policy route-map test2 rule 1 match peer 1.1.1.5",
+ "set policy route-map test2 rule 1 match rpki invalid",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
def test_vyos_route_maps_overridden_idempotent(self):
set_module_args(
dict(
diff --git a/tests/unit/modules/network/vyos/test_vyos_route_maps14.py b/tests/unit/modules/network/vyos/test_vyos_route_maps14.py
new file mode 100644
index 00000000..b60ad44a
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_route_maps14.py
@@ -0,0 +1,716 @@
+# (c) 2021 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_route_maps
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosRouteMapsModule(TestVyosModule):
+ module = vyos_route_maps
+
+ def setUp(self):
+ super(TestVyosRouteMapsModule, 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_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.route_maps.route_maps.Route_mapsFacts.get_config",
+ )
+
+ 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.route_maps.route_maps.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.route_maps.route_maps.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(TestVyosRouteMapsModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_execute_show_command.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ if filename is None:
+ filename = "vyos_route_maps_config_v14.cfg"
+
+ def load_from_file(*args, **kwargs):
+ output = load_fixture(filename)
+ return output
+
+ self.execute_show_command.side_effect = load_from_file
+
+ def test_vyos_route_maps_merged_idempotent(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test3",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(
+ rpki="invalid",
+ interface="eth2",
+ metric=1,
+ peer="1.1.1.2",
+ ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ ),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="internet"),
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.20",
+ local_preference=4,
+ metric=5,
+ metric_type="type-1",
+ origin="egp",
+ originator_id="10.0.2.3",
+ src="10.0.2.15",
+ tag=5,
+ weight=4,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_route_maps_merged(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test2",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(
+ rpki="invalid",
+ interface="eth2",
+ metric=1,
+ peer="1.1.1.3",
+ ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ ),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="internet"),
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.22",
+ large_community="10:20:21",
+ local_preference=4,
+ metric=5,
+ metric_type="type-2",
+ origin="egp",
+ originator_id="10.0.2.2",
+ src="10.0.2.15",
+ tag=4,
+ weight=4,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set policy route-map test2 rule 1 action permit",
+ "set policy route-map test2 rule 1 set bgp-extcommunity-rt 22:11",
+ "set policy route-map test2 rule 1 set ip-next-hop 10.20.10.22",
+ "set policy route-map test2 rule 1 set ipv6-next-hop global fdda:5cc1:23:4::1f",
+ "set policy route-map test2 rule 1 set large-community replace 10:20:21",
+ "set policy route-map test2 rule 1 set local-preference 4",
+ "set policy route-map test2 rule 1 set metric 5",
+ "set policy route-map test2 rule 1 set metric-type type-2",
+ "set policy route-map test2 rule 1 set origin egp",
+ "set policy route-map test2 rule 1 set originator-id 10.0.2.2",
+ "set policy route-map test2 rule 1 set src 10.0.2.15",
+ "set policy route-map test2 rule 1 set tag 4",
+ "set policy route-map test2 rule 1 set weight 4",
+ "set policy route-map test2 rule 1 set community replace internet",
+ "set policy route-map test2 rule 1 match interface eth2",
+ "set policy route-map test2 rule 1 match metric 1",
+ "set policy route-map test2 rule 1 match peer 1.1.1.3",
+ "set policy route-map test2 rule 1 match ipv6 nexthop fdda:5cc1:23:4::1f",
+ "set policy route-map test2 rule 1 match rpki invalid",
+ ]
+
+ self.execute_module(changed=True, commands=commands)
+
+ def test_route_maps_extras_merged(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test2",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ call="2",
+ continue_sequence=2,
+ match=dict(
+ rpki="invalid",
+ interface="eth2",
+ metric=1,
+ peer="1.1.1.3",
+ ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ community=dict(community_list="235"),
+ protocol="bgp",
+ ),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="internet"),
+ extcommunity_rt="22:11",
+ extcommunity_soo="220:110",
+ extcommunity_bandwidth="100",
+ extcommunity_bandwidth_non_transitive=True,
+ atomic_aggregate=True,
+ aggregator={"ip": "10.20.11.22", "as": "245"},
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.22",
+ large_community="10:20:21",
+ as_path_prepend="100 200 350",
+ as_path_exclude="150",
+ local_preference=4,
+ metric=5,
+ metric_type="type-2",
+ origin="egp",
+ originator_id="10.0.2.2",
+ src="10.0.2.15",
+ tag=4,
+ weight=4,
+ table=7,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="merged",
+ ),
+ )
+ commands = [
+ "set policy route-map test2 rule 1 action permit",
+ "set policy route-map test2 rule 1 call 2",
+ "set policy route-map test2 rule 1 set bgp-extcommunity-rt 22:11",
+ "set policy route-map test2 rule 1 set ip-next-hop 10.20.10.22",
+ "set policy route-map test2 rule 1 set ipv6-next-hop global fdda:5cc1:23:4::1f",
+ "set policy route-map test2 rule 1 set large-community replace 10:20:21",
+ "set policy route-map test2 rule 1 set as-path prepend '100 200 350'",
+ "set policy route-map test2 rule 1 set as-path exclude 150",
+ "set policy route-map test2 rule 1 set local-preference 4",
+ "set policy route-map test2 rule 1 set metric 5",
+ "set policy route-map test2 rule 1 set metric-type type-2",
+ "set policy route-map test2 rule 1 set origin egp",
+ "set policy route-map test2 rule 1 set originator-id 10.0.2.2",
+ "set policy route-map test2 rule 1 set src 10.0.2.15",
+ "set policy route-map test2 rule 1 set tag 4",
+ "set policy route-map test2 rule 1 set weight 4",
+ "set policy route-map test2 rule 1 set table 7",
+ "set policy route-map test2 rule 1 set community replace internet",
+ "set policy route-map test2 rule 1 set extcommunity rt 22:11",
+ "set policy route-map test2 rule 1 set extcommunity soo 220:110",
+ "set policy route-map test2 rule 1 set extcommunity bandwidth 100",
+ "set policy route-map test2 rule 1 set extcommunity bandwidth-non-transitive",
+ "set policy route-map test2 rule 1 set atomic-aggregate",
+ "set policy route-map test2 rule 1 set aggregator as 245",
+ "set policy route-map test2 rule 1 set aggregator ip 10.20.11.22",
+ "set policy route-map test2 rule 1 match interface eth2",
+ "set policy route-map test2 rule 1 match metric 1",
+ "set policy route-map test2 rule 1 match peer 1.1.1.3",
+ "set policy route-map test2 rule 1 match ipv6 nexthop fdda:5cc1:23:4::1f",
+ "set policy route-map test2 rule 1 match rpki invalid",
+ "set policy route-map test2 rule 1 match protocol bgp",
+ "set policy route-map test2 rule 1 match community community-list 235",
+ "set policy route-map test2 rule 1 continue 2",
+ ]
+
+ self.execute_module(changed=True, commands=commands)
+
+ def test_route_maps_replaced(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test3",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(
+ rpki="invalid",
+ metric=1,
+ peer="1.1.1.3",
+ ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ ),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="100:100"),
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.22",
+ large_community="10:20:21",
+ local_preference=4,
+ metric=5,
+ metric_type="type-2",
+ origin="egp",
+ originator_id="10.0.2.2",
+ src="fdda:5cc1:23:4::12",
+ tag=4,
+ weight=4,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete policy route-map test3 rule 1 match interface eth2",
+ "set policy route-map test3 rule 1 set ip-next-hop 10.20.10.22",
+ "set policy route-map test3 rule 1 set community replace 100:100",
+ "set policy route-map test3 rule 1 set large-community replace 10:20:21",
+ "set policy route-map test3 rule 1 set metric-type type-2",
+ "set policy route-map test3 rule 1 set originator-id 10.0.2.2",
+ "set policy route-map test3 rule 1 set tag 4",
+ "set policy route-map test3 rule 1 set src fdda:5cc1:23:4::12",
+ "set policy route-map test3 rule 1 match peer 1.1.1.3",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_route_maps_replaced_idempotent(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test3",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(
+ rpki="invalid",
+ interface="eth2",
+ metric=1,
+ peer="1.1.1.2",
+ ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ ),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="internet"),
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.20",
+ local_preference=4,
+ metric=5,
+ metric_type="type-1",
+ origin="egp",
+ originator_id="10.0.2.3",
+ src="10.0.2.15",
+ tag=5,
+ weight=4,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="replaced",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_route_maps_overridden(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test2",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(rpki="invalid", peer="1.1.1.3"),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="internet"),
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.22",
+ large_community="10:20:21",
+ local_preference=4,
+ metric=5,
+ metric_type="type-2",
+ origin="egp",
+ originator_id="10.0.2.2",
+ src="10.0.2.15",
+ tag=4,
+ weight=4,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="overridden",
+ ),
+ )
+ commands = [
+ "delete policy route-map test3",
+ "set policy route-map test2 rule 1 action permit",
+ "set policy route-map test2 rule 1 set bgp-extcommunity-rt 22:11",
+ "set policy route-map test2 rule 1 set ip-next-hop 10.20.10.22",
+ "set policy route-map test2 rule 1 set ipv6-next-hop global fdda:5cc1:23:4::1f",
+ "set policy route-map test2 rule 1 set large-community replace 10:20:21",
+ "set policy route-map test2 rule 1 set local-preference 4",
+ "set policy route-map test2 rule 1 set metric 5",
+ "set policy route-map test2 rule 1 set metric-type type-2",
+ "set policy route-map test2 rule 1 set origin egp",
+ "set policy route-map test2 rule 1 set originator-id 10.0.2.2",
+ "set policy route-map test2 rule 1 set src 10.0.2.15",
+ "set policy route-map test2 rule 1 set tag 4",
+ "set policy route-map test2 rule 1 set weight 4",
+ "set policy route-map test2 rule 1 set community replace internet",
+ "set policy route-map test2 rule 1 match peer 1.1.1.3",
+ "set policy route-map test2 rule 1 match rpki invalid",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_route_maps__deny_overridden(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test2",
+ entries=[
+ dict(
+ sequence=1,
+ action="deny",
+ match=dict(rpki="invalid", peer="1.1.1.5"),
+ ),
+ ],
+ ),
+ ],
+ state="overridden",
+ ),
+ )
+ commands = [
+ "delete policy route-map test3",
+ "set policy route-map test2 rule 1 action deny",
+ "set policy route-map test2 rule 1 match peer 1.1.1.5",
+ "set policy route-map test2 rule 1 match rpki invalid",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_route_maps_overridden_idempotent(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test3",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(
+ rpki="invalid",
+ interface="eth2",
+ metric=1,
+ peer="1.1.1.2",
+ ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ ),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="internet"),
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.20",
+ local_preference=4,
+ metric=5,
+ metric_type="type-1",
+ origin="egp",
+ originator_id="10.0.2.3",
+ src="10.0.2.15",
+ tag=5,
+ weight=4,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="overridden",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_route_maps_rendered(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test3",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(
+ rpki="invalid",
+ interface="eth2",
+ metric=1,
+ peer="1.1.1.2",
+ ipv6=dict(next_hop="fdda:5cc1:23:4::1f"),
+ ),
+ set=dict(
+ ipv6_next_hop=dict(
+ ip_type="global",
+ value="fdda:5cc1:23:4::1f",
+ ),
+ community=dict(value="internet"),
+ bgp_extcommunity_rt="22:11",
+ ip_next_hop="10.20.10.20",
+ local_preference=4,
+ metric=5,
+ metric_type="type-1",
+ origin="egp",
+ originator_id="10.0.2.3",
+ src="10.0.2.15",
+ tag=5,
+ weight=4,
+ ),
+ ),
+ ],
+ ),
+ dict(
+ route_map="test1",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ description="test",
+ on_match=dict(next=True),
+ ),
+ dict(
+ sequence=2,
+ action="permit",
+ on_match=dict(goto=4),
+ ),
+ ],
+ ),
+ ],
+ state="rendered",
+ ),
+ )
+ rendered_cmds = [
+ "set policy route-map test3 rule 1 action permit",
+ "set policy route-map test3 rule 1 set bgp-extcommunity-rt 22:11",
+ "set policy route-map test3 rule 1 set ip-next-hop 10.20.10.20",
+ "set policy route-map test3 rule 1 set ipv6-next-hop global fdda:5cc1:23:4::1f",
+ "set policy route-map test3 rule 1 set local-preference 4",
+ "set policy route-map test3 rule 1 set metric 5",
+ "set policy route-map test3 rule 1 set metric-type type-1",
+ "set policy route-map test3 rule 1 set origin egp",
+ "set policy route-map test3 rule 1 set originator-id 10.0.2.3",
+ "set policy route-map test3 rule 1 set src 10.0.2.15",
+ "set policy route-map test3 rule 1 set tag 5",
+ "set policy route-map test3 rule 1 set weight 4",
+ "set policy route-map test3 rule 1 set community replace internet",
+ "set policy route-map test3 rule 1 match interface eth2",
+ "set policy route-map test3 rule 1 match metric 1",
+ "set policy route-map test3 rule 1 match peer 1.1.1.2",
+ "set policy route-map test3 rule 1 match ipv6 nexthop fdda:5cc1:23:4::1f",
+ "set policy route-map test3 rule 1 match rpki invalid",
+ "set policy route-map test1 rule 1 description test",
+ "set policy route-map test1 rule 1 action permit",
+ "set policy route-map test1 rule 1 on-match next",
+ "set policy route-map test1 rule 2 action permit",
+ "set policy route-map test1 rule 2 on-match goto 4",
+ ]
+ result = self.execute_module(changed=False)
+ self.assertEqual(
+ sorted(result["rendered"]),
+ sorted(rendered_cmds),
+ result["rendered"],
+ )
+
+ def test_yos_route_maps_parsed(self):
+ parsed_str = (
+ "set policy route-map test3 rule 1 action 'permit'"
+ "\nset policy route-map test3 rule 1 match interface 'eth2'\nset policy route-map test3 rule 1 match ipv6 nexthop"
+ " 'fdda:5cc1:23:4::1f'\nset policy route-map test3 rule 1 match metric '1'\nset policy route-map test3 rule 1 match peer "
+ "'1.1.1.2'\nset policy route-map test3 rule 1 match rpki 'invalid'\nset policy route-map test3 rule 1 set bgp-extcommunity-rt "
+ "'22:11'\nset policy route-map test3 rule 1 set community replace 'internet'\nset policy route-map test3 rule 1 set ipv6-next-hop global"
+ " 'fdda:5cc1:23:4::1f'\nset policy route-map test3 rule 1 set ip-next-hop '10.20.10.20'\nset policy route-map "
+ "test3 rule 1 set local-preference '4'\nset policy route-map test3 rule 1 set metric '5'\nset policy route-map test3 "
+ "rule 1 set metric-type 'type-1'\nset policy route-map test3 rule 1 set origin 'egp'\nset policy route-map test3 rule 1 set originator-id "
+ "'10.0.2.3'\nset policy route-map test3 rule 1 set src '10.0.2.15'"
+ "\nset policy route-map test3 rule 1 set tag '5'\nset policy route-map test3 rule 1 set weight '4'"
+ )
+ set_module_args(dict(running_config=parsed_str, state="parsed"))
+ result = self.execute_module(changed=False)
+ parsed_list = [
+ {
+ "entries": [
+ {
+ "action": "permit",
+ "match": {
+ "interface": "eth2",
+ "ipv6": {"next_hop": "fdda:5cc1:23:4::1f"},
+ "metric": 1,
+ "peer": "1.1.1.2",
+ "rpki": "invalid",
+ },
+ "sequence": 1,
+ "set": {
+ "bgp_extcommunity_rt": "22:11",
+ "community": {"value": "internet"},
+ "ip_next_hop": "10.20.10.20",
+ "ipv6_next_hop": {
+ "ip_type": "global",
+ "value": "fdda:5cc1:23:4::1f",
+ },
+ "local_preference": "4",
+ "metric": "5",
+ "metric_type": "type-1",
+ "origin": "egp",
+ "originator_id": "10.0.2.3",
+ "src": "10.0.2.15",
+ "tag": "5",
+ "weight": "4",
+ },
+ },
+ ],
+ "route_map": "test3",
+ },
+ ]
+ self.assertEqual(parsed_list, result["parsed"])
+
+ def test_vyos_route_maps_gathered(self):
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False)
+ gathered_list = [
+ {
+ "entries": [
+ {
+ "action": "permit",
+ "match": {
+ "interface": "eth2",
+ "ipv6": {"next_hop": "fdda:5cc1:23:4::1f"},
+ "metric": 1,
+ "peer": "1.1.1.2",
+ "rpki": "invalid",
+ },
+ "sequence": 1,
+ "set": {
+ "bgp_extcommunity_rt": "22:11",
+ "community": {"value": "internet"},
+ "ip_next_hop": "10.20.10.20",
+ "ipv6_next_hop": {
+ "ip_type": "global",
+ "value": "fdda:5cc1:23:4::1f",
+ },
+ "local_preference": "4",
+ "metric": "5",
+ "metric_type": "type-1",
+ "origin": "egp",
+ "originator_id": "10.0.2.3",
+ "src": "10.0.2.15",
+ "tag": "5",
+ "weight": "4",
+ },
+ },
+ ],
+ "route_map": "test3",
+ },
+ ]
+ self.assertEqual(gathered_list, result["gathered"])
+
+ def test_vyos_route_maps_deleted(self):
+ set_module_args(
+ dict(
+ config=[
+ dict(
+ route_map="test3",
+ entries=[
+ dict(
+ sequence=1,
+ action="permit",
+ match=dict(
+ rpki="invalid",
+ interface="eth2",
+ ),
+ set=dict(
+ origin="egp",
+ originator_id="10.0.2.3",
+ src="10.0.2.15",
+ tag=5,
+ weight=4,
+ ),
+ ),
+ ],
+ ),
+ ],
+ state="deleted",
+ ),
+ )
+ commands = ["delete policy route-map test3"]
+ self.execute_module(changed=True, commands=commands)
diff --git a/tests/unit/modules/network/vyos/test_vyos_snmp_server.py b/tests/unit/modules/network/vyos/test_vyos_snmp_server.py
index 9ac650bf..91abb69f 100644
--- a/tests/unit/modules/network/vyos/test_vyos_snmp_server.py
+++ b/tests/unit/modules/network/vyos/test_vyos_snmp_server.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_static_routes.py b/tests/unit/modules/network/vyos/test_vyos_static_routes.py
index 71f1fbf1..955c997a 100644
--- a/tests/unit/modules/network/vyos/test_vyos_static_routes.py
+++ b/tests/unit/modules/network/vyos/test_vyos_static_routes.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_static_routes14.py b/tests/unit/modules/network/vyos/test_vyos_static_routes14.py
index e5e40823..2dd4c0aa 100644
--- a/tests/unit/modules/network/vyos/test_vyos_static_routes14.py
+++ b/tests/unit/modules/network/vyos/test_vyos_static_routes14.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
diff --git a/tests/unit/modules/network/vyos/test_vyos_system.py b/tests/unit/modules/network/vyos/test_vyos_system.py
index cf405cab..56874cba 100644
--- a/tests/unit/modules/network/vyos/test_vyos_system.py
+++ b/tests/unit/modules/network/vyos/test_vyos_system.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from unittest.mock import patch
@@ -45,11 +44,26 @@ class TestVyosSystemModule(TestVyosModule):
)
self.load_config = self.mock_load_config.start()
+ self.mock_get_os_version = patch(
+ "ansible_collections.vyos.vyos.plugins.modules.vyos_system.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.modules.vyos_system.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(TestVyosSystemModule, self).tearDown()
self.mock_get_config.stop()
self.mock_load_config.stop()
+ self.mock_get_os_version.stop()
+ self.mock_facts_get_os_version.stop()
def load_fixtures(self, commands=None, filename=None):
self.get_config.return_value = load_fixture("vyos_config_config.cfg")
@@ -112,3 +126,67 @@ class TestVyosSystemModule(TestVyosModule):
"delete system name-server",
]
self.execute_module(changed=True, commands=commands)
+
+
+class TestVyosSystemModule14(TestVyosModule):
+ module = vyos_system
+
+ def setUp(self):
+ super(TestVyosSystemModule14, self).setUp()
+
+ self.mock_get_config = patch(
+ "ansible_collections.vyos.vyos.plugins.modules.vyos_system.get_config",
+ )
+ self.get_config = self.mock_get_config.start()
+
+ self.mock_load_config = patch(
+ "ansible_collections.vyos.vyos.plugins.modules.vyos_system.load_config",
+ )
+ self.load_config = self.mock_load_config.start()
+
+ self.mock_get_os_version = patch(
+ "ansible_collections.vyos.vyos.plugins.modules.vyos_system.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.modules.vyos_system.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(TestVyosSystemModule14, self).tearDown()
+
+ self.mock_get_config.stop()
+ self.mock_load_config.stop()
+ self.mock_get_os_version.stop()
+ self.mock_facts_get_os_version.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ self.get_config.return_value = load_fixture("vyos_config_config.cfg")
+
+ def test_vyos_system_domain_search(self):
+ set_module_args(dict(domain_search=["foo.example.com", "bar.example.com"]))
+ commands = [
+ "set system domain-search 'foo.example.com'",
+ "set system domain-search 'bar.example.com'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_system_clear_domain_search(self):
+ set_module_args(dict(domain_search=[]))
+ commands = ["delete system domain-search"]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_system_clear_all(self):
+ set_module_args(dict(state="absent"))
+ commands = [
+ "delete system host-name",
+ "delete system domain-search",
+ "delete system domain-name",
+ "delete system name-server",
+ ]
+ self.execute_module(changed=True, commands=commands)
diff --git a/tests/unit/modules/network/vyos/test_vyos_user.py b/tests/unit/modules/network/vyos/test_vyos_user.py
index e8c50783..27ab1f38 100644
--- a/tests/unit/modules/network/vyos/test_vyos_user.py
+++ b/tests/unit/modules/network/vyos/test_vyos_user.py
@@ -62,6 +62,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)
@@ -141,6 +171,39 @@ class TestVyosUserModule(TestVyosModule):
],
)
+ def test_vyos_user_set_security_keys(self):
+ set_module_args(
+ dict(
+ name="ansible",
+ public_keys=[
+ dict(
+ name="ecdsa@host",
+ key="AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTY",
+ type="sk-ecdsa-sha2-nistp256@openssh.com",
+ ),
+ dict(
+ name="ed25519@host",
+ key="AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFIR0jrMvBdmvTJNY5EDhOD",
+ type="sk-ssh-ed25519@openssh.com",
+ ),
+ ],
+ ),
+ )
+ result = self.execute_module(changed=True)
+ self.assertEqual(
+ result["commands"],
+ [
+ "set system login user ansible authentication public-keys "
+ "ecdsa@host key 'AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTY'",
+ "set system login user ansible authentication public-keys ecdsa@host "
+ "type 'sk-ecdsa-sha2-nistp256@openssh.com'",
+ "set system login user ansible authentication public-keys ed25519@host "
+ "key 'AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFIR0jrMvBdmvTJNY5EDhOD'",
+ "set system login user ansible authentication public-keys ed25519@host "
+ "type 'sk-ssh-ed25519@openssh.com'",
+ ],
+ )
+
def test_vyos_user_set_ssh_key_idempotent(self):
set_module_args(
dict(
@@ -237,3 +300,43 @@ class TestVyosUserModule(TestVyosModule):
),
)
result = self.execute_module(changed=False)
+
+ def test_vyos_user_aggregate_with_public_keys(self):
+ set_module_args(
+ dict(
+ aggregate=[
+ dict(
+ name="user1",
+ public_keys=[
+ dict(
+ name="user1@host1",
+ key="AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2u",
+ type="ssh-ed25519",
+ ),
+ ],
+ ),
+ dict(
+ name="user2",
+ public_keys=[
+ dict(
+ name="user2@host2",
+ key="AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2u",
+ type="ssh-ed25519",
+ ),
+ ],
+ ),
+ ],
+ ),
+ )
+ result = self.execute_module(changed=True)
+ self.assertEqual(
+ sorted(result["commands"]),
+ sorted(
+ [
+ "set system login user user1 authentication public-keys user1@host1 key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2u'",
+ "set system login user user1 authentication public-keys user1@host1 type 'ssh-ed25519'",
+ "set system login user user2 authentication public-keys user2@host2 key 'AAAAC3NzaC1lZDI1NTE5AAAAIFIR0jrMvBdmvTJNY5EDhOD+eixvbOinhY1eBU2u'",
+ "set system login user user2 authentication public-keys user2@host2 type 'ssh-ed25519'",
+ ],
+ ),
+ )
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")
diff --git a/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec.py b/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec.py
new file mode 100644
index 00000000..6c616ad1
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec.py
@@ -0,0 +1,568 @@
+#
+# (c) 2026, Red Hat, inc
+# 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
+
+from textwrap import dedent
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_vpn_ipsec
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosVpnIpsecModule(TestVyosModule):
+ module = vyos_vpn_ipsec
+
+ def setUp(self):
+ super(TestVyosVpnIpsecModule, 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_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.vpn_ipsec.vpn_ipsec.Vpn_ipsecFacts.get_vpn_ipsec_data",
+ )
+ self.execute_show_command = self.mock_execute_show_command.start()
+
+ def tearDown(self):
+ super(TestVyosVpnIpsecModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_execute_show_command.stop()
+
+ # Single fixture file, reused across every test. Carries: two
+ # ike_group entries (IKE-TEST richly configured incl.
+ # dead_peer_detection/disable_mobike, IKE-EXTRA minimal, to prove
+ # "unlisted items preserved"); esp_group with compression+proposal;
+ # psk/ppk with multi-value id lists; a profile; and every top-level
+ # singleton field (disable_uniqreqids, interface, log, options).
+ def load_fixtures(self, commands=None, filename=None):
+ if filename == "EMPTY":
+ self.execute_show_command.side_effect = None
+ self.execute_show_command.return_value = ""
+ return
+
+ if filename is None:
+ filename = "vyos_vpn_ipsec_config.cfg"
+
+ def load_from_file(*args, **kwargs):
+ return load_fixture(filename)
+
+ self.execute_show_command.side_effect = load_from_file
+
+ # -------------------------------------------------------------------
+ # merged
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_merged_idempotent(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ compression=True,
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ike_group=[
+ dict(
+ name="IKE-TEST",
+ key_exchange="ikev2",
+ disable_mobike=True,
+ dead_peer_detection=dict(action="restart", interval=15, timeout=60),
+ proposal=[
+ dict(proposal_id=1, encryption="aes256", hash="sha256", dh_group=14),
+ ],
+ ),
+ ],
+ ),
+ state="merged",
+ )
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), [])
+
+ def test_vyos_vpn_ipsec_merged_new_group_leaves_existing_untouched(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-NEW",
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ),
+ state="merged",
+ )
+ compare_cmds = [
+ "set vpn ipsec esp-group ESP-NEW",
+ "set vpn ipsec esp-group ESP-NEW proposal 1",
+ "set vpn ipsec esp-group ESP-NEW proposal 1 encryption aes256",
+ "set vpn ipsec esp-group ESP-NEW proposal 1 hash sha256",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_bool_field_toggle(self):
+ # disable_mobike explicitly set to True in want when have already
+ # has it True -> no-op; here we flip a different bool
+ # (compression on ESP-TEST is already True in have) by instead
+ # adding a brand new bool-bearing field: options.virtual_ip,
+ # entirely absent from have.
+ playbook = dict(config=dict(options=dict(virtual_ip=True)), state="merged")
+ compare_cmds = ["set vpn ipsec options virtual-ip"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_add_psk_id(self):
+ # multi-value list field: add one id, existing ids must survive
+ playbook = dict(
+ config=dict(
+ authentication=dict(
+ psk=[dict(name="PSK-TEST", id=["third@example.com"])],
+ ),
+ ),
+ state="merged",
+ )
+ compare_cmds = ["set vpn ipsec authentication psk PSK-TEST id third@example.com"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_create_ppk(self):
+ playbook = dict(
+ config=dict(
+ authentication=dict(
+ ppk=[dict(name="PPK-NEW", id=["new-ppk-id"], secret="new-secret")],
+ ),
+ ),
+ state="merged",
+ )
+ compare_cmds = [
+ "set vpn ipsec authentication ppk PPK-NEW",
+ "set vpn ipsec authentication ppk PPK-NEW id new-ppk-id",
+ "set vpn ipsec authentication ppk PPK-NEW secret 'new-secret'",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_profile_add_bind_tunnel(self):
+ # multi-value list field on an EXISTING profile: add one tunnel,
+ # existing tun0 must survive
+ playbook = dict(
+ config=dict(
+ profile=[dict(name="testprofile", bind_tunnel=["tun1"])],
+ ),
+ state="merged",
+ )
+ compare_cmds = ["set vpn ipsec profile testprofile bind tunnel tun1"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_create_profile_from_scratch(self):
+ # Regression test: creating a brand new profile must correctly
+ # emit its authentication.mode/pre_shared_secret commands. This
+ # specific path was silently broken (a leftover dotted-Jinja
+ # setval caused render() to return nothing, so addcmd() quietly
+ # dropped both commands with no error) -- caught only by a real
+ # device integration test, since no existing unit test actually
+ # created a profile from scratch or touched its authentication
+ # sub-dict through generate_commands().
+ playbook = dict(
+ config=dict(
+ profile=[
+ dict(
+ name="NEWPROFILE",
+ authentication=dict(
+ mode="pre-shared-secret",
+ pre_shared_secret="brand-new-secret",
+ ),
+ esp_group="ESP-TEST",
+ ike_group="IKE-TEST",
+ ),
+ ],
+ ),
+ state="merged",
+ )
+ compare_cmds = [
+ "set vpn ipsec profile NEWPROFILE",
+ "set vpn ipsec profile NEWPROFILE authentication mode pre-shared-secret",
+ "set vpn ipsec profile NEWPROFILE authentication pre-shared-secret 'brand-new-secret'",
+ "set vpn ipsec profile NEWPROFILE esp-group ESP-TEST",
+ "set vpn ipsec profile NEWPROFILE ike-group IKE-TEST",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # replaced
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_replaced_only_named_item_touched(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ proposal=[dict(proposal_id=1, encryption="aes128", hash="sha256")],
+ ),
+ ],
+ ),
+ state="replaced",
+ )
+ compare_cmds = [
+ # compression omitted from want -> reconciled away (attribute
+ # inside the selected instance is always reset under replaced)
+ "delete vpn ipsec esp-group ESP-TEST compression",
+ "set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes128",
+ # ike_group/profile/psk/ppk not named at all -> left alone.
+ # Top-level singleton fields (no instance scope) are always
+ # reconciled under replaced/overridden regardless of what's
+ # named elsewhere.
+ "delete vpn ipsec disable-uniqreqids",
+ "delete vpn ipsec interface eth0",
+ "delete vpn ipsec interface eth1",
+ "delete vpn ipsec log level 1",
+ "delete vpn ipsec log subsystem chd",
+ "delete vpn ipsec log subsystem ike",
+ "delete vpn ipsec options flexvpn",
+ "delete vpn ipsec options retransmission attempts 3",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_replaced_idempotent(self):
+ # want fully mirrors the ENTIRE fixture state for every top-level
+ # singleton field plus the one named esp_group -- proves
+ # replaced's per-item/per-field reconciliation doesn't spuriously
+ # touch a fully-matching configuration.
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ compression=True,
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ disable_uniqreqids=True,
+ interface=["eth0", "eth1"],
+ log=dict(level=1, subsystem=["chd", "ike"]),
+ options=dict(flexvpn=True, retransmission=dict(attempts=3)),
+ ),
+ state="replaced",
+ )
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), [])
+
+ def test_vyos_vpn_ipsec_replaced_scoped_dead_peer_detection(self):
+ # naming ike_group under replaced without dead_peer_detection ->
+ # every DPD field reconciled away (nested-dict attribute reset)
+ playbook = dict(
+ config=dict(
+ ike_group=[dict(name="IKE-TEST", key_exchange="ikev2")],
+ ),
+ state="replaced",
+ )
+ compare_cmds = [
+ "delete vpn ipsec ike-group IKE-TEST dead-peer-detection action restart",
+ "delete vpn ipsec ike-group IKE-TEST dead-peer-detection interval 15",
+ "delete vpn ipsec ike-group IKE-TEST dead-peer-detection timeout 60",
+ "delete vpn ipsec ike-group IKE-TEST disable-mobike",
+ "delete vpn ipsec ike-group IKE-TEST proposal 1",
+ "delete vpn ipsec disable-uniqreqids",
+ "delete vpn ipsec interface eth0",
+ "delete vpn ipsec interface eth1",
+ "delete vpn ipsec log level 1",
+ "delete vpn ipsec log subsystem chd",
+ "delete vpn ipsec log subsystem ike",
+ "delete vpn ipsec options flexvpn",
+ "delete vpn ipsec options retransmission attempts 3",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # overridden
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_overridden_removes_unlisted_instances(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ proposal=[dict(proposal_id=1, encryption="aes128", hash="sha256")],
+ ),
+ ],
+ ),
+ state="overridden",
+ )
+ compare_cmds = [
+ "delete vpn ipsec esp-group ESP-TEST compression",
+ "set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes128",
+ "delete vpn ipsec ike-group IKE-TEST",
+ "delete vpn ipsec ike-group IKE-EXTRA",
+ "delete vpn ipsec profile testprofile",
+ "delete vpn ipsec authentication psk PSK-TEST",
+ "delete vpn ipsec authentication ppk PPK-TEST",
+ "delete vpn ipsec disable-uniqreqids",
+ "delete vpn ipsec interface eth0",
+ "delete vpn ipsec interface eth1",
+ "delete vpn ipsec log level 1",
+ "delete vpn ipsec log subsystem chd",
+ "delete vpn ipsec log subsystem ike",
+ "delete vpn ipsec options flexvpn",
+ "delete vpn ipsec options retransmission attempts 3",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # deleted -- bare (delete everything)
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_deleted_bare(self):
+ playbook = dict(state="deleted")
+ compare_cmds = [
+ "delete vpn ipsec esp-group ESP-TEST",
+ "delete vpn ipsec ike-group IKE-TEST",
+ "delete vpn ipsec ike-group IKE-EXTRA",
+ "delete vpn ipsec profile testprofile",
+ "delete vpn ipsec authentication psk PSK-TEST",
+ "delete vpn ipsec authentication ppk PPK-TEST",
+ "delete vpn ipsec disable-uniqreqids",
+ "delete vpn ipsec interface eth0",
+ "delete vpn ipsec interface eth1",
+ "delete vpn ipsec log level 1",
+ "delete vpn ipsec log subsystem chd",
+ "delete vpn ipsec log subsystem ike",
+ "delete vpn ipsec options flexvpn",
+ "delete vpn ipsec options retransmission attempts 3",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_deleted_bare_idempotent(self):
+ playbook = dict(state="deleted")
+ set_module_args(playbook)
+ result = self.execute_module(changed=False, filename="EMPTY")
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), [])
+
+ # -------------------------------------------------------------------
+ # deleted -- scoped (delete only what's named)
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_deleted_scoped_named_item_only(self):
+ playbook = dict(config=dict(ike_group=[dict(name="IKE-EXTRA")]), state="deleted")
+ compare_cmds = ["delete vpn ipsec ike-group IKE-EXTRA"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_deleted_scoped_nonexistent_name_idempotent(self):
+ playbook = dict(config=dict(ike_group=[dict(name="IKE-NONEXISTENT")]), state="deleted")
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), [])
+
+ def test_vyos_vpn_ipsec_deleted_scoped_interface_element(self):
+ # element-level list deletion: name only eth0, eth1 must survive
+ playbook = dict(config=dict(interface=["eth0"]), state="deleted")
+ compare_cmds = ["delete vpn ipsec interface eth0"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_deleted_scoped_log_subsystem_element(self):
+ playbook = dict(config=dict(log=dict(subsystem=["chd"])), state="deleted")
+ compare_cmds = ["delete vpn ipsec log subsystem chd"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_deleted_scoped_options_field(self):
+ playbook = dict(config=dict(options=dict(flexvpn=True)), state="deleted")
+ compare_cmds = ["delete vpn ipsec options flexvpn"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # rendered (no device contact -- have is empty regardless of fixture)
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_rendered(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-RENDER-TEST",
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ),
+ state="rendered",
+ )
+ compare_cmds = [
+ "set vpn ipsec esp-group ESP-RENDER-TEST",
+ "set vpn ipsec esp-group ESP-RENDER-TEST proposal 1",
+ "set vpn ipsec esp-group ESP-RENDER-TEST proposal 1 encryption aes256",
+ "set vpn ipsec esp-group ESP-RENDER-TEST proposal 1 hash sha256",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module()
+ self.maxDiff = None
+ self.assertEqual(sorted(result["rendered"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # parsed (reads running_config directly, not the mocked show command)
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_parsed(self):
+ set_module_args(
+ dict(
+ running_config=dedent(
+ """\
+ set vpn ipsec esp-group ESP-PARSE-TEST proposal 1 encryption aes256
+ set vpn ipsec esp-group ESP-PARSE-TEST proposal 1 hash sha256
+ set vpn ipsec ike-group IKE-PARSE-TEST key-exchange ikev2
+ """,
+ ),
+ state="parsed",
+ ),
+ )
+ parsed = dict(
+ esp_group=[
+ dict(
+ name="ESP-PARSE-TEST",
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ike_group=[dict(name="IKE-PARSE-TEST", key_exchange="ikev2")],
+ )
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(result["parsed"], parsed)
+
+ # -------------------------------------------------------------------
+ # gathered
+ # -------------------------------------------------------------------
+
+ def _normalize_scalar_lists(self, node):
+ """Multi-value scalar-list fields (id, subsystem, interface, ...)
+ have no meaningful order -- dict_merge's internal list handling
+ doesn't guarantee a stable sequence between runs. Sort them
+ in-place (recursively) before comparing so tests aren't
+ sensitive to that non-determinism, while list-of-dict
+ collections (already sorted by name in process_facts) are left
+ untouched.
+ """
+ if isinstance(node, dict):
+ for k, v in node.items():
+ if isinstance(v, list) and v and all(not isinstance(i, dict) for i in v):
+ node[k] = sorted(v)
+ else:
+ self._normalize_scalar_lists(v)
+ elif isinstance(node, list):
+ for item in node:
+ self._normalize_scalar_lists(item)
+ return node
+
+ def test_vyos_vpn_ipsec_gathered(self):
+ set_module_args(dict(state="gathered"))
+ gathered = dict(
+ authentication=dict(
+ ppk=[dict(name="PPK-TEST", id=["ppk-id-1"], secret="test-ppk-secret")],
+ psk=[
+ dict(
+ name="PSK-TEST",
+ id=["local@example.com", "remote@example.com"],
+ secret="test-not-real-secret",
+ ),
+ ],
+ ),
+ disable_uniqreqids=True,
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ compression=True,
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ike_group=[
+ dict(name="IKE-EXTRA", key_exchange="ikev1"),
+ dict(
+ name="IKE-TEST",
+ key_exchange="ikev2",
+ disable_mobike=True,
+ dead_peer_detection=dict(action="restart", interval=15, timeout=60),
+ proposal=[
+ dict(proposal_id=1, encryption="aes256", hash="sha256", dh_group=14),
+ ],
+ ),
+ ],
+ interface=["eth0", "eth1"],
+ log=dict(level=1, subsystem=["chd", "ike"]),
+ options=dict(flexvpn=True, retransmission=dict(attempts=3)),
+ profile=[
+ dict(
+ name="testprofile",
+ authentication=dict(
+ mode="pre-shared-secret",
+ pre_shared_secret="test-not-real-secret",
+ ),
+ bind_tunnel=["tun0"],
+ esp_group="ESP-TEST",
+ ike_group="IKE-TEST",
+ ),
+ ],
+ )
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(
+ self._normalize_scalar_lists(result["gathered"]),
+ self._normalize_scalar_lists(gathered),
+ )
diff --git a/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec_s2s.py b/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec_s2s.py
new file mode 100644
index 00000000..014fb867
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec_s2s.py
@@ -0,0 +1,245 @@
+#
+# (c) 2026, Red Hat, inc
+# 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
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_vpn_ipsec_s2s
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosVpnIpsecS2sModule(TestVyosModule):
+ module = vyos_vpn_ipsec_s2s
+
+ def setUp(self):
+ super(TestVyosVpnIpsecS2sModule, 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_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.vpn_ipsec_s2s.vpn_ipsec_s2s.Vpn_ipsec_s2sFacts.get_vpn_ipsec_s2s_data",
+ )
+ self.execute_show_command = self.mock_execute_show_command.start()
+
+ def tearDown(self):
+ super(TestVyosVpnIpsecS2sModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_execute_show_command.stop()
+
+ # Single fixture file, reused across every test. Carries PEER-TEST
+ # (richly configured -- authentication, tunnel, vti, every scalar
+ # type) and PEER-EXTRA (minimal, to prove "unlisted peer preserved
+ # under replaced / removed under overridden").
+ def load_fixtures(self, commands=None, filename=None):
+ if filename is None:
+ filename = "vyos_vpn_ipsec_s2s_config.cfg"
+
+ def load_from_file(*args, **kwargs):
+ return load_fixture(filename)
+
+ self.execute_show_command.side_effect = load_from_file
+
+ # -------------------------------------------------------------------
+ # merged
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_s2s_merged_idempotent(self):
+ playbook = dict(
+ config=dict(
+ peer=[
+ dict(
+ name="PEER-TEST",
+ authentication=dict(
+ local_id="local@example.com",
+ mode="pre-shared-secret",
+ remote_id="remote@example.com",
+ ),
+ childless="prefer",
+ connection_type="initiate",
+ default_esp_group="ESP-TEST",
+ description="test peer for site-to-site module",
+ force_udp_encapsulation=True,
+ ike_group="IKE-TEST",
+ ikev2_reauth="inherit",
+ local_address="any",
+ remote_address=["203.0.113.1"],
+ replay_window=32,
+ virtual_address=["0.0.0.0"],
+ tunnel=[
+ dict(
+ tunnel_id=1,
+ esp_group="ESP-TEST",
+ protocol="tcp",
+ priority=10,
+ local=dict(port=443, prefix=["10.0.0.0/24"]),
+ remote=dict(port=443, prefix=["10.1.0.0/24"]),
+ ),
+ ],
+ vti=dict(
+ bind="vti0",
+ esp_group="ESP-TEST",
+ traffic_selector=dict(
+ local=dict(prefix=["10.2.0.0/24"]),
+ remote=dict(prefix=["10.3.0.0/24"]),
+ ),
+ ),
+ ),
+ ],
+ ),
+ state="merged",
+ )
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), [])
+
+ def test_vyos_vpn_ipsec_s2s_merged_new_peer_leaves_existing_untouched(self):
+ playbook = dict(
+ config=dict(peer=[dict(name="PEER-NEW", ike_group="IKE-TEST")]),
+ state="merged",
+ )
+ compare_cmds = [
+ "set vpn ipsec site-to-site peer PEER-NEW",
+ "set vpn ipsec site-to-site peer PEER-NEW ike-group 'IKE-TEST'",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # replaced
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_s2s_replaced_only_named_peer_touched(self):
+ # Name PEER-TEST fully (mirroring the fixture exactly) except a
+ # changed tunnel priority -- PEER-EXTRA must be left alone.
+ playbook = dict(
+ config=dict(
+ peer=[
+ dict(
+ name="PEER-TEST",
+ authentication=dict(
+ local_id="local@example.com",
+ mode="pre-shared-secret",
+ remote_id="remote@example.com",
+ ),
+ childless="prefer",
+ connection_type="initiate",
+ default_esp_group="ESP-TEST",
+ description="test peer for site-to-site module",
+ force_udp_encapsulation=True,
+ ike_group="IKE-TEST",
+ ikev2_reauth="inherit",
+ local_address="any",
+ remote_address=["203.0.113.1"],
+ replay_window=32,
+ virtual_address=["0.0.0.0"],
+ tunnel=[
+ dict(
+ tunnel_id=1,
+ esp_group="ESP-TEST",
+ protocol="tcp",
+ priority=99,
+ local=dict(port=443, prefix=["10.0.0.0/24"]),
+ remote=dict(port=443, prefix=["10.1.0.0/24"]),
+ ),
+ ],
+ vti=dict(
+ bind="vti0",
+ esp_group="ESP-TEST",
+ traffic_selector=dict(
+ local=dict(prefix=["10.2.0.0/24"]),
+ remote=dict(prefix=["10.3.0.0/24"]),
+ ),
+ ),
+ ),
+ ],
+ ),
+ state="replaced",
+ )
+ compare_cmds = ["set vpn ipsec site-to-site peer PEER-TEST tunnel 1 priority '99'"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # overridden
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_s2s_overridden_removes_unlisted_peer(self):
+ playbook = dict(
+ config=dict(peer=[dict(name="PEER-TEST", ike_group="IKE-TEST")]),
+ state="overridden",
+ )
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertIn("delete vpn ipsec site-to-site peer PEER-EXTRA", result["commands"])
+ # PEER-TEST had far more fields than just ike_group in the
+ # fixture -- overridden must reconcile all of those away too,
+ # since only ike_group was named.
+ self.assertIn(
+ "delete vpn ipsec site-to-site peer PEER-TEST default-esp-group 'ESP-TEST'",
+ result["commands"],
+ )
+
+ # -------------------------------------------------------------------
+ # deleted -- bare and scoped
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_s2s_deleted_bare(self):
+ set_module_args(dict(state="deleted"))
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertIn("delete vpn ipsec site-to-site peer PEER-TEST", result["commands"])
+ self.assertIn("delete vpn ipsec site-to-site peer PEER-EXTRA", result["commands"])
+
+ def test_vyos_vpn_ipsec_s2s_deleted_scoped_named_peer_only(self):
+ set_module_args(dict(config=dict(peer=[dict(name="PEER-EXTRA")]), state="deleted"))
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(result["commands"], ["delete vpn ipsec site-to-site peer PEER-EXTRA"])
+
+ # -------------------------------------------------------------------
+ # rendered / parsed / gathered
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_s2s_rendered(self):
+ playbook = dict(
+ config=dict(peer=[dict(name="PEER-RENDER-TEST", ike_group="IKE-TEST")]),
+ state="rendered",
+ )
+ compare_cmds = [
+ "set vpn ipsec site-to-site peer PEER-RENDER-TEST",
+ "set vpn ipsec site-to-site peer PEER-RENDER-TEST ike-group 'IKE-TEST'",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module()
+ self.maxDiff = None
+ self.assertEqual(sorted(result["rendered"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_s2s_gathered(self):
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ names = sorted(p["name"] for p in result["gathered"]["peer"])
+ self.assertEqual(names, ["PEER-EXTRA", "PEER-TEST"])
diff --git a/tests/unit/modules/network/vyos/test_vyos_vrf.py b/tests/unit/modules/network/vyos/test_vyos_vrf.py
new file mode 100644
index 00000000..aa6284a0
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_vrf.py
@@ -0,0 +1,688 @@
+# (c) 2021 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/>.
+
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_vrf
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosVrfModule(TestVyosModule):
+ module = vyos_vrf
+
+ def setUp(self):
+ super(TestVyosVrfModule, self).setUp()
+
+ self.fake_connection = MagicMock()
+ self.fake_connection.get.return_value = "{}"
+
+ self.fake_connection.get_device_info.return_value = {
+ "version": "1.5",
+ }
+
+ self.fake_connection.get_device_info.return_value = {
+ "network_os_major_version": "1.5",
+ }
+ patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos.get_connection",
+ return_value=self.fake_connection,
+ ).start()
+
+ def _get_resource_connection_side_effect(module, *args, **kwargs):
+ module._connection = self.fake_connection
+ return self.fake_connection
+
+ patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base.get_resource_connection",
+ side_effect=_get_resource_connection_side_effect,
+ ).start()
+
+ patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection",
+ side_effect=_get_resource_connection_side_effect,
+ ).start()
+
+ self.mock_execute_show_command = patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vrf.vrf.VrfFacts.get_config",
+ ).start()
+
+ patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.vrf.vrf.get_os_version",
+ return_value="1.5",
+ ).start()
+
+ patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global.get_os_version",
+ return_value="1.5",
+ ).start()
+
+ self.maxDiff = None
+
+ def tearDown(self):
+ super(TestVyosVrfModule, self).tearDown()
+ patch.stopall()
+
+ def load_fixtures(self, commands=None, filename=None):
+ if filename is None:
+ filename = "vyos_vrf_config.cfg"
+
+ def load_from_file(*args, **kwargs):
+ return load_fixture(filename)
+
+ self.mock_execute_show_command.side_effect = load_from_file
+
+ def test_vrf_merged_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ bind_to_all=True,
+ instances=[
+ dict(
+ name="vrf-blue",
+ description="blue-vrf",
+ disable=False,
+ table_id=100,
+ vni=1000,
+ ),
+ dict(
+ name="vrf-red",
+ description="red-vrf",
+ disable=True,
+ table_id=101,
+ vni=1001,
+ address_family=[
+ dict(
+ afi="ipv4",
+ disable_forwarding=True,
+ route_maps=[
+ dict(rm_name="rm1", protocol="rip"),
+ ],
+ ),
+ dict(
+ afi="ipv6",
+ nht_no_resolve_via_default=True,
+ ),
+ ],
+ protocols=dict(
+ bgp=dict(
+ as_number=65000,
+ neighbor=[
+ dict(
+ address="192.0.2.1",
+ remote_as=65002,
+ ),
+ dict(
+ address="1.1.1.3",
+ remote_as=400,
+ passive=True,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ state="merged",
+ ),
+ )
+
+ self.execute_module(changed=False, commands=[])
+
+ def test_vrf_merged(self):
+ set_module_args(
+ dict(
+ config=dict(
+ bind_to_all=False,
+ instances=[
+ dict(
+ name="vrf-green",
+ description="green-vrf",
+ table_id=110,
+ vni=1010,
+ protocols=dict(
+ ospf=dict(
+ log_adjacency_changes="detail",
+ max_metric=dict(
+ router_lsa=dict(
+ administrative=True,
+ on_shutdown=10,
+ on_startup=10,
+ ),
+ ),
+ default_information=dict(
+ originate=dict(
+ always=True,
+ metric=10,
+ metric_type=2,
+ ),
+ ),
+ auto_cost=dict(
+ reference_bandwidth=2,
+ ),
+ neighbor=[
+ dict(
+ neighbor_id="192.0.11.12",
+ poll_interval=10,
+ priority=2,
+ ),
+ ],
+ redistribute=[
+ dict(
+ route_type="bgp",
+ metric=10,
+ metric_type=2,
+ ),
+ ],
+ parameters=dict(
+ router_id="192.0.1.1",
+ rfc1583_compatibility=True,
+ abr_type="cisco",
+ ),
+ areas=[
+ dict(
+ area_id="2",
+ area_type=dict(
+ normal=True,
+ ),
+ authentication="plaintext-password",
+ shortcut="enable",
+ ),
+ dict(
+ area_id="3",
+ area_type=dict(
+ nssa=dict(
+ set=True,
+ ),
+ ),
+ ),
+ dict(
+ area_id="4",
+ area_type=dict(
+ stub=dict(
+ default_cost=20,
+ ),
+ ),
+ network=[
+ dict(
+ address="192.0.2.0/24",
+ ),
+ ],
+ range=[
+ dict(
+ address="192.0.3.0/24",
+ cost=10,
+ ),
+ dict(
+ address="192.0.4.0/24",
+ cost=12,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ state="merged",
+ ),
+ )
+
+ commands = [
+ "delete vrf bind-to-all",
+ "set vrf name vrf-green table 110",
+ "set vrf name vrf-green vni 1010",
+ "set vrf name vrf-green description green-vrf",
+ "set vrf name vrf-green protocols ospf log-adjacency-changes 'detail'",
+ "set vrf name vrf-green protocols ospf max-metric router-lsa administrative",
+ "set vrf name vrf-green protocols ospf max-metric router-lsa on-shutdown 10",
+ "set vrf name vrf-green protocols ospf max-metric router-lsa on-startup 10",
+ "set vrf name vrf-green protocols ospf default-information originate always",
+ "set vrf name vrf-green protocols ospf default-information originate metric 10",
+ "set vrf name vrf-green protocols ospf default-information originate metric-type 2",
+ "set vrf name vrf-green protocols ospf auto-cost reference-bandwidth '2'",
+ "set vrf name vrf-green protocols ospf neighbor 192.0.11.12",
+ "set vrf name vrf-green protocols ospf neighbor 192.0.11.12 poll-interval 10",
+ "set vrf name vrf-green protocols ospf neighbor 192.0.11.12 priority 2",
+ "set vrf name vrf-green protocols ospf redistribute bgp",
+ "set vrf name vrf-green protocols ospf redistribute bgp metric 10",
+ "set vrf name vrf-green protocols ospf redistribute bgp metric-type 2",
+ "set vrf name vrf-green protocols ospf parameters router-id '192.0.1.1'",
+ "set vrf name vrf-green protocols ospf parameters rfc1583-compatibility",
+ "set vrf name vrf-green protocols ospf parameters abr-type 'cisco'",
+ "set vrf name vrf-green protocols ospf area '2'",
+ "set vrf name vrf-green protocols ospf area 2 area-type normal",
+ "set vrf name vrf-green protocols ospf area 2 authentication plaintext-password",
+ "set vrf name vrf-green protocols ospf area 2 shortcut enable",
+ "set vrf name vrf-green protocols ospf area '3'",
+ "set vrf name vrf-green protocols ospf area 3 area-type nssa",
+ "set vrf name vrf-green protocols ospf area '4'",
+ "set vrf name vrf-green protocols ospf area 4 area-type stub default-cost 20",
+ "set vrf name vrf-green protocols ospf area 4 network 192.0.2.0/24",
+ "set vrf name vrf-green protocols ospf area 4 range 192.0.3.0/24",
+ "set vrf name vrf-green protocols ospf area 4 range 192.0.3.0/24 cost 10",
+ "set vrf name vrf-green protocols ospf area 4 range 192.0.4.0/24",
+ "set vrf name vrf-green protocols ospf area 4 range 192.0.4.0/24 cost 12",
+ ]
+
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrf_replaced(self):
+ set_module_args(
+ dict(
+ config=dict(
+ bind_to_all=True,
+ instances=[
+ dict(
+ name="vrf-blue",
+ description="blue-vrf-replaced",
+ disable=True,
+ table_id=100,
+ vni=1000,
+ ),
+ dict(
+ name="vrf-red",
+ description="red-vrf",
+ disable=True,
+ table_id=101,
+ vni=1001,
+ address_family=[
+ dict(
+ afi="ipv4",
+ disable_forwarding=False,
+ route_maps=[
+ dict(rm_name="rm1", protocol="rip"),
+ ],
+ ),
+ dict(
+ afi="ipv6",
+ nht_no_resolve_via_default=False,
+ ),
+ ],
+ ),
+ ],
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "set vrf name vrf-blue description blue-vrf-replaced",
+ "set vrf name vrf-blue disable",
+ "delete vrf name vrf-red ip disable-forwarding",
+ "delete vrf name vrf-red ipv6 nht no-resolve-via-default",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrf_replaced_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ bind_to_all=True,
+ instances=[
+ dict(
+ name="vrf-blue",
+ description="blue-vrf",
+ disable=False,
+ table_id=100,
+ vni=1000,
+ ),
+ dict(
+ name="vrf-red",
+ description="red-vrf",
+ disable=True,
+ table_id=101,
+ vni=1001,
+ address_family=[
+ dict(
+ afi="ipv4",
+ disable_forwarding=True,
+ route_maps=[
+ dict(rm_name="rm1", protocol="rip"),
+ ],
+ ),
+ dict(
+ afi="ipv6",
+ nht_no_resolve_via_default=True,
+ ),
+ ],
+ ),
+ ],
+ ),
+ state="replaced",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vrf_overridden(self):
+ set_module_args(
+ dict(
+ config=dict(
+ instances=[
+ dict(
+ name="vrf-blue",
+ description="blue-vrf",
+ disable=True,
+ table_id=103,
+ vni=1002,
+ ),
+ dict(
+ name="vrf-red",
+ description="red-vrf",
+ disable=True,
+ table_id=101,
+ vni=1001,
+ protocols=dict(
+ bgp=dict(
+ as_number=65000,
+ neighbor=[
+ dict(
+ address="192.0.2.1",
+ remote_as=65003,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ state="overridden",
+ ),
+ )
+ commands = [
+ "delete vrf name vrf-blue",
+ "commit",
+ "delete vrf name vrf-red",
+ "commit",
+ "set vrf name vrf-blue table 103",
+ "set vrf name vrf-blue vni 1002",
+ "set vrf name vrf-blue description blue-vrf",
+ "set vrf name vrf-blue disable",
+ "set vrf name vrf-red table 101",
+ "set vrf name vrf-red vni 1001",
+ "set vrf name vrf-red description red-vrf",
+ "set vrf name vrf-red disable",
+ "set vrf name vrf-red protocols bgp system-as 65000",
+ "set vrf name vrf-red protocols bgp neighbor 192.0.2.1 remote-as 65003",
+ "delete vrf bind-to-all",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrf_overridden_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ bind_to_all=True,
+ instances=[
+ dict(
+ name="vrf-blue",
+ description="blue-vrf",
+ disable=False,
+ table_id=100,
+ vni=1000,
+ ),
+ dict(
+ name="vrf-red",
+ description="red-vrf",
+ disable=True,
+ table_id=101,
+ vni=1001,
+ address_family=[
+ dict(
+ afi="ipv4",
+ disable_forwarding=True,
+ nht_no_resolve_via_default=False,
+ route_maps=[
+ dict(rm_name="rm1", protocol="rip"),
+ ],
+ ),
+ dict(
+ afi="ipv6",
+ disable_forwarding=False,
+ nht_no_resolve_via_default=True,
+ ),
+ ],
+ protocols=dict(
+ bgp=dict(
+ as_number=65000,
+ neighbor=[
+ dict(
+ address="192.0.2.1",
+ remote_as=65002,
+ ),
+ dict(
+ address="1.1.1.3",
+ remote_as=400,
+ passive=True,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ state="overridden",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vrf_rendered(self):
+ set_module_args(
+ dict(
+ config=dict(
+ bind_to_all=True,
+ instances=[
+ dict(
+ name="vrf-green",
+ description="green-vrf",
+ disabled=True,
+ table_id=105,
+ vni=1000,
+ ),
+ dict(
+ name="vrf-amber",
+ description="amber-vrf",
+ disable=False,
+ table_id=111,
+ vni=1001,
+ address_family=[
+ dict(
+ afi="ipv4",
+ disable_forwarding=True,
+ route_maps=[
+ dict(rm_name="rm1", protocol="ospf"),
+ ],
+ ),
+ dict(
+ afi="ipv6",
+ nht_no_resolve_via_default=False,
+ ),
+ ],
+ ),
+ ],
+ ),
+ state="rendered",
+ ),
+ )
+ rendered_commands = [
+ "set vrf bind-to-all",
+ "set vrf name vrf-green table 105",
+ "set vrf name vrf-green vni 1000",
+ "set vrf name vrf-green description green-vrf",
+ "set vrf name vrf-green disable",
+ "set vrf name vrf-amber table 111",
+ "set vrf name vrf-amber vni 1001",
+ "set vrf name vrf-amber description amber-vrf",
+ "set vrf name vrf-amber ip protocol ospf route-map rm1",
+ "set vrf name vrf-amber ip disable-forwarding",
+ ]
+ result = self.execute_module(changed=False)
+ self.assertEqual(
+ sorted(result["rendered"]),
+ sorted(rendered_commands),
+ result["rendered"],
+ )
+
+ def test_vrf_parsed(self):
+ commands = (
+ "set vrf bind-to-all",
+ "set vrf name vrf1 description 'red'",
+ "set vrf name vrf1 disable",
+ "set vrf name vrf1 table 101",
+ "set vrf name vrf1 vni 501",
+ "set vrf name vrf2 description 'green'",
+ "set vrf name vrf2 disable",
+ "set vrf name vrf2 table 102",
+ "set vrf name vrf2 vni 102",
+ "set vrf name vrf1 ip disable-forwarding",
+ "set vrf name vrf1 ip nht no-resolve-via-default",
+ "set vrf name vrf-red ip protocol ospf route-map 'rm1'",
+ "set vrf name vrf-red ipv6 nht no-resolve-via-default",
+ )
+ parsed_str = "\n".join(commands)
+ set_module_args(dict(running_config=parsed_str, state="parsed"))
+ result = self.execute_module(changed=False)
+ parsed_list = {
+ "bind_to_all": True,
+ "instances": [
+ {
+ "address_family": [
+ {
+ "afi": "ipv4",
+ "disable_forwarding": True,
+ "nht_no_resolve_via_default": True,
+ },
+ ],
+ "description": "red",
+ "disable": True,
+ "name": "vrf1",
+ },
+ {
+ "description": "green",
+ "disable": True,
+ "name": "vrf2",
+ },
+ {
+ "address_family": [
+ {
+ "afi": "ipv4",
+ "disable_forwarding": False,
+ "nht_no_resolve_via_default": False,
+ "route_maps": [
+ {
+ "protocol": "ospf",
+ "rm_name": "rm1",
+ },
+ ],
+ },
+ {
+ "afi": "ipv6",
+ "disable_forwarding": False,
+ "nht_no_resolve_via_default": True,
+ },
+ ],
+ "disable": False,
+ "name": "vrf-red",
+ },
+ ],
+ }
+ self.assertEqual(parsed_list, result["parsed"])
+
+ def test_vrf_gathered(self):
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False)
+ gathered_list = {
+ "bind_to_all": True,
+ "instances": [
+ {
+ "description": "blue-vrf",
+ "disable": False,
+ "name": "vrf-blue",
+ "table_id": 100,
+ "vni": 1000,
+ },
+ {
+ "address_family": [
+ {
+ "afi": "ipv4",
+ "disable_forwarding": True,
+ "nht_no_resolve_via_default": False,
+ "route_maps": [
+ {"protocol": "rip", "rm_name": "rm1"},
+ ],
+ },
+ {
+ "afi": "ipv6",
+ "disable_forwarding": False,
+ "nht_no_resolve_via_default": True,
+ },
+ ],
+ "description": "red-vrf",
+ "disable": True,
+ "name": "vrf-red",
+ "protocols": {
+ "bgp": {
+ "as_number": 65000,
+ "neighbor": [
+ {"address": "1.1.1.3", "passive": True, "remote_as": 400},
+ {"address": "192.0.2.1", "remote_as": 65002},
+ ],
+ },
+ },
+ "table_id": 101,
+ "vni": 1001,
+ },
+ ],
+ }
+ self.assertEqual(gathered_list, result["gathered"])
+
+ def test_vrf_deleted(self):
+ set_module_args(
+ dict(
+ config=dict(
+ bind_to_all=True,
+ instances=[
+ dict(
+ name="vrf-blue",
+ ),
+ ],
+ ),
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete vrf bind-to-all",
+ "delete vrf name vrf-blue",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vrf_all_deleted(self):
+ set_module_args(
+ dict(
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete vrf",
+ ]
+ self.execute_module(changed=True, commands=commands)
diff --git a/tests/unit/modules/network/vyos/vyos_module.py b/tests/unit/modules/network/vyos/vyos_module.py
index 60d94b5a..b1bdc2ec 100644
--- a/tests/unit/modules/network/vyos/vyos_module.py
+++ b/tests/unit/modules/network/vyos/vyos_module.py
@@ -18,7 +18,6 @@
# Make coding more python3-ish
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
import json
@@ -30,7 +29,6 @@ from ansible_collections.vyos.vyos.tests.unit.modules.utils import (
ModuleTestCase,
)
-
fixture_path = os.path.join(os.path.dirname(__file__), "fixtures")
fixture_data = {}
diff --git a/tests/unit/modules/utils.py b/tests/unit/modules/utils.py
index e2d17abd..c2a19070 100644
--- a/tests/unit/modules/utils.py
+++ b/tests/unit/modules/utils.py
@@ -1,6 +1,5 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
import json
import unittest