summaryrefslogtreecommitdiff
path: root/plugins/module_utils/vyos.py
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-05-06 08:08:27 +1000
committeromnom62 <omnom62@outlook.com>2026-05-06 08:08:27 +1000
commit9a3fa5777e7203d46faf1185cfb56c4fc121d885 (patch)
tree80988400ba3582cfc97ff6445fb8f97c724d3fc6 /plugins/module_utils/vyos.py
parentc2493986714604aa992bea28628b74587b3b51bd (diff)
downloadrest.vyos-9a3fa5777e7203d46faf1185cfb56c4fc121d885.tar.gz
rest.vyos-9a3fa5777e7203d46faf1185cfb56c4fc121d885.zip
More modules
Diffstat (limited to 'plugins/module_utils/vyos.py')
-rw-r--r--plugins/module_utils/vyos.py231
1 files changed, 146 insertions, 85 deletions
diff --git a/plugins/module_utils/vyos.py b/plugins/module_utils/vyos.py
index 469fbd3..87fc37b 100644
--- a/plugins/module_utils/vyos.py
+++ b/plugins/module_utils/vyos.py
@@ -1,104 +1,165 @@
-from ansible.module_utils.connection import Connection
+"""
+VyOSModule — high-level wrapper used by vyos.rest resource modules.
+
+Provides ``get_config()``, ``apply_commands()``, and ``save_config()``
+on top of ``VyOSRestClient``, so resource modules can work with simple
+``("set", path)`` / ``("delete", path)`` command tuples rather than
+calling the REST client directly.
+
+Version-adaptive paths
+----------------------
+Some VyOS config paths changed between minor versions (e.g. the NTP
+``allow-client address`` node was removed in 1.5+). ``apply_commands``
+handles this transparently: if a ``set`` command is rejected by the device
+it retries with the last path segment removed (one level up), covering
+the most common schema simplifications. A ``delete`` that fails is
+treated as a no-op (already absent).
+"""
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VyOSRestClient,
+ VyOSRestError,
+)
class VyOSModule:
+ """Thin wrapper around VyOSRestClient for resource modules.
+
+ Args:
+ module: AnsibleModule instance.
+ """
def __init__(self, module):
- self.module = module
-
- if not module._socket_path:
- module.fail_json(msg="Connection must be httpapi")
-
- self.connection = Connection(module._socket_path)
- self.existing = {}
-
- # ----------------------------------------------------------
- # Low-level transport
- # ----------------------------------------------------------
-
- def _send(self, path, op=None, path_list=None, commands=None):
- payload = {}
- if op:
- payload["op"] = op
- if path_list:
- payload["path"] = path_list
- if commands:
- payload["commands"] = commands
-
- response = self.connection.send_request(
- path=path,
- method="POST",
- data=payload,
- )
-
- if not response:
- self.module.fail_json(msg="Empty response from device")
-
- if not response.get("success"):
- error = response.get("error", "")
- # VyOS returns an error when path has no config — treat as empty
- if "empty" in error.lower() or "path is empty" in error.lower():
- return {}
- self.module.fail_json(
- msg="VyOS API error: {0}".format(error),
- response=response,
- )
+ self._module = module
+ self._client = VyOSRestClient(module)
- return response.get("data") or {}
+ # ------------------------------------------------------------------
+ # Read
+ # ------------------------------------------------------------------
- # ----------------------------------------------------------
- # Config retrieval
- # ----------------------------------------------------------
+ def get_config(self, path=None):
+ """Retrieve the configuration subtree at *path*.
- def get_config(self, path_list):
- """
- Fetch config at path_list via showConfig.
- Returns empty dict when path has no configuration.
+ Args:
+ path (list, optional): Config path tokens. ``[]`` or ``None``
+ returns the entire running configuration.
+
+ Returns:
+ dict: Configuration subtree, or ``{}`` if the path doesn't exist.
"""
- result = self._send(
- path="/retrieve",
- op="showConfig",
- path_list=path_list,
- )
- self.existing = result
- return result
-
- # ----------------------------------------------------------
- # Config application
- # ----------------------------------------------------------
+ try:
+ result = self._client.retrieve_show_config(path or [])
+ return result.get("data") or {}
+ except VyOSRestError:
+ return {}
+
+ # ------------------------------------------------------------------
+ # Write
+ # ------------------------------------------------------------------
def apply_commands(self, commands):
+ """Execute a list of ``(op, path)`` command tuples against the device.
+
+ Args:
+ commands (list): Each item is a 2-tuple ``(op, path)`` where:
+ - *op* is ``"set"`` or ``"delete"``
+ - *path* is a list of config path tokens
+
+ Optionally a 3-tuple ``(op, path, value)`` for leaf nodes
+ that carry a value.
+
+ Returns:
+ list: Results from each command (dicts with ``op`` and ``path``).
+
+ Raises:
+ VyOSRestError: If a command fails and cannot be recovered.
+
+ Notes:
+ **Version-adaptive set:** if a ``set`` is rejected by the device,
+ the method retries once with the last path segment removed.
+ This handles schema simplifications between VyOS releases
+ (e.g. ``allow-client address <prefix>`` -> ``allow-client <prefix>``
+ in VyOS 1.5+).
+
+ **Idempotent delete:** ``delete`` failures are silently ignored
+ (the path is already absent).
"""
- Send a list of commands to /configure.
- Accepts tuples (op, path) or dicts {"op": ..., "path": ...}.
- Returns the raw API response dict.
- Raises via fail_json on API error.
- Does NOT call exit_json — the caller controls module exit.
- """
- if not commands:
- return {}
+ results = []
- payload_commands = []
for cmd in commands:
- if isinstance(cmd, tuple):
- op, path = cmd
- payload_commands.append({"op": op, "path": path})
- elif isinstance(cmd, dict):
- payload_commands.append(cmd)
+ # Support two command formats:
+ # Tuple: ("set", ["path", "tokens"]) or ("set", ["path"], "value")
+ # Dict: {"op": "set", "path": ["path", "tokens"]}
+ if isinstance(cmd, dict):
+ op = cmd["op"]
+ path = list(cmd["path"])
+ value = cmd.get("value")
+ else:
+ op = cmd[0]
+ path = list(cmd[1])
+ value = cmd[2] if len(cmd) > 2 else None
+
+ if op == "set":
+ results.append(self._apply_set(path, value))
+ elif op == "delete":
+ results.append(self._apply_delete(path))
else:
- self.module.fail_json(
- msg="Invalid command format: {0}".format(cmd),
+ self._module.fail_json(
+ msg="Unknown command op '{op}' in apply_commands".format(op=op),
)
- return self._send(
- path="/configure",
- commands=payload_commands,
- )
+ return results
+
+ def _apply_set(self, path, value=None):
+ """Set a config path, retrying with a shortened path on failure."""
+ try:
+ self._client.configure_set(path, value)
+ return {"op": "set", "path": path, "status": "ok"}
+ except VyOSRestError as exc:
+ # Retry without the second-to-last segment (version-adaptive).
+ # Example: ["service","ntp","allow-client","address","10.0.0.0/24"]
+ # -> ["service","ntp","allow-client","10.0.0.0/24"]
+ # Only attempt if path is long enough to have an intermediate node.
+ if len(path) >= 3:
+ short_path = path[:-2] + [path[-1]]
+ try:
+ self._client.configure_set(short_path, value)
+ return {"op": "set", "path": short_path, "status": "ok-adapted"}
+ except VyOSRestError:
+ pass
+ raise VyOSRestError(
+ "set {p} failed: {e}".format(p=" ".join(path), e=str(exc)),
+ )
- def save_config(self):
- """
- Persist running config to disk.
- Returns True on success, fails via fail_json on error.
+ def _apply_delete(self, path):
+ """Delete a config path; silently ignore if already absent."""
+ try:
+ self._client.configure_delete(path)
+ return {"op": "delete", "path": path, "status": "ok"}
+ except VyOSRestError:
+ return {"op": "delete", "path": path, "status": "noop"}
+
+ # ------------------------------------------------------------------
+ # Persist
+ # ------------------------------------------------------------------
+
+ def save_config(self, file_path=None):
+ """Save the running configuration to disk.
+
+ Args:
+ file_path (str, optional): Destination path on the device.
+ Defaults to ``/config/config.boot``.
+
+ Returns:
+ bool: ``True`` if saved successfully, ``False`` otherwise.
"""
- self._send(path="/config-file", op="save")
- return True
+ try:
+ self._client.config_file_save(file_path)
+ return True
+ except VyOSRestError:
+ return False