summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-06-19 17:37:25 +1000
committeromnom62 <omnom62@outlook.com>2026-06-19 17:37:25 +1000
commit88248610065245ea8ee5bbc299b116a37da584f3 (patch)
tree2ac90cdbb27b96b8a24c48ab9ce9fb93394c4c98
parentdb8c9bdef5d524ce92c7068514a87a4b36a3ddd8 (diff)
downloadrest.vyos-88248610065245ea8ee5bbc299b116a37da584f3.tar.gz
rest.vyos-88248610065245ea8ee5bbc299b116a37da584f3.zip
T8989: vyos_interfaces
-rw-r--r--README.md13
-rw-r--r--plugins/modules/vyos_interfaces.py334
-rw-r--r--tests/integration/targets/vyos_interfaces/aliases1
-rw-r--r--tests/integration/targets/vyos_interfaces/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_interfaces/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_interfaces/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_interfaces/tests/httpapi/_populate_config.yaml9
-rw-r--r--tests/integration/targets/vyos_interfaces/tests/httpapi/_remove_config.yaml7
-rw-r--r--tests/integration/targets/vyos_interfaces/tests/httpapi/deleted.yaml30
-rw-r--r--tests/integration/targets/vyos_interfaces/tests/httpapi/gathered.yaml21
-rw-r--r--tests/integration/targets/vyos_interfaces/tests/httpapi/merged.yaml32
-rw-r--r--tests/integration/targets/vyos_interfaces/tests/httpapi/replaced.yaml33
-rw-r--r--tests/integration/targets/vyos_interfaces/vars/main.yaml2
-rw-r--r--tests/unit/fixtures/interfaces_running.json15
-rw-r--r--tests/unit/modules/test_vyos_interfaces.py315
15 files changed, 833 insertions, 8 deletions
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
<!--end collection content-->
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()