summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-07-06 15:40:34 +1000
committeromnom62 <omnom62@outlook.com>2026-07-06 15:40:34 +1000
commitb34a4e34d9321413bc79d2f29aed13ef897084c8 (patch)
tree67230a32e6654c8ad34cefc4226f3886d4eac934
parent6cc3f10560311b65fd5ddf3d389a141f321b1b69 (diff)
downloadrest.vyos-b34a4e34d9321413bc79d2f29aed13ef897084c8.tar.gz
rest.vyos-b34a4e34d9321413bc79d2f29aed13ef897084c8.zip
T8989: Wave 4 vyos_config module with integration and unit tests
-rw-r--r--docs/vyos.rest.vyos_config_module.rst228
-rw-r--r--tests/integration/targets/vyos_config/aliases1
-rw-r--r--tests/integration/targets/vyos_config/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_config/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_config/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_config/tests/httpapi/_remove_config.yaml7
-rw-r--r--tests/integration/targets/vyos_config/tests/httpapi/delete.yaml37
-rw-r--r--tests/integration/targets/vyos_config/tests/httpapi/lines.yaml44
-rw-r--r--tests/integration/targets/vyos_config/vars/main.yaml2
-rw-r--r--tests/unit/modules/test_vyos_config.py107
10 files changed, 455 insertions, 0 deletions
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
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="1">Parameter</th>
+ <th>Choices/<font color="blue">Defaults</font></th>
+ <th width="100%">Comments</th>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>lines</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Ordered list of <code>set</code> or <code>delete</code> commands to apply.</div>
+ <div>Commands should be in standard VyOS CLI format, e.g. <code>set system host-name router1</code> or <code>delete protocols bgp</code>.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>match</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li><div style="color: blue"><b>line</b>&nbsp;&larr;</div></li>
+ <li>none</li>
+ </ul>
+ </td>
+ <td>
+ <div>Controls how commands are matched against the running configuration.</div>
+ <div><code>line</code> checks each command against the running config and only applies commands that would change the configuration.</div>
+ <div><code>none</code> applies all commands without checking the running config.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>save</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li><div style="color: blue"><b>no</b>&nbsp;&larr;</div></li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Save the configuration to disk after applying changes.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>src</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">path</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Path to a file containing <code>set</code>/<code>delete</code> commands, one per line.</div>
+ <div>Blank lines and lines starting with <code>#</code> are ignored.</div>
+ <div>Mutually exclusive with <code>lines</code>.</div>
+ </td>
+ </tr>
+ </table>
+ <br/>
+
+
+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 <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
+
+.. raw:: html
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="1">Key</th>
+ <th>Returned</th>
+ <th width="100%">Description</th>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>commands</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>always</td>
+ <td>
+ <div>List of commands applied to the device.</div>
+ <br/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>response</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when changes are applied</td>
+ <td>
+ <div>Raw API response from the device.</div>
+ <br/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>saved</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>when save is true and changes were made</td>
+ <td>
+ <div>Whether the configuration was saved to disk.</div>
+ <br/>
+ </td>
+ </tr>
+ </table>
+ <br/><br/>
+
+
+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()