From 88248610065245ea8ee5bbc299b116a37da584f3 Mon Sep 17 00:00:00 2001 From: omnom62 Date: Fri, 19 Jun 2026 17:37:25 +1000 Subject: T8989: vyos_interfaces --- README.md | 13 +- plugins/modules/vyos_interfaces.py | 334 +++++++++++++++++++++ tests/integration/targets/vyos_interfaces/aliases | 1 + .../targets/vyos_interfaces/defaults/main.yaml | 3 + .../targets/vyos_interfaces/tasks/httpapi.yaml | 21 ++ .../targets/vyos_interfaces/tasks/main.yaml | 5 + .../tests/httpapi/_populate_config.yaml | 9 + .../tests/httpapi/_remove_config.yaml | 7 + .../vyos_interfaces/tests/httpapi/deleted.yaml | 30 ++ .../vyos_interfaces/tests/httpapi/gathered.yaml | 21 ++ .../vyos_interfaces/tests/httpapi/merged.yaml | 32 ++ .../vyos_interfaces/tests/httpapi/replaced.yaml | 33 ++ .../targets/vyos_interfaces/vars/main.yaml | 2 + tests/unit/fixtures/interfaces_running.json | 15 + tests/unit/modules/test_vyos_interfaces.py | 315 +++++++++++++++++++ 15 files changed, 833 insertions(+), 8 deletions(-) create mode 100644 plugins/modules/vyos_interfaces.py create mode 100644 tests/integration/targets/vyos_interfaces/aliases create mode 100644 tests/integration/targets/vyos_interfaces/defaults/main.yaml create mode 100644 tests/integration/targets/vyos_interfaces/tasks/httpapi.yaml create mode 100644 tests/integration/targets/vyos_interfaces/tasks/main.yaml create mode 100644 tests/integration/targets/vyos_interfaces/tests/httpapi/_populate_config.yaml create mode 100644 tests/integration/targets/vyos_interfaces/tests/httpapi/_remove_config.yaml create mode 100644 tests/integration/targets/vyos_interfaces/tests/httpapi/deleted.yaml create mode 100644 tests/integration/targets/vyos_interfaces/tests/httpapi/gathered.yaml create mode 100644 tests/integration/targets/vyos_interfaces/tests/httpapi/merged.yaml create mode 100644 tests/integration/targets/vyos_interfaces/tests/httpapi/replaced.yaml create mode 100644 tests/integration/targets/vyos_interfaces/vars/main.yaml create mode 100644 tests/unit/fixtures/interfaces_running.json create mode 100644 tests/unit/modules/test_vyos_interfaces.py diff --git a/README.md b/README.md index e056af4..8357287 100644 --- a/README.md +++ b/README.md @@ -74,20 +74,17 @@ Name | Description [vyos.rest.vyos](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_httpapi.rst)|HttpApi plugin for VyOS REST API ### Modules - -Modules marked ⚠️ are not yet available in this release. - Name | Description --- | --- [vyos.rest.vyos_banner](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_banner_module.rst)|Manage multiline banners on VyOS devices via REST API. [vyos.rest.vyos_configure](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_configure_module.rst)|Send raw set/delete commands to a VyOS device via REST API. [vyos.rest.vyos_hostname](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_hostname_module.rst)|Manage the system hostname on a VyOS device via the REST API. +[vyos.rest.vyos_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_interfaces_module.rst)|Manage interface configuration on VyOS devices via REST API. [vyos.rest.vyos_lldp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_lldp_global_module.rst)|Manage LLDP global configuration on VyOS via REST API. -[vyos.rest.vyos_logging_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_logging_global_module.rst)|Manage syslog configuration on VyOS devices using REST API. -[vyos.rest.vyos_ntp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ntp_global_module.rst)|Manage NTP configuration on VyOS devices using REST API. -[vyos.rest.vyos_prefix_lists](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_prefix_lists_module.rst)|⚠️ Manage prefix-list configuration on VyOS devices using REST API. *(not yet available)* -[vyos.rest.vyos_route_maps](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_route_maps_module.rst)|Manage route-map configuration on VyOS devices using REST API. -[vyos.rest.vyos_snmp_server](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_snmp_server_module.rst)|Manage SNMP server configuration on VyOS devices using REST API. +[vyos.rest.vyos_logging_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_logging_global_module.rst)|Manage syslog configuration on VyOS devices using REST API +[vyos.rest.vyos_ntp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ntp_global_module.rst)|Manage NTP configuration on VyOS devices using REST API +[vyos.rest.vyos_route_maps](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_route_maps_module.rst)|Manage route-map configuration on VyOS devices using REST API +[vyos.rest.vyos_snmp_server](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_snmp_server_module.rst)|Manage SNMP server configuration on VyOS devices using REST API diff --git a/plugins/modules/vyos_interfaces.py b/plugins/modules/vyos_interfaces.py new file mode 100644 index 0000000..6ed45d5 --- /dev/null +++ b/plugins/modules/vyos_interfaces.py @@ -0,0 +1,334 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# 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 + +DOCUMENTATION = r""" +--- +module: vyos_interfaces +short_description: Manage interface configuration on VyOS devices via REST API. +description: + - Manages L2 interface configuration (description, MTU, speed, duplex, enabled) + on VyOS devices using the HTTPS REST API. + - IP address configuration is handled by M(vyos.rest.vyos_l3_interfaces). +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: List of interface configurations. + type: list + elements: dict + suboptions: + name: + description: Full interface name (e.g. eth0, bond0, lo). + type: str + required: true + description: + description: Interface description. + type: str + enabled: + description: Whether the interface is enabled. False sets the disable flag. + type: bool + default: true + mtu: + description: Interface MTU. + type: int + duplex: + description: Interface duplex setting. + type: str + choices: [auto, full, half] + speed: + description: Interface speed setting. + type: str + choices: [auto, "10", "100", "1000", "2500", "10000"] + state: + description: + - C(merged) - Merge config with existing interface settings. + - C(replaced) - Replace config for listed interfaces. + - C(overridden) - Replace config for all interfaces. + - C(deleted) - Remove listed interface config or all interface config. + - C(gathered) - Read interface config from device without changes. + type: str + choices: [merged, replaced, overridden, deleted, gathered] + default: merged +seealso: + - module: vyos.vyos.vyos_interfaces + - module: vyos.rest.vyos_l3_interfaces +""" + +EXAMPLES = r""" +- name: Merge interface configuration + vyos.rest.vyos_interfaces: + config: + - name: eth0 + description: Management interface + mtu: 1500 + enabled: true + state: merged + +- name: Disable an interface + vyos.rest.vyos_interfaces: + config: + - name: eth1 + enabled: false + state: merged + +- name: Delete interface description + vyos.rest.vyos_interfaces: + config: + - name: eth0 + state: deleted + +- name: Gather current interface configuration + vyos.rest.vyos_interfaces: + state: gathered +""" + +RETURN = r""" +before: + description: Interface configuration before this module ran. + returned: always + type: list +after: + description: Interface configuration after this module ran. + returned: when changed + type: list +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current interface configuration as structured data. + returned: when state is gathered + type: list +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +# Interface name prefix → API type key +_IFACE_TYPE = { + "eth": "ethernet", + "bond": "bonding", + "lo": "loopback", + "tun": "tunnel", + "wg": "wireguard", + "vti": "vti", + "dum": "dummy", + "vtun": "openvpn", + "ppp": "pppoe", + "wlan": "wireless", + "br": "bridge", +} + +# L2 fields managed by this module — excludes address, hw-id etc. +_L2_FIELDS = ["description", "mtu", "duplex", "speed"] + + +def _iface_type(name): + for prefix, itype in _IFACE_TYPE.items(): + if name.startswith(prefix): + return itype + return "ethernet" + + +def _iface_base(name): + return ["interfaces", _iface_type(name), name] + + +def get_running_config(vyos): + raw = vyos.get_config(["interfaces"]) + if not raw or not isinstance(raw, dict): + return [] + + result = [] + for itype, ifaces in sorted(raw.items()): + if not isinstance(ifaces, dict): + continue + for iname, idata in sorted(ifaces.items()): + idata = idata or {} + entry = {"name": iname} + if idata.get("description"): + entry["description"] = idata["description"] + if "mtu" in idata: + entry["mtu"] = int(idata["mtu"]) + if "duplex" in idata: + entry["duplex"] = idata["duplex"] + if "speed" in idata: + entry["speed"] = idata["speed"] + entry["enabled"] = "disable" not in idata + result.append(entry) + + return result + + +def _normalize(config): + """Convert argspec list to dict keyed by interface name.""" + return {entry["name"]: entry for entry in (config or [])} + + +def _iface_cmds(name, want, have): + """Generate set/delete commands to bring have → want for one interface.""" + cmds = [] + base = _iface_base(name) + have = have or {} + + # description + want_desc = want.get("description") + have_desc = have.get("description") + if want_desc is not None and want_desc != have_desc: + cmds.append(("set", base + ["description", want_desc])) + elif want_desc is None and have_desc is not None: + cmds.append(("delete", base + ["description"])) + + # mtu + want_mtu = want.get("mtu") + have_mtu = have.get("mtu") + if want_mtu is not None and want_mtu != have_mtu: + cmds.append(("set", base + ["mtu", str(want_mtu)])) + + # duplex + want_duplex = want.get("duplex") + have_duplex = have.get("duplex") + if want_duplex is not None and want_duplex != have_duplex: + cmds.append(("set", base + ["duplex", want_duplex])) + + # speed + want_speed = want.get("speed") + have_speed = have.get("speed") + if want_speed is not None and want_speed != have_speed: + cmds.append(("set", base + ["speed", want_speed])) + + # enabled / disable flag + want_enabled = want.get("enabled", True) + have_enabled = have.get("enabled", True) + if not want_enabled and have_enabled: + cmds.append(("set", base + ["disable"])) + elif want_enabled and not have_enabled: + cmds.append(("delete", base + ["disable"])) + + return cmds + + +def _delete_iface_config(name, have): + """Generate delete commands to remove L2 config from an interface.""" + cmds = [] + base = _iface_base(name) + have = have or {} + + for field in _L2_FIELDS: + if field in have: + cmds.append(("delete", base + [field])) + if not have.get("enabled", True): + cmds.append(("delete", base + ["disable"])) + + return cmds + + +def build_commands(config, have_raw, state): + cmds = [] + have_map = _normalize(have_raw) + + if state == "deleted": + if not config: + for name, have in have_map.items(): + cmds += _delete_iface_config(name, have) + else: + for entry in config: + name = entry["name"] + cmds += _delete_iface_config(name, have_map.get(name, {})) + return cmds + + want_map = _normalize(config) + + if state == "overridden": + # delete L2 config from interfaces not in want + for name in set(have_map) - set(want_map): + cmds += _delete_iface_config(name, have_map[name]) + + for name, want in want_map.items(): + have = have_map.get(name, {}) + + if state == "replaced": + # pre-check — only act if something differs + test_cmds = _iface_cmds(name, want, have) + if not test_cmds: + continue + # delete L2 fields then rebuild + cmds += _delete_iface_config(name, have) + have = {} + + cmds += _iface_cmds(name, want, have if state != "replaced" else {}) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + description=dict(type="str"), + enabled=dict(type="bool", default=True), + mtu=dict(type="int"), + duplex=dict(type="str", choices=["auto", "full", "half"]), + speed=dict(type="str", choices=["auto", "10", "100", "1000", "2500", "10000"]), + ), + ), + state=dict( + type="str", + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered"], + ), +) + + +def main(): + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or [] + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/vyos_interfaces/aliases b/tests/integration/targets/vyos_interfaces/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_interfaces/defaults/main.yaml b/tests/integration/targets/vyos_interfaces/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_interfaces/tasks/httpapi.yaml b/tests/integration/targets/vyos_interfaces/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/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_interfaces/tasks/main.yaml b/tests/integration/targets/vyos_interfaces/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/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_interfaces/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..9562fce --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/_populate_config.yaml @@ -0,0 +1,9 @@ +--- +- name: Populate interface config for testing + vyos.rest.vyos_interfaces: + config: + - name: eth0 + description: Ansible test interface + mtu: 1450 + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..4014e56 --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/_remove_config.yaml @@ -0,0 +1,7 @@ +--- +- name: Remove interface L2 config + vyos.rest.vyos_interfaces: + config: + - name: eth0 + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..e8dfd06 --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/deleted.yaml @@ -0,0 +1,30 @@ +--- +- debug: + msg: START vyos_interfaces deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete interface configuration + register: result + vyos.rest.vyos_interfaces: &id001 + config: + - name: eth0 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..022e21e --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_interfaces gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather interface configuration + register: result + vyos.rest.vyos_interfaces: + state: gathered + + - assert: + that: + - result.changed == false + - result.gathered | selectattr('name', 'eq', 'eth0') | list | length == 1 + - result.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='description') | first == 'Ansible test interface' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/merged.yaml new file mode 100644 index 0000000..b5a82ef --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/merged.yaml @@ -0,0 +1,32 @@ +--- +- debug: + msg: START vyos_interfaces merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge interface configuration + register: result + vyos.rest.vyos_interfaces: &id001 + config: + - name: eth0 + description: Ansible test interface + mtu: 1450 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..430b441 --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/replaced.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_interfaces replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace interface configuration + register: result + vyos.rest.vyos_interfaces: &id001 + config: + - name: eth0 + description: Ansible replaced interface + mtu: 1400 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_interfaces/vars/main.yaml b/tests/integration/targets/vyos_interfaces/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/unit/fixtures/interfaces_running.json b/tests/unit/fixtures/interfaces_running.json new file mode 100644 index 0000000..ee67fa2 --- /dev/null +++ b/tests/unit/fixtures/interfaces_running.json @@ -0,0 +1,15 @@ +{ + "ethernet": { + "eth0": { + "address": "dhcp", + "description": "Management", + "duplex": "auto", + "hw-id": "52:54:00:65:5a:24", + "mtu": "1500", + "speed": "auto" + } + }, + "loopback": { + "lo": {} + } +} diff --git a/tests/unit/modules/test_vyos_interfaces.py b/tests/unit/modules/test_vyos_interfaces.py new file mode 100644 index 0000000..d522c81 --- /dev/null +++ b/tests/unit/modules/test_vyos_interfaces.py @@ -0,0 +1,315 @@ +# -*- 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_interfaces import ( + _delete_iface_config, + _iface_base, + _iface_cmds, + _iface_type, + 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 TestVyOSInterfacesIfaceType(unittest.TestCase): + + def test_eth_is_ethernet(self): + self.assertEqual(_iface_type("eth0"), "ethernet") + + def test_bond_is_bonding(self): + self.assertEqual(_iface_type("bond0"), "bonding") + + def test_lo_is_loopback(self): + self.assertEqual(_iface_type("lo"), "loopback") + + def test_wg_is_wireguard(self): + self.assertEqual(_iface_type("wg0"), "wireguard") + + def test_br_is_bridge(self): + self.assertEqual(_iface_type("br0"), "bridge") + + def test_unknown_defaults_to_ethernet(self): + self.assertEqual(_iface_type("xyz0"), "ethernet") + + def test_iface_base_ethernet(self): + self.assertEqual( + _iface_base("eth0"), + ["interfaces", "ethernet", "eth0"], + ) + + def test_iface_base_loopback(self): + self.assertEqual( + _iface_base("lo"), + ["interfaces", "loopback", "lo"], + ) + + +class TestVyOSInterfacesGetRunningFixture(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("interfaces_running.json") + + def test_fixture_parses_eth0(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertEqual(eth0["description"], "Management") + self.assertEqual(eth0["mtu"], 1500) + self.assertEqual(eth0["duplex"], "auto") + self.assertEqual(eth0["speed"], "auto") + self.assertTrue(eth0["enabled"]) + + def test_fixture_parses_loopback(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + lo = next(e for e in result if e["name"] == "lo") + self.assertTrue(lo["enabled"]) + self.assertNotIn("description", lo) + + def test_fixture_disabled_interface(self): + fixture = dict(self.fixture) + fixture["ethernet"]["eth1"] = {"disable": {}, "description": "Unused"} + self.set_running_config(fixture) + result = get_running_config(self.mock_vyos) + eth1 = next(e for e in result if e["name"] == "eth1") + self.assertFalse(eth1["enabled"]) + + +class TestVyOSInterfacesGetRunning(VyOSModuleTestCase): + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + def test_mtu_cast_to_int(self): + self.set_running_config( + { + "ethernet": { + "eth0": {"mtu": "1500"}, + }, + }, + ) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertIsInstance(eth0["mtu"], int) + self.assertEqual(eth0["mtu"], 1500) + + def test_hw_id_not_included(self): + self.set_running_config( + { + "ethernet": { + "eth0": {"hw-id": "52:54:00:65:5a:24"}, + }, + }, + ) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertNotIn("hw_id", eth0) + self.assertNotIn("hw-id", eth0) + + def test_enabled_true_when_no_disable(self): + self.set_running_config( + { + "ethernet": {"eth0": {}}, + }, + ) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertTrue(eth0["enabled"]) + + def test_enabled_false_when_disable_present(self): + self.set_running_config( + { + "ethernet": {"eth0": {"disable": {}}}, + }, + ) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertFalse(eth0["enabled"]) + + +class TestVyOSInterfacesIfaceCmds(unittest.TestCase): + + def test_set_description(self): + want = {"description": "WAN", "enabled": True} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "description", "WAN"]), + cmds, + ) + + def test_set_mtu(self): + want = {"mtu": 9000, "enabled": True} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "mtu", "9000"]), + cmds, + ) + + def test_set_duplex(self): + want = {"duplex": "full", "enabled": True} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "duplex", "full"]), + cmds, + ) + + def test_set_speed(self): + want = {"speed": "1000", "enabled": True} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "speed", "1000"]), + cmds, + ) + + def test_disable_interface(self): + want = {"enabled": False} + have = {"enabled": True} + cmds = _iface_cmds("eth0", want, have) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "disable"]), + cmds, + ) + + def test_enable_interface(self): + want = {"enabled": True} + have = {"enabled": False} + cmds = _iface_cmds("eth0", want, have) + self.assertIn( + ("delete", ["interfaces", "ethernet", "eth0", "disable"]), + cmds, + ) + + def test_idempotent_description(self): + want = {"description": "WAN", "enabled": True} + have = {"description": "WAN", "enabled": True} + cmds = _iface_cmds("eth0", want, have) + self.assertEqual(cmds, []) + + def test_delete_description_when_none(self): + want = {"enabled": True} + have = {"description": "Old", "enabled": True} + cmds = _iface_cmds("eth0", want, have) + self.assertIn( + ("delete", ["interfaces", "ethernet", "eth0", "description"]), + cmds, + ) + + +class TestVyOSInterfacesDeleteIfaceConfig(unittest.TestCase): + + def test_deletes_all_l2_fields(self): + have = { + "description": "WAN", + "mtu": 1500, + "duplex": "auto", + "speed": "auto", + "enabled": True, + } + cmds = _delete_iface_config("eth0", have) + paths = [c[1] for c in cmds] + self.assertIn(["interfaces", "ethernet", "eth0", "description"], paths) + self.assertIn(["interfaces", "ethernet", "eth0", "mtu"], paths) + self.assertIn(["interfaces", "ethernet", "eth0", "duplex"], paths) + self.assertIn(["interfaces", "ethernet", "eth0", "speed"], paths) + + def test_deletes_disable_when_disabled(self): + have = {"enabled": False} + cmds = _delete_iface_config("eth0", have) + self.assertIn( + ("delete", ["interfaces", "ethernet", "eth0", "disable"]), + cmds, + ) + + def test_empty_have_produces_no_commands(self): + cmds = _delete_iface_config("eth0", {}) + self.assertEqual(cmds, []) + + +class TestVyOSInterfacesBuildCommands(unittest.TestCase): + + def _have_eth0(self): + return [ + { + "name": "eth0", + "description": "Management", + "mtu": 1500, + "enabled": True, + }, + ] + + def test_merged_adds_description(self): + config = [{"name": "eth0", "description": "WAN", "enabled": True}] + cmds = build_commands(config, [], "merged") + paths = [c[1] for c in cmds] + self.assertIn( + ["interfaces", "ethernet", "eth0", "description", "WAN"], + paths, + ) + + def test_merged_idempotent(self): + cmds = build_commands(self._have_eth0(), self._have_eth0(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_removes_l2_fields(self): + config = [{"name": "eth0", "enabled": True}] + cmds = build_commands(config, self._have_eth0(), "deleted") + paths = [c[1] for c in cmds] + self.assertIn(["interfaces", "ethernet", "eth0", "description"], paths) + self.assertIn(["interfaces", "ethernet", "eth0", "mtu"], paths) + + def test_deleted_idempotent_when_no_l2(self): + have = [{"name": "eth0", "enabled": True}] + config = [{"name": "eth0", "enabled": True}] + cmds = build_commands(config, have, "deleted") + self.assertEqual(cmds, []) + + def test_replaced_idempotent(self): + cmds = build_commands(self._have_eth0(), self._have_eth0(), "replaced") + self.assertEqual(cmds, []) + + def test_replaced_updates_description(self): + config = [{"name": "eth0", "description": "NEW", "mtu": 1500, "enabled": True}] + cmds = build_commands(config, self._have_eth0(), "replaced") + self.assertTrue(len(cmds) > 0) + + def test_overridden_clears_interfaces_not_in_want(self): + have = [ + {"name": "eth0", "description": "Management", "enabled": True}, + {"name": "eth1", "description": "LAN", "enabled": True}, + ] + config = [{"name": "eth0", "description": "Management", "enabled": True}] + cmds = build_commands(config, have, "overridden") + paths = [c[1] for c in cmds] + self.assertIn(["interfaces", "ethernet", "eth1", "description"], paths) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3