diff options
Diffstat (limited to 'tests')
24 files changed, 1158 insertions, 6 deletions
diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..6cfb063 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml @@ -0,0 +1,28 @@ +--- +- debug: + msg: START vyos_prefix_lists deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete all prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: &id001 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete prefix_lists configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_prefix_lists: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..083cb48 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/gathered.yaml @@ -0,0 +1,20 @@ +--- +- debug: + msg: START vyos_prefix_lists gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: + state: gathered + + - assert: + that: + - result.changed == false + - result.gathered | selectattr('afi','eq','ipv4') | list | length > 0 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/merged.yaml new file mode 100644 index 0000000..a70f826 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/merged.yaml @@ -0,0 +1,42 @@ +--- +- debug: + msg: START vyos_prefix_lists merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: &id001 + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + description: PL configured by ansible + entries: + - sequence: 2 + action: permit + prefix: 92.168.10.0/26 + le: 32 + - sequence: 3 + action: deny + prefix: 72.168.2.0/24 + ge: 26 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge prefix_lists configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_prefix_lists: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..f895488 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/overridden.yaml @@ -0,0 +1,38 @@ +--- +- debug: + msg: START vyos_prefix_lists overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: &id001 + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + description: Overridden by ansible + entries: + - sequence: 2 + action: permit + prefix: 92.168.10.0/26 + le: 32 + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override prefix_lists configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_prefix_lists: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..bd8c5c1 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/replaced.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_prefix_lists replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: &id001 + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + entries: + - sequence: 10 + action: permit + prefix: 10.0.0.0/8 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace prefix_lists configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_prefix_lists: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..de816eb --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/rtt.yaml @@ -0,0 +1,81 @@ +--- +- debug: + msg: START vyos_prefix_lists round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + description: PL configured by ansible + entries: + - sequence: 2 + action: permit + prefix: 92.168.10.0/26 + le: 32 + - sequence: 3 + action: deny + prefix: 72.168.2.0/24 + ge: 26 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_prefix_lists: + state: gathered + + - name: RTT - Assert gathered matches applied + vars: + ipv4_pls: >- + {{ gathered.gathered + | selectattr('afi', 'eq', 'ipv4') + | map(attribute='prefix_lists') + | first }} + assert: + that: + - gathered.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + - ipv4_pls | selectattr('name', 'eq', 'AnsibleIPv4PrefixList') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + description: Modified by ansible + entries: + - sequence: 2 + action: permit + prefix: 92.168.10.0/26 + le: 32 + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_prefix_lists: + state: gathered + + - name: RTT - Assert modification applied correctly + vars: + ipv4_pls2: >- + {{ gathered2.gathered + | selectattr('afi', 'eq', 'ipv4') + | map(attribute='prefix_lists') + | first }} + pl_entries: >- + {{ ipv4_pls2 + | selectattr('name', 'eq', 'AnsibleIPv4PrefixList') + | map(attribute='entries') + | first }} + assert: + that: + - gathered2.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + - pl_entries | length == 1 + - pl_entries | selectattr('sequence', 'eq', 2) | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_static_routes/aliases b/tests/integration/targets/vyos_static_routes/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_static_routes/defaults/main.yaml b/tests/integration/targets/vyos_static_routes/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml b/tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_static_routes/tasks/main.yaml b/tests/integration/targets/vyos_static_routes/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run httpapi tests + ansible.builtin.include_tasks: httpapi.yaml + tags: + - httpapi diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..e257075 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml @@ -0,0 +1,19 @@ +--- +- name: Populate static_routes config for testing + vyos.rest.vyos_static_routes: + config: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 10.0.0.1 + - dest: 203.0.113.0/24 + blackhole_config: + distance: 200 + - afi: ipv6 + routes: + - dest: 2001:db8::/32 + next_hops: + - forward_router_address: 2001:db8::1 + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..dd02da6 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove pre-existing static_routes config + vyos.rest.vyos_static_routes: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..b02e256 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml @@ -0,0 +1,28 @@ +--- +- debug: + msg: START vyos_static_routes deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete all static_routes configuration + register: result + vyos.rest.vyos_static_routes: &id001 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete static_routes configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_static_routes: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..c6d5e59 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml @@ -0,0 +1,20 @@ +--- +- debug: + msg: START vyos_static_routes gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather static_routes configuration + register: result + vyos.rest.vyos_static_routes: + state: gathered + + - assert: + that: + - result.changed == false + - result.gathered | selectattr('afi', 'eq', 'ipv4') | list | length > 0 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml new file mode 100644 index 0000000..6cf6a01 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_static_routes merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge static_routes configuration + register: result + vyos.rest.vyos_static_routes: &id001 + config: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 10.0.0.1 + - dest: 203.0.113.0/24 + blackhole_config: + distance: 200 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge static_routes configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_static_routes: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..e716d75 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/overridden.yaml @@ -0,0 +1,34 @@ +--- +- debug: + msg: START vyos_static_routes overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override static_routes configuration + register: result + vyos.rest.vyos_static_routes: &id001 + config: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 10.0.0.2 + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override static_routes configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_static_routes: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..a27e637 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_static_routes replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace static_routes configuration + register: result + vyos.rest.vyos_static_routes: &id001 + config: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 10.0.0.2 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace static_routes configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_static_routes: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..e0c76f9 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/rtt.yaml @@ -0,0 +1,71 @@ +--- +- debug: + msg: START vyos_static_routes round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_static_routes: + config: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 10.0.0.1 + - dest: 203.0.113.0/24 + blackhole_config: + distance: 200 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_static_routes: + state: gathered + + - name: RTT - Assert gathered matches applied + vars: + ipv4_routes: >- + {{ gathered.gathered + | selectattr('afi', 'eq', 'ipv4') + | map(attribute='routes') + | first }} + assert: + that: + - gathered.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + - ipv4_routes | selectattr('dest', 'eq', '192.0.2.0/24') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_static_routes: + config: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 10.0.0.2 + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_static_routes: + state: gathered + + - name: RTT - Assert modification applied correctly + vars: + ipv4_routes2: >- + {{ gathered2.gathered + | selectattr('afi', 'eq', 'ipv4') + | map(attribute='routes') + | first }} + dest_hops: >- + {{ ipv4_routes2 + | selectattr('dest', 'eq', '192.0.2.0/24') + | map(attribute='next_hops') + | first }} + assert: + that: + - gathered2.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + - dest_hops | selectattr('forward_router_address', 'eq', '10.0.0.2') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_static_routes/vars/main.yaml b/tests/integration/targets/vyos_static_routes/vars/main.yaml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/tests/integration/targets/vyos_static_routes/vars/main.yaml @@ -0,0 +1 @@ +--- diff --git a/tests/unit/fixtures/prefix_lists_running.json b/tests/unit/fixtures/prefix_lists_running.json index 2a6ecd3..ebc72d0 100644 --- a/tests/unit/fixtures/prefix_lists_running.json +++ b/tests/unit/fixtures/prefix_lists_running.json @@ -6,14 +6,12 @@ "2": { "action": "permit", "prefix": "92.168.10.0/26", - "le": "32", - "description": "Rule 2 given by ansible" + "le": "32" }, "3": { "action": "deny", "prefix": "72.168.2.0/24", - "ge": "26", - "description": "Rule 3" + "ge": "26" } } } @@ -25,8 +23,7 @@ "5": { "action": "permit", "prefix": "2001:db8:8000::/35", - "le": "37", - "description": "Permit rule" + "le": "37" } } }, diff --git a/tests/unit/fixtures/static_routes_running.json b/tests/unit/fixtures/static_routes_running.json new file mode 100644 index 0000000..46cf700 --- /dev/null +++ b/tests/unit/fixtures/static_routes_running.json @@ -0,0 +1,21 @@ +{ + "route": { + "192.0.2.0/24": { + "next-hop": { + "10.0.0.1": {} + } + }, + "203.0.113.0/24": { + "blackhole": { + "distance": "200" + } + } + }, + "route6": { + "2001:db8::/32": { + "next-hop": { + "2001:db8::1": {} + } + } + } +} diff --git a/tests/unit/modules/test_vyos_prefix_lists.py b/tests/unit/modules/test_vyos_prefix_lists.py new file mode 100644 index 0000000..e33de42 --- /dev/null +++ b/tests/unit/modules/test_vyos_prefix_lists.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_prefix_lists import ( + _normalize, + build_commands, + get_running_config, +) + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + path = os.path.join(fixtures_dir, filename) + with open(path) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + self.mock_vyos.get_config.return_value = data + + +class TestVyOSPrefixListsGetRunning(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("prefix_lists_running.json") + + def test_parses_ipv4_prefix_list(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next((e for e in result if e["afi"] == "ipv4"), None) + self.assertIsNotNone(ipv4) + pl = next((p for p in ipv4["prefix_lists"] if p["name"] == "AnsibleIPv4PrefixList"), None) + self.assertIsNotNone(pl) + self.assertEqual(pl["description"], "PL configured by ansible") + + def test_parses_ipv4_rules(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next(e for e in result if e["afi"] == "ipv4") + pl = ipv4["prefix_lists"][0] + seqs = [r["sequence"] for r in pl["entries"]] + self.assertIn(2, seqs) + self.assertIn(3, seqs) + + def test_parses_ipv4_rule_fields(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next(e for e in result if e["afi"] == "ipv4") + rule2 = next(r for r in ipv4["prefix_lists"][0]["entries"] if r["sequence"] == 2) + self.assertEqual(rule2["action"], "permit") + self.assertEqual(rule2["prefix"], "92.168.10.0/26") + self.assertEqual(rule2["le"], 32) + + def test_parses_ipv6_prefix_lists(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv6 = next((e for e in result if e["afi"] == "ipv6"), None) + self.assertIsNotNone(ipv6) + names = [p["name"] for p in ipv6["prefix_lists"]] + self.assertIn("AllowIPv6Prefix", names) + self.assertIn("DenyIPv6Prefix", names) + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSPrefixListsNormalize(unittest.TestCase): + + def test_normalize_ipv4(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [{"sequence": 10, "action": "permit", "prefix": "10.0.0.0/8"}], + }, + ], + }, + ] + result = _normalize(config) + self.assertIn("PL1", result["ipv4"]) + self.assertIn(10, result["ipv4"]["PL1"]["rules"]) + self.assertEqual(result["ipv4"]["PL1"]["rules"][10]["action"], "permit") + + def test_normalize_filters_none_values(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "prefix": "10.0.0.0/8", + "ge": None, + "le": None, + }, + ], + }, + ], + }, + ] + result = _normalize(config) + rule = result["ipv4"]["PL1"]["rules"][10] + self.assertNotIn("ge", rule) + self.assertNotIn("le", rule) + + +class TestVyOSPrefixListsBuildCommands(unittest.TestCase): + + def _have_empty(self): + return [] + + def _have_with_ipv4_pl(self): + return [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "prefix": "10.0.0.0/8", + }, + ], + }, + ], + }, + ] + + def test_merged_adds_new_prefix_list(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL-NEW", + "entries": [ + { + "sequence": 5, + "action": "permit", + "prefix": "192.168.0.0/24", + }, + ], + }, + ], + }, + ] + cmds = build_commands(config, self._have_empty(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["policy", "prefix-list", "PL-NEW", "rule", "5", "action", "permit"], paths) + self.assertIn( + ["policy", "prefix-list", "PL-NEW", "rule", "5", "prefix", "192.168.0.0/24"], + paths, + ) + + def test_merged_idempotent_existing_rule(self): + config = self._have_with_ipv4_pl() + cmds = build_commands(config, self._have_with_ipv4_pl(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_no_config_deletes_all(self): + cmds = build_commands([], self._have_with_ipv4_pl(), "deleted") + self.assertIn(("delete", ["policy", "prefix-list"]), cmds) + + def test_deleted_with_config_deletes_named(self): + config = [{"afi": "ipv4", "prefix_lists": [{"name": "PL1"}]}] + cmds = build_commands(config, self._have_with_ipv4_pl(), "deleted") + self.assertIn(("delete", ["policy", "prefix-list", "PL1"]), cmds) + + def test_replaced_deletes_then_resets(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "deny", + "prefix": "10.0.0.0/8", + }, + ], + }, + ], + }, + ] + cmds = build_commands(config, self._have_with_ipv4_pl(), "replaced") + # Should delete PL1 first then re-add + ops = [(c[0], c[1]) for c in cmds] + delete_idx = next( + i for i, c in enumerate(ops) if c == ("delete", ["policy", "prefix-list", "PL1"]) + ) + set_idx = next(i for i, c in enumerate(ops) if c[0] == "set" and "deny" in c[1]) + self.assertLess(delete_idx, set_idx) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_static_routes.py b/tests/unit/modules/test_vyos_static_routes.py new file mode 100644 index 0000000..1a50da9 --- /dev/null +++ b/tests/unit/modules/test_vyos_static_routes.py @@ -0,0 +1,365 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_static_routes import ( + _ROUTE_OPTIONS, + ARGUMENT_SPEC, + _derive_key_field, + _device_to_argspec, + _keyed_list_from_device, + _keyed_list_to_device, + _next_hop_entry_from_device, + _next_hop_entry_to_device, + _route_entry_from_device, + _route_entry_to_device, + _want_to_device, + build_commands, + cast_by_spec, + get_running_config, +) + +from .base import load_fixture + + +_BASE = ["protocols", "static"] + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.fixture = load_fixture("static_routes_running.json") + self.mock_vyos.get_config = MagicMock(return_value=self.fixture) + + +class TestGetRunningConfig(VyOSModuleTestCase): + def test_returns_config_directly(self): + result = get_running_config(self.mock_vyos) + self.assertIn("route", result) + self.assertIn("route6", result) + + def test_empty_config(self): + self.mock_vyos.get_config = MagicMock(return_value=None) + self.assertEqual(get_running_config(self.mock_vyos), {}) + + +class TestDeriveKeyField(unittest.TestCase): + def test_derives_dest_key(self): + route_opts = ARGUMENT_SPEC["config"]["options"]["routes"]["options"] + self.assertEqual(_derive_key_field(route_opts), "dest") + + def test_derives_forward_router_address_key(self): + nh_opts = ARGUMENT_SPEC["config"]["options"]["routes"]["options"]["next_hops"]["options"] + self.assertEqual(_derive_key_field(nh_opts), "forward_router_address") + + def test_raises_if_none_required(self): + with self.assertRaises(ValueError): + _derive_key_field({"a": {"type": "str"}}) + + def test_raises_if_more_than_one_required(self): + with self.assertRaises(ValueError): + _derive_key_field({"a": {"required": True}, "b": {"required": True}}) + + +class TestKeyedListHelper(unittest.TestCase): + def test_to_device_basic(self): + result = _keyed_list_to_device( + [{"dest": "192.0.2.0/24", "blackhole_config": {"distance": 200}}], + "dest", + ) + self.assertEqual(result, {"192.0.2.0/24": {"blackhole_config": {"distance": 200}}}) + + def test_from_device_basic(self): + result = _keyed_list_from_device({"192.0.2.0/24": {"a": 1}}, "dest") + self.assertEqual(result, [{"dest": "192.0.2.0/24", "a": 1}]) + + def test_empty(self): + self.assertEqual(_keyed_list_to_device([], "dest"), {}) + self.assertEqual(_keyed_list_from_device({}, "dest"), []) + + +class TestBlackholeNoTypeField(unittest.TestCase): + """Regression test for the confirmed hallucinated field: the + original module's blackhole_config.type does not correspond to + anything on the device -- confirmed against vyos-1x, the + "blackhole" node has only "distance" (and "tag", out of scope). + The field has been removed entirely.""" + + def test_type_not_in_argspec(self): + bh_opts = ARGUMENT_SPEC["config"]["options"]["routes"]["options"]["blackhole_config"] + self.assertNotIn("type", bh_opts["options"]) + self.assertEqual(set(bh_opts["options"].keys()), {"distance"}) + + +class TestRouteEntryToDeviceFromDevice(unittest.TestCase): + def test_blackhole_presence_only(self): + """An empty blackhole_config (no distance) still creates a + bare presence node -- achieving the same "just blackhole, no + distance" result the original's bogus "type" field was used + for, without needing any sentinel field at all.""" + result = _route_entry_to_device({"blackhole_config": {}}) + self.assertEqual(result, {"blackhole": {}}) + + def test_blackhole_with_distance(self): + result = _route_entry_to_device({"blackhole_config": {"distance": 200}}) + self.assertEqual(result, {"blackhole": {"distance": 200}}) + + def test_next_hops_keyed_by_address(self): + result = _route_entry_to_device( + {"next_hops": [{"forward_router_address": "10.0.0.1", "admin_distance": 50}]}, + ) + self.assertEqual(result, {"next-hop": {"10.0.0.1": {"distance": 50}}}) + + def test_disabled_next_hop(self): + result = _route_entry_to_device( + {"next_hops": [{"forward_router_address": "10.0.0.1", "enabled": False}]}, + ) + self.assertEqual(result["next-hop"]["10.0.0.1"], {"disable": {}}) + + def test_enabled_true_produces_no_disable_leaf(self): + result = _route_entry_to_device( + {"next_hops": [{"forward_router_address": "10.0.0.1", "enabled": True}]}, + ) + self.assertNotIn("disable", result["next-hop"]["10.0.0.1"]) + + def test_from_device_blackhole(self): + """from_device stays purely structural (kebab->snake only); + int-casting is cast_by_spec's responsibility, applied + downstream in main() -- confirmed separately below.""" + entry = _route_entry_from_device({"blackhole": {"distance": "200"}}) + self.assertEqual(entry["blackhole_config"]["distance"], "200") + + def test_from_device_next_hop_disabled(self): + entry = _route_entry_from_device({"next-hop": {"10.0.0.1": {"disable": {}}}}) + self.assertEqual(entry["next_hops"][0]["enabled"], False) + + def test_from_device_next_hop_enabled_omitted(self): + """Confirmed device behavior: an enabled next-hop has no + "disable" leaf at all -- "enabled" should not appear in the + parsed entry either, matching the argspec default.""" + entry = _route_entry_from_device({"next-hop": {"10.0.0.1": {}}}) + self.assertNotIn("enabled", entry["next_hops"][0]) + + def test_empty(self): + self.assertEqual(_route_entry_to_device({}), {}) + self.assertEqual(_route_entry_from_device({}), {}) + + +class TestNextHopEntryToDeviceFromDevice(unittest.TestCase): + def test_interface(self): + result = _next_hop_entry_to_device({"interface": "eth0"}) + self.assertEqual(result, {"interface": "eth0"}) + + def test_from_device_interface(self): + entry = _next_hop_entry_from_device({"interface": "eth0"}) + self.assertEqual(entry, {"interface": "eth0"}) + + def test_from_device_distance_cast_to_int(self): + entry = _next_hop_entry_from_device({"distance": "50"}) + self.assertEqual(entry["admin_distance"], 50) + self.assertIsInstance(entry["admin_distance"], int) + + +class TestWantToDevice(unittest.TestCase): + def test_empty(self): + self.assertEqual(_want_to_device([]), {}) + self.assertEqual(_want_to_device(None), {}) + + def test_afi_without_routes_omitted(self): + self.assertEqual(_want_to_device([{"afi": "ipv4"}]), {}) + + def test_keyed_by_route_key(self): + config = [{"afi": "ipv4", "routes": [{"dest": "192.0.2.0/24"}]}] + result = _want_to_device(config) + self.assertIn("192.0.2.0/24", result["route"]) + + def test_ipv6_uses_route6_key(self): + config = [{"afi": "ipv6", "routes": [{"dest": "2001:db8::/32"}]}] + result = _want_to_device(config) + self.assertIn("route6", result) + + +class TestDeviceToArgspecFixture(VyOSModuleTestCase): + def test_both_afis_parsed(self): + raw = get_running_config(self.mock_vyos) + result = _device_to_argspec(raw) + afis = [e["afi"] for e in result] + self.assertIn("ipv4", afis) + self.assertIn("ipv6", afis) + + def test_blackhole_route_parsed_with_casting(self): + """from_device alone leaves distance as the raw device string; + cast_by_spec (applied downstream in main(), confirmed here + directly) is what casts it to int, since cast_by_spec recurses + into type="dict" suboptions like blackhole_config.""" + raw = get_running_config(self.mock_vyos) + result = _device_to_argspec(raw) + ipv4_routes = next(e for e in result if e["afi"] == "ipv4")["routes"] + bh_route = next(r for r in ipv4_routes if r["dest"] == "203.0.113.0/24") + self.assertEqual(bh_route["blackhole_config"]["distance"], "200") + cast_by_spec(bh_route, _ROUTE_OPTIONS) + self.assertEqual(bh_route["blackhole_config"]["distance"], 200) + + def test_next_hop_route_parsed(self): + raw = get_running_config(self.mock_vyos) + result = _device_to_argspec(raw) + ipv4_routes = next(e for e in result if e["afi"] == "ipv4")["routes"] + nh_route = next(r for r in ipv4_routes if r["dest"] == "192.0.2.0/24") + self.assertEqual(nh_route["next_hops"][0]["forward_router_address"], "10.0.0.1") + + def test_empty_config(self): + self.assertEqual(_device_to_argspec({}), []) + self.assertEqual(_device_to_argspec(None), []) + + +class TestBuildCommands(VyOSModuleTestCase): + def setUp(self): + super().setUp() + self.raw = get_running_config(self.mock_vyos) + + def test_merged_idempotent_against_own_fixture(self): + have = _device_to_argspec(self.raw) + self.assertEqual(build_commands(have, self.raw, "merged"), []) + + def test_replaced_idempotent_against_own_fixture(self): + have = _device_to_argspec(self.raw) + self.assertEqual(build_commands(have, self.raw, "replaced"), []) + + def test_overridden_idempotent_against_own_fixture(self): + have = _device_to_argspec(self.raw) + self.assertEqual(build_commands(have, self.raw, "overridden"), []) + + def test_clear_omitted_next_hop_attribute_on_replaced(self): + """The primary confirmed bug fix from the PR review: the + original _route_cmds only emitted commands for setting values, + never for clearing an omitted attribute back to default, and + "replaced" state's own change-detection missed this entirely + since it only inspected generated set-commands.""" + raw_have = {"route": {"192.0.2.0/24": {"next-hop": {"10.0.0.1": {"distance": "50"}}}}} + config = [ + { + "afi": "ipv4", + "routes": [ + { + "dest": "192.0.2.0/24", + "next_hops": [ + {"forward_router_address": "10.0.0.1"}, + ], + }, + ], + }, + ] + cmds = build_commands(config, raw_have, "replaced") + expected = ("delete", _BASE + ["route", "192.0.2.0/24", "next-hop", "10.0.0.1", "distance"]) + self.assertIn(expected, cmds) + + def test_replaced_scoped_to_named_route_only(self): + raw_have = { + "route": { + "192.0.2.0/24": {"next-hop": {"10.0.0.1": {}}}, + "203.0.113.0/24": {"blackhole": {"distance": "200"}}, + }, + } + config = [ + { + "afi": "ipv4", + "routes": [ + { + "dest": "192.0.2.0/24", + "next_hops": [ + {"forward_router_address": "10.0.0.1"}, + ], + }, + ], + }, + ] + cmds = build_commands(config, raw_have, "replaced") + self.assertEqual(cmds, []) + self.assertFalse(any("203.0.113.0/24" in str(c) for c in cmds)) + + def test_overridden_deletes_omitted_route(self): + raw_have = { + "route": { + "192.0.2.0/24": {"next-hop": {"10.0.0.1": {}}}, + "203.0.113.0/24": {"blackhole": {"distance": "200"}}, + }, + } + config = [ + { + "afi": "ipv4", + "routes": [ + { + "dest": "192.0.2.0/24", + "next_hops": [ + {"forward_router_address": "10.0.0.1"}, + ], + }, + ], + }, + ] + cmds = build_commands(config, raw_have, "overridden") + self.assertIn(("delete", _BASE + ["route", "203.0.113.0/24"]), cmds) + + def test_deleted_named_route(self): + cmds = build_commands( + [{"afi": "ipv4", "routes": [{"dest": "192.0.2.0/24"}]}], + self.raw, + "deleted", + ) + self.assertEqual(cmds, [("delete", _BASE + ["route", "192.0.2.0/24"])]) + + def test_deleted_named_afi_no_routes(self): + cmds = build_commands([{"afi": "ipv4"}], self.raw, "deleted") + self.assertEqual(cmds, [("delete", _BASE + ["route"])]) + + def test_deleted_no_config_removes_all(self): + cmds = build_commands([], self.raw, "deleted") + self.assertEqual(cmds, [("delete", _BASE)]) + + def test_deleted_named_nonexistent_is_noop(self): + cmds = build_commands( + [{"afi": "ipv4", "routes": [{"dest": "198.51.100.0/24"}]}], + self.raw, + "deleted", + ) + self.assertEqual(cmds, []) + + def test_collapsed_bare_route_no_blackhole_or_next_hop(self): + raw_have = {"route": {"192.0.2.0/24": {}}} + config = [{"afi": "ipv4", "routes": [{"dest": "192.0.2.0/24"}]}] + self.assertEqual(build_commands(config, raw_have, "merged"), []) + + def test_merged_new_blackhole_route(self): + config = [{"afi": "ipv4", "routes": [{"dest": "198.51.100.0/24", "blackhole_config": {}}]}] + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["route", "198.51.100.0/24", "blackhole"]), cmds) + + def test_merged_new_disabled_next_hop(self): + config = [ + { + "afi": "ipv4", + "routes": [ + { + "dest": "198.51.100.0/24", + "next_hops": [ + {"forward_router_address": "10.0.0.9", "enabled": False}, + ], + }, + ], + }, + ] + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", _BASE + ["route", "198.51.100.0/24", "next-hop", "10.0.0.9", "disable"]), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() |
