From b34a4e34d9321413bc79d2f29aed13ef897084c8 Mon Sep 17 00:00:00 2001 From: omnom62 Date: Mon, 6 Jul 2026 15:40:34 +1000 Subject: T8989: Wave 4 vyos_config module with integration and unit tests --- docs/vyos.rest.vyos_config_module.rst | 228 +++++++++++++++++++++ tests/integration/targets/vyos_config/aliases | 1 + .../targets/vyos_config/defaults/main.yaml | 3 + .../targets/vyos_config/tasks/httpapi.yaml | 21 ++ .../targets/vyos_config/tasks/main.yaml | 5 + .../vyos_config/tests/httpapi/_remove_config.yaml | 7 + .../targets/vyos_config/tests/httpapi/delete.yaml | 37 ++++ .../targets/vyos_config/tests/httpapi/lines.yaml | 44 ++++ .../integration/targets/vyos_config/vars/main.yaml | 2 + tests/unit/modules/test_vyos_config.py | 107 ++++++++++ 10 files changed, 455 insertions(+) create mode 100644 docs/vyos.rest.vyos_config_module.rst create mode 100644 tests/integration/targets/vyos_config/aliases create mode 100644 tests/integration/targets/vyos_config/defaults/main.yaml create mode 100644 tests/integration/targets/vyos_config/tasks/httpapi.yaml create mode 100644 tests/integration/targets/vyos_config/tasks/main.yaml create mode 100644 tests/integration/targets/vyos_config/tests/httpapi/_remove_config.yaml create mode 100644 tests/integration/targets/vyos_config/tests/httpapi/delete.yaml create mode 100644 tests/integration/targets/vyos_config/tests/httpapi/lines.yaml create mode 100644 tests/integration/targets/vyos_config/vars/main.yaml create mode 100644 tests/unit/modules/test_vyos_config.py diff --git a/docs/vyos.rest.vyos_config_module.rst b/docs/vyos.rest.vyos_config_module.rst new file mode 100644 index 0000000..a961e8d --- /dev/null +++ b/docs/vyos.rest.vyos_config_module.rst @@ -0,0 +1,228 @@ +.. _vyos.rest.vyos_config_module: + + +********************* +vyos.rest.vyos_config +********************* + +**Manage VyOS configuration using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages VyOS device configuration via the REST API. +- Accepts configuration commands in CLI ``set``/``delete`` string format and applies them via the REST ``/configure`` endpoint. +- Uses REST API (``connection=httpapi``) instead of CLI. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ lines + +
+ list + / elements=string +
+
+ +
Ordered list of set or delete commands to apply.
+
Commands should be in standard VyOS CLI format, e.g. set system host-name router1 or delete protocols bgp.
+
+
+ match + +
+ string +
+
+
    Choices: +
  • line ←
  • +
  • none
  • +
