summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-03-02 21:20:30 +1000
committeromnom62 <omnom62@outlook.com>2026-03-02 21:20:30 +1000
commit7952a8645124ef160c2759a2dae9e9cb32eac1a9 (patch)
treea01436ff4f4af14d7386715e9e7c613f05d5ac2c /plugins
downloadrest.vyos-7952a8645124ef160c2759a2dae9e9cb32eac1a9.tar.gz
rest.vyos-7952a8645124ef160c2759a2dae9e9cb32eac1a9.zip
first commit
Diffstat (limited to 'plugins')
-rw-r--r--plugins/README.md31
-rw-r--r--plugins/httpapi/vyos.py65
-rw-r--r--plugins/module_utils/utils.py40
-rw-r--r--plugins/module_utils/vyos.py177
-rw-r--r--plugins/modules/vyos_ntp_global.py379
5 files changed, 692 insertions, 0 deletions
diff --git a/plugins/README.md b/plugins/README.md
new file mode 100644
index 0000000..615f703
--- /dev/null
+++ b/plugins/README.md
@@ -0,0 +1,31 @@
+# Collections Plugins Directory
+
+This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
+is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
+would contain module utils and modules respectively.
+
+Here is an example directory of the majority of plugins currently supported by Ansible:
+
+```
+└── plugins
+ ├── action
+ ├── become
+ ├── cache
+ ├── callback
+ ├── cliconf
+ ├── connection
+ ├── filter
+ ├── httpapi
+ ├── inventory
+ ├── lookup
+ ├── module_utils
+ ├── modules
+ ├── netconf
+ ├── shell
+ ├── strategy
+ ├── terminal
+ ├── test
+ └── vars
+```
+
+A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.18/plugins/plugins.html).
diff --git a/plugins/httpapi/vyos.py b/plugins/httpapi/vyos.py
new file mode 100644
index 0000000..049ef97
--- /dev/null
+++ b/plugins/httpapi/vyos.py
@@ -0,0 +1,65 @@
+from ansible.plugins.httpapi import HttpApiBase
+from urllib.parse import urlencode
+import json
+
+
+DOCUMENTATION = r'''
+---
+httpapi: vyos
+short_description: VyOS REST API
+description:
+ - HTTPAPI plugin for interacting with VyOS REST API.
+options:
+ api_key:
+ description: VyOS API key
+ required: true
+ type: str
+ vars:
+ - name: ansible_httpapi_api_key
+ env:
+ - name: ANSIBLE_HTTPAPI_API_KEY
+'''
+
+
+class HttpApi(HttpApiBase):
+
+ def set_options(self, task_keys=None, var_options=None, direct=None):
+ super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
+
+ def send_request(self, path, data=None, method="POST", headers=None):
+
+ api_key = self.get_option("api_key")
+
+ form_data = {}
+
+ if data is not None:
+ if not isinstance(data, str):
+ data = json.dumps(data)
+ form_data["data"] = data
+
+ form_data["key"] = api_key
+
+ body = urlencode(form_data).encode("utf-8")
+
+ if headers is None:
+ headers = {}
+
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
+
+ status, response = self.connection.send(
+ path,
+ body,
+ method=method,
+ headers=headers
+ )
+
+ if hasattr(response, "read"):
+ response = response.read()
+
+ if isinstance(response, bytes):
+ response = response.decode("utf-8")
+
+ if isinstance(response, str):
+ response = json.loads(response)
+
+ return response \ No newline at end of file
diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py
new file mode 100644
index 0000000..f256247
--- /dev/null
+++ b/plugins/module_utils/utils.py
@@ -0,0 +1,40 @@
+def normalize_to_list(value):
+ """
+ Normalize VyOS REST return values to list.
+
+ Handles:
+ dict -> keys
+ list -> same
+ str -> [value]
+ None -> []
+ """
+ if isinstance(value, dict):
+ return list(value.keys())
+
+ if isinstance(value, list):
+ return value
+
+ if isinstance(value, str):
+ return [value]
+
+ return []
+
+
+def normalize_to_dict(value):
+ """
+ Normalize VyOS REST return values to dict.
+
+ list -> {item: {}}
+ str -> {value: {}}
+ dict -> same
+ """
+ if isinstance(value, dict):
+ return value
+
+ if isinstance(value, list):
+ return {v: {} for v in value}
+
+ if isinstance(value, str):
+ return {value: {}}
+
+ return {} \ No newline at end of file
diff --git a/plugins/module_utils/vyos.py b/plugins/module_utils/vyos.py
new file mode 100644
index 0000000..aa17a9f
--- /dev/null
+++ b/plugins/module_utils/vyos.py
@@ -0,0 +1,177 @@
+from ansible.module_utils.connection import Connection
+import json
+
+
+class VyOSModule:
+
+ 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 = {}
+ self.commands = []
+
+ #
+ # 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"):
+ self.module.fail_json(msg="VyOS error", response=response)
+
+ return response.get("data", {})
+
+ #
+ # Fetch config
+ #
+ def get_config(self, path_list):
+
+ self.existing = self._send(
+ path="/retrieve",
+ op="showConfig",
+ path_list=path_list
+ )
+
+ return self.existing
+
+ #
+ # Add command
+ #
+ def add_command(self, cmd):
+
+ self.commands.append(cmd)
+
+ #
+ # Diff
+ #
+ def get_diff(self):
+
+ diff = []
+
+ existing_servers = []
+
+ #
+ # Extract existing servers safely
+ #
+ if isinstance(self.existing, dict):
+ server_block = self.existing.get("server", {})
+ if isinstance(server_block, dict):
+ existing_servers = list(server_block.keys())
+
+ #
+ # Compare commands vs existing state
+ #
+ for cmd in self.commands:
+
+ op = cmd.get("op")
+ path = cmd.get("path", [])
+
+ if not path:
+ continue
+
+ server = path[-1]
+
+ if op == "set":
+ if server not in existing_servers:
+ diff.append(cmd)
+
+ elif op == "delete":
+ if server in existing_servers:
+ diff.append(cmd)
+
+ return diff
+ #
+ # Apply config
+ #
+ def apply_config(self, diff):
+
+ if not diff:
+ self.module.exit_json(
+ changed=False,
+ msg="Already configured"
+ )
+
+ response = self._send(
+ path="/configure",
+ commands=diff
+ )
+
+ self.module.exit_json(
+ changed=True,
+ commands=diff,
+ response=response
+ )
+
+ #
+ # Apply commands
+ #
+ def apply_commands(self, commands):
+
+ if not commands:
+ self.module.exit_json(
+ changed=False,
+ commands=[],
+ msg="Already configured"
+ )
+
+ # convert tuples to dicts for API
+ 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)
+ else:
+ self.module.fail_json(msg=f"Invalid command type: {cmd}")
+
+ response = self._send(
+ path="/configure",
+ commands=payload_commands
+ )
+
+ self.module.exit_json(
+ changed=True,
+ commands=commands,
+ response=response
+ )
+ #
+ # Delete config
+ #
+ def delete_config(self, cmds):
+
+ response = self._send(
+ path="/configure",
+ commands=cmds
+ )
+
+ self.module.exit_json(
+ changed=True,
+ commands=cmds,
+ response=response
+ ) \ No newline at end of file
diff --git a/plugins/modules/vyos_ntp_global.py b/plugins/modules/vyos_ntp_global.py
new file mode 100644
index 0000000..746838c
--- /dev/null
+++ b/plugins/modules/vyos_ntp_global.py
@@ -0,0 +1,379 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+from ansible_collections.vyos.rest.plugins.module_utils.utils import normalize_to_list
+
+# ------------------------------------------------------------
+# Helpers
+# ------------------------------------------------------------
+
+def normalize_config(config):
+
+ result = {
+ "allow_clients": sorted(config.get("allow_clients", [])),
+ "listen_addresses": sorted(config.get("listen_addresses", [])),
+ "servers": {}
+ }
+
+ for s in config.get("servers", []):
+
+ name = s["server"]
+
+ result["servers"][name] = sorted(s.get("options", []))
+
+ return result
+
+def normalize_servers(value):
+ result = {}
+
+ if isinstance(value, dict):
+ for server, data in value.items():
+
+ if isinstance(data, dict):
+ result[server] = sorted(list(data.keys()))
+
+ elif isinstance(data, list):
+ result[server] = sorted(data)
+
+ elif isinstance(data, str):
+ result[server] = [data]
+
+ else:
+ result[server] = []
+
+ elif isinstance(value, list):
+ for server in value:
+ result[server] = []
+
+ elif isinstance(value, str):
+ result[value] = []
+
+ return result
+
+
+def get_running_config(vyos):
+
+ raw = vyos.get_config(["service", "ntp"])
+
+ result = {
+ "allow_clients": [],
+ "listen_addresses": [],
+ "servers": {}
+ }
+
+ if not raw:
+ return result
+
+ allow_raw = raw.get("allow-client", {}).get("address", [])
+ result["allow_clients"] = sorted(normalize_to_list(allow_raw))
+
+ listen_raw = raw.get("listen-address", [])
+ result["listen_addresses"] = sorted(normalize_to_list(listen_raw))
+
+ servers_raw = raw.get("server", {})
+ result["servers"] = normalize_servers(servers_raw)
+
+ return result
+
+def build_commands(desired, existing, state):
+
+ cmds = []
+
+ # overridden = delete all then merged
+ if state == "overridden":
+
+ if existing["allow_clients"]:
+ cmds.append(("delete", ["service", "ntp", "allow-clients"]))
+
+ if existing["listen_addresses"]:
+ cmds.append(("delete", ["service", "ntp", "listen-address"]))
+
+ if existing["servers"]:
+ cmds.append(("delete", ["service", "ntp", "server"]))
+
+ state = "merged"
+
+ cmds += diff_list(
+ "allow-client", "address", # singular
+ desired["allow_clients"],
+ existing["allow_clients"],
+ state
+ )
+
+ # listen_addresses
+ cmds += diff_list(
+ "listen-address", None,
+ desired["listen_addresses"],
+ existing["listen_addresses"],
+ state
+ )
+
+ # servers
+ cmds += diff_servers(
+ desired["servers"],
+ existing["servers"],
+ state
+ )
+
+ return cmds
+
+
+def diff_list(node, subnode, desired, existing, state):
+
+ cmds = []
+
+ desired = set(desired)
+ existing = set(existing)
+
+ if state in ["merged", "replaced"]:
+
+ for v in desired - existing:
+
+ path = ["service", "ntp", node]
+
+ if subnode:
+ path += [subnode, v]
+ else:
+ path += [v]
+
+ cmds.append(("set", path))
+
+ if state in ["replaced", "deleted"]:
+
+ for v in existing - desired:
+
+ path = ["service", "ntp", node]
+
+ if subnode:
+ path += [subnode, v]
+ else:
+ path += [v]
+
+ cmds.append(("delete", path))
+
+ return cmds
+
+
+def diff_servers(desired, existing, state):
+
+ cmds = []
+
+ desired_set = set(desired.keys())
+ existing_set = set(existing.keys())
+
+ # add/update
+ if state in ["merged", "replaced"]:
+
+ for server in desired_set:
+
+ desired_opts = set(desired[server])
+ existing_opts = set(existing.get(server, []))
+
+ # add server
+ if server not in existing_set:
+ cmds.append(("set", ["service", "ntp", "server", server]))
+
+ # add options
+ for opt in desired_opts - existing_opts:
+
+ cmds.append(
+ ("set", ["service", "ntp", "server", server, opt])
+ )
+
+ # remove options (replaced only)
+ if state == "replaced":
+
+ for opt in existing_opts - desired_opts:
+
+ cmds.append(
+ ("delete", ["service", "ntp", "server", server, opt])
+ )
+
+ # delete
+ if state in ["replaced", "deleted"]:
+
+ for server in existing_set - desired_set:
+
+ cmds.append(("delete", ["service", "ntp", "server", server]))
+
+ return cmds
+
+
+def parse_running_config(text):
+
+ result = {
+ "allow_clients": [],
+ "listen_addresses": [],
+ "servers": {}
+ }
+
+ for line in text.splitlines():
+
+ parts = line.strip().split()
+
+ if len(parts) < 4:
+ continue
+
+ if parts[3] == "allow-clients":
+ result["allow_clients"].append(parts[-1])
+
+ elif parts[3] == "listen-address":
+ result["listen_addresses"].append(parts[-1])
+
+ elif parts[3] == "server":
+
+ server = parts[4]
+
+ if server not in result["servers"]:
+ result["servers"][server] = []
+
+ if len(parts) > 5:
+ result["servers"][server].append(parts[5])
+
+ return result
+
+
+def render_commands(config):
+
+ cmds = []
+
+ for c in config["allow_clients"]:
+ cmds.append(f"set service ntp allow-clients address {c}")
+
+ for l in config["listen_addresses"]:
+ cmds.append(f"set service ntp listen-address {l}")
+
+ for server, opts in config["servers"].items():
+
+ if not opts:
+ cmds.append(f"set service ntp server {server}")
+
+ for opt in opts:
+ cmds.append(f"set service ntp server {server} {opt}")
+
+ return cmds
+
+# ------------------------------------------------------------
+# Main
+# ------------------------------------------------------------
+
+def main():
+
+ argument_spec = dict(
+
+ config=dict(
+ type="dict",
+ options=dict(
+ allow_clients=dict(type="list", elements="str"),
+ listen_addresses=dict(type="list", elements="str"),
+ servers=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ server=dict(type="str", required=True),
+ options=dict(type="list", elements="str")
+ )
+ )
+ )
+ ),
+
+ running_config=dict(type="str"),
+
+ state=dict(
+ default="merged",
+ choices=[
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ "rendered",
+ "parsed"
+ ]
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or {}
+
+ result = {
+ "changed": False
+ }
+
+ # --------------------------------------------------------
+ # parsed
+ # --------------------------------------------------------
+
+ if state == "parsed":
+
+ parsed = parse_running_config(module.params["running_config"])
+
+ module.exit_json(parsed=parsed)
+
+ # --------------------------------------------------------
+ # rendered
+ # --------------------------------------------------------
+
+ desired = normalize_config(config)
+
+ if state == "rendered":
+
+ module.exit_json(rendered=render_commands(desired))
+
+ # --------------------------------------------------------
+ # gathered
+ # --------------------------------------------------------
+
+ existing = get_running_config(vyos)
+
+ if state == "gathered":
+
+ module.exit_json(gathered=existing)
+
+ # --------------------------------------------------------
+ # deleted
+ # --------------------------------------------------------
+
+ if state == "deleted":
+
+ desired = {
+ "allow_clients": [],
+ "listen_addresses": [],
+ "servers": {}
+ }
+
+ # --------------------------------------------------------
+ # diff engine
+ # --------------------------------------------------------
+
+ commands = build_commands(desired, existing, state)
+
+ result["before"] = existing
+
+ result["commands"] = commands
+
+ if commands:
+
+ result["changed"] = True
+
+ if not module.check_mode:
+
+ vyos.apply_commands(commands)
+
+ result["after"] = desired
+
+ module.exit_json(**result)
+
+
+if __name__ == "__main__":
+ main() \ No newline at end of file