+
+
Controls how commands are matched against the running configuration.
+
line checks each command against the running config and only applies commands that would change the configuration.
+
none applies all commands without checking the running config.
+
+
+ save + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Save the configuration to disk after applying changes.
+
+
+ src + +
+ path +
+
+ +
Path to a file containing set/delete commands, one per line.
+
Blank lines and lines starting with # are ignored.
+
Mutually exclusive with lines.
+
+
+ + +Notes +----- + +.. note:: + - Requires ``ansible_connection=httpapi`` with the VyOS httpapi plugin. + - ``ansible_network_os`` must be set to ``vyos.rest.vyos``. + - Unlike the CLI collection's ``vyos_config``, this module does not support ``backup``, ``confirm``, or ``comment`` options as these are CLI-specific. + - Commands are parsed from CLI string format into REST API path arrays. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Apply configuration lines + vyos.rest.vyos_config: + lines: + - set system host-name router1 + - set system domain-name example.com + - set interfaces ethernet eth0 description "WAN" + save: true + + - name: Delete configuration + vyos.rest.vyos_config: + lines: + - delete protocols bgp + save: true + + - name: Apply config from file + vyos.rest.vyos_config: + src: /tmp/vyos_config.txt + match: none + save: true + + - name: Always apply without matching + vyos.rest.vyos_config: + lines: + - set system host-name router1 + match: none + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ commands + +
+ list +
+
always +
List of commands applied to the device.
+
+
+
+ response + +
+ dictionary +
+
when changes are applied +
Raw API response from the device.
+
+
+
+ saved + +
+ boolean +
+
when save is true and changes were made +
Whether the configuration was saved to disk.
+
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/tests/integration/targets/vyos_config/aliases b/tests/integration/targets/vyos_config/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_config/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_config/defaults/main.yaml b/tests/integration/targets/vyos_config/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_config/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_config/tasks/httpapi.yaml b/tests/integration/targets/vyos_config/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_config/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_config/tasks/main.yaml b/tests/integration/targets/vyos_config/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_config/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_config/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_config/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..4e7a37a --- /dev/null +++ b/tests/integration/targets/vyos_config/tests/httpapi/_remove_config.yaml @@ -0,0 +1,7 @@ +--- +- name: Remove test configuration + vyos.rest.vyos_config: + lines: + - delete interfaces ethernet eth1 description + match: none + ignore_errors: true diff --git a/tests/integration/targets/vyos_config/tests/httpapi/delete.yaml b/tests/integration/targets/vyos_config/tests/httpapi/delete.yaml new file mode 100644 index 0000000..0db8e84 --- /dev/null +++ b/tests/integration/targets/vyos_config/tests/httpapi/delete.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_config delete integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Setup - add description + vyos.rest.vyos_config: + lines: + - set interfaces ethernet eth1 description "To be deleted" + match: none + + - name: Delete configuration + register: result + vyos.rest.vyos_config: + lines: + - delete interfaces ethernet eth1 description + save: true + + - assert: + that: + - result.changed == true + + - name: Delete again (IDEMPOTENT) + register: result + vyos.rest.vyos_config: + lines: + - delete interfaces ethernet eth1 description + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_config/tests/httpapi/lines.yaml b/tests/integration/targets/vyos_config/tests/httpapi/lines.yaml new file mode 100644 index 0000000..11b69ce --- /dev/null +++ b/tests/integration/targets/vyos_config/tests/httpapi/lines.yaml @@ -0,0 +1,44 @@ +--- +- debug: + msg: START vyos_config lines integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Apply configuration lines + register: result + vyos.rest.vyos_config: + lines: + - set interfaces ethernet eth1 description "Test description" + save: true + + - assert: + that: + - result.changed == true + - result.commands | length == 1 + + - name: Apply same config (match=line IDEMPOTENT) + register: result + vyos.rest.vyos_config: + lines: + - set interfaces ethernet eth1 description "Test description" + match: line + + - assert: + that: + - result.changed == false + - result.commands == [] + + - name: Apply with match=none (always apply) + register: result + vyos.rest.vyos_config: + lines: + - set interfaces ethernet eth1 description "Test description" + match: none + + - assert: + that: + - result.changed == true + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_config/vars/main.yaml b/tests/integration/targets/vyos_config/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_config/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/unit/modules/test_vyos_config.py b/tests/unit/modules/test_vyos_config.py new file mode 100644 index 0000000..749aa82 --- /dev/null +++ b/tests/unit/modules/test_vyos_config.py @@ -0,0 +1,107 @@ +# -*- 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_config import ( + filter_commands, + parse_commands, + parse_line, +) + + +class TestVyOSConfigParseLine(unittest.TestCase): + + def test_set_single_value(self): + op, path = parse_line("set system host-name router1") + self.assertEqual(op, "set") + self.assertEqual(path, ["system", "host-name", "router1"]) + + def test_delete(self): + op, path = parse_line("delete protocols bgp") + self.assertEqual(op, "delete") + self.assertEqual(path, ["protocols", "bgp"]) + + def test_quoted_value(self): + op, path = parse_line('set interfaces ethernet eth0 description "My WAN"') + self.assertEqual(op, "set") + self.assertEqual(path, ["interfaces", "ethernet", "eth0", "description", "My WAN"]) + + def test_blank_line(self): + self.assertIsNone(parse_line("")) + + def test_comment_line(self): + self.assertIsNone(parse_line("# this is a comment")) + + def test_whitespace_only(self): + self.assertIsNone(parse_line(" ")) + + def test_invalid_op(self): + self.assertIsNone(parse_line("get system host-name")) + + +class TestVyOSConfigParseCommands(unittest.TestCase): + + def test_mixed_lines(self): + lines = [ + "# comment", + "", + "set system host-name router1", + "delete protocols bgp", + ] + result = parse_commands(lines) + self.assertEqual(len(result), 2) + self.assertEqual(result[0], ("set", ["system", "host-name", "router1"])) + self.assertEqual(result[1], ("delete", ["protocols", "bgp"])) + + +class TestVyOSConfigFilterCommands(unittest.TestCase): + + def setUp(self): + self.mock_vyos = MagicMock() + + def test_set_already_exists(self): + # API returns {"host-name": "router1"} for path ["system", "host-name"] + self.mock_vyos.get_config = MagicMock( + return_value={"host-name": "router1"}, + ) + cmds = [("set", ["system", "host-name", "router1"])] + result = filter_commands(cmds, self.mock_vyos) + self.assertEqual(result, []) + + def test_set_different_value(self): + self.mock_vyos.get_config = MagicMock( + return_value={"host-name": "old-name"}, + ) + cmds = [("set", ["system", "host-name", "new-name"])] + result = filter_commands(cmds, self.mock_vyos) + self.assertEqual(len(result), 1) + + def test_set_not_present(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + cmds = [("set", ["system", "host-name", "router1"])] + result = filter_commands(cmds, self.mock_vyos) + self.assertEqual(len(result), 1) + + def test_delete_exists(self): + self.mock_vyos.get_config = MagicMock( + return_value={"description": "some desc"}, + ) + cmds = [("delete", ["interfaces", "ethernet", "eth0", "description"])] + result = filter_commands(cmds, self.mock_vyos) + self.assertEqual(len(result), 1) + + def test_delete_not_exists(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + cmds = [("delete", ["interfaces", "ethernet", "eth0", "description"])] + result = filter_commands(cmds, self.mock_vyos) + self.assertEqual(result, []) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3