summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-03-20 21:50:13 +1000
committeromnom62 <omnom62@outlook.com>2026-03-20 21:50:13 +1000
commit7599ba96bc3b659cb2a5f00b1be46ab6f55f48a6 (patch)
tree35b39a126fe94f8917352aa2c229d3022d1223e4 /plugins
parent9dc03bb92e08455812e0969505495309456a4bfb (diff)
downloadrest.vyos-7599ba96bc3b659cb2a5f00b1be46ab6f55f48a6.tar.gz
rest.vyos-7599ba96bc3b659cb2a5f00b1be46ab6f55f48a6.zip
precommit changes
Diffstat (limited to 'plugins')
-rw-r--r--plugins/httpapi/vyos.py21
-rw-r--r--plugins/module_utils/utils.py2
-rw-r--r--plugins/module_utils/vyos.py37
-rw-r--r--plugins/modules/vyos_banner.py366
-rw-r--r--plugins/modules/vyos_hostname.py35
-rw-r--r--plugins/modules/vyos_ntp_global.py58
6 files changed, 452 insertions, 67 deletions
diff --git a/plugins/httpapi/vyos.py b/plugins/httpapi/vyos.py
index 83a753f..9340261 100644
--- a/plugins/httpapi/vyos.py
+++ b/plugins/httpapi/vyos.py
@@ -1,14 +1,17 @@
-from ansible.plugins.httpapi import HttpApiBase
-from urllib.parse import urlencode
import json
+from urllib.parse import urlencode
-DOCUMENTATION = r'''
+from ansible.plugins.httpapi import HttpApiBase
+
+
+DOCUMENTATION = r"""
---
httpapi: vyos
short_description: VyOS REST API
description:
- HTTPAPI plugin for interacting with VyOS REST API.
+author: Evgeny Molotkov (@eomnom62)
options:
api_key:
description: VyOS API key
@@ -20,13 +23,17 @@ options:
ini:
- section: httpapi
key: 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)
+ super().set_options(
+ task_keys=task_keys,
+ var_options=var_options,
+ direct=direct,
+ )
def send_request(self, path, data=None, method="POST", headers=None):
@@ -59,7 +66,7 @@ class HttpApi(HttpApiBase):
path,
body,
method=method,
- headers=headers
+ headers=headers,
)
if hasattr(response, "read"):
@@ -71,4 +78,4 @@ class HttpApi(HttpApiBase):
if isinstance(response, str):
response = json.loads(response)
- return response \ No newline at end of file
+ return response
diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py
index f256247..f8a738f 100644
--- a/plugins/module_utils/utils.py
+++ b/plugins/module_utils/utils.py
@@ -37,4 +37,4 @@ def normalize_to_dict(value):
if isinstance(value, str):
return {value: {}}
- return {} \ No newline at end of file
+ return {}
diff --git a/plugins/module_utils/vyos.py b/plugins/module_utils/vyos.py
index 40926d5..525ed0c 100644
--- a/plugins/module_utils/vyos.py
+++ b/plugins/module_utils/vyos.py
@@ -1,5 +1,4 @@
from ansible.module_utils.connection import Connection
-import json
class VyOSModule:
@@ -32,11 +31,11 @@ class VyOSModule:
if commands:
payload["commands"] = commands
- response = self.connection.send_request(
- path=path,
- method="POST",
- data=payload
- )
+ response = self.connection.send_request(
+ path=path,
+ method="POST",
+ data=payload,
+ )
if not response:
self.module.fail_json(msg="Empty response from device")
@@ -54,7 +53,7 @@ class VyOSModule:
self.existing = self._send(
path="/retrieve",
op="showConfig",
- path_list=path_list
+ path_list=path_list,
)
return self.existing
@@ -105,6 +104,7 @@ class VyOSModule:
diff.append(cmd)
return diff
+
#
# Apply config
#
@@ -113,18 +113,18 @@ class VyOSModule:
if not diff:
self.module.exit_json(
changed=False,
- msg="Already configured"
+ msg="Already configured",
)
response = self._send(
path="/configure",
- commands=diff
+ commands=diff,
)
self.module.exit_json(
changed=True,
commands=diff,
- response=response
+ response=response,
)
#
@@ -136,7 +136,7 @@ class VyOSModule:
self.module.exit_json(
changed=False,
commands=[],
- msg="Already configured"
+ msg="Already configured",
)
# convert tuples to dicts for API
@@ -152,7 +152,7 @@ class VyOSModule:
response = self._send(
path="/configure",
- commands=payload_commands
+ commands=payload_commands,
)
self.save_config()
@@ -162,6 +162,7 @@ class VyOSModule:
commands=commands,
response=response,
)
+
#
# Delete config
#
@@ -169,20 +170,20 @@ class VyOSModule:
response = self._send(
path="/configure",
- commands=cmds
+ commands=cmds,
)
self.module.exit_json(
changed=True,
commands=cmds,
- response=response
+ response=response,
)
-
+
def save_config(self):
- response = self._send(
+ response = self._send( # noqa: F841
path="/config-file",
- op="save"
+ op="save",
)
- return True \ No newline at end of file
+ return True
diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py
new file mode 100644
index 0000000..0c35f78
--- /dev/null
+++ b/plugins/modules/vyos_banner.py
@@ -0,0 +1,366 @@
+#!/usr/bin/python
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+
+
+DOCUMENTATION = r"""
+---
+module: vyos_banner
+short_description: Manage login banners on VyOS devices using REST API
+description:
+ - Configure pre-login and post-login banners on VyOS devices via REST API.
+ - Supports idempotent configuration using structured data.
+ - Multiline banner text is supported.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - Your Name (@yourhandle)
+
+options:
+ config:
+ description:
+ - Banner configuration.
+ type: dict
+ required: true
+ suboptions:
+ banner:
+ description:
+ - Banner type to configure.
+ type: str
+ required: true
+ choices:
+ - pre-login
+ - post-login
+ text:
+ description:
+ - Banner text (supports multiline string).
+ type: str
+
+ state:
+ description:
+ - Desired state of the configuration.
+ type: str
+ default: merged
+ choices:
+ - merged
+ - replaced
+ - overridden
+ - deleted
+ - gathered
+
+notes:
+ - This module requires C(ansible_connection=httpapi).
+ - Banner text comparison is whitespace-normalized for idempotency.
+"""
+
+EXAMPLES = r"""
+- name: Configure pre-login banner
+ vyos.rest.vyos_banner:
+ config:
+ banner: pre-login
+ text: |
+ Unauthorized access is prohibited
+ Disconnect immediately
+ state: merged
+
+- name: Replace post-login banner
+ vyos.rest.vyos_banner:
+ config:
+ banner: post-login
+ text: |
+ Welcome to VyOS
+ state: replaced
+
+- name: Remove pre-login banner
+ vyos.rest.vyos_banner:
+ config:
+ banner: pre-login
+ state: deleted
+
+- name: Gather banner configuration
+ vyos.rest.vyos_banner:
+ config:
+ banner: pre-login
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Configuration before changes.
+ returned: always
+ type: dict
+
+after:
+ description: Configuration after changes.
+ returned: when changed
+ type: dict
+
+commands:
+ description: List of commands sent to the device.
+ returned: when changes are required
+ type: list
+
+gathered:
+ description: Current device configuration.
+ returned: when state is gathered
+ type: dict
+
+response:
+ description: Raw response from VyOS REST API.
+ returned: when changes are applied
+ type: dict
+"""
+
+
+# ------------------------------------------------------------
+# Helpers
+# ------------------------------------------------------------
+
+
+def normalize_text(text):
+ if text is None:
+ return None
+ return text.strip()
+
+
+def get_running_config(vyos, banner):
+ try:
+ raw = vyos.get_config(["system", "login", "banner"])
+ except Exception as e:
+ if "Configuration under specified path is empty" in str(e):
+ return {"banner": banner, "text": None}
+ raise
+
+ if not raw or not isinstance(raw, dict):
+ return {"banner": banner, "text": None}
+
+ val = raw.get(banner)
+
+ if not val:
+ return {"banner": banner, "text": None}
+
+ if isinstance(val, str):
+ return {
+ "banner": banner,
+ "text": val.strip(),
+ }
+
+ if isinstance(val, list):
+ lines = [line.strip() for line in val if line.strip()]
+ return {
+ "banner": banner,
+ "text": "\n".join(lines) if lines else None,
+ }
+
+ return {"banner": banner, "text": None}
+
+
+def build_commands(want, have, state):
+ """
+ Build list of VyOS REST API commands to apply `want` configuration
+ based on current `have` configuration and desired `state`.
+ Handles:
+ - missing paths
+ - idempotency
+ - multiline banners
+ - merged, replaced, overridden, deleted
+ """
+ commands = []
+
+ banner = want["banner"]
+ want_text = want.get("text")
+ have_text = have.get("text")
+
+ base_path = ["system", "login", "banner", banner]
+
+ want_lines = want_text.splitlines() if want_text else []
+ have_lines = have_text.splitlines() if have_text else []
+
+ banner_exists = have_text is not None
+ # --------------------------------------------------------
+ # deleted
+ # --------------------------------------------------------
+ if state == "deleted":
+ if banner_exists or have_text is not None:
+ commands.append(
+ {
+ "op": "delete",
+ "path": base_path,
+ },
+ )
+ return commands
+
+ # --------------------------------------------------------
+ # merged
+ # --------------------------------------------------------
+
+ if state == "merged":
+
+ # normalize both
+ want_lines = want_text.splitlines() if want_text else []
+ have_lines = have_text.splitlines() if have_text else []
+
+ # if identical → idempotent
+ if want_lines == have_lines:
+ return []
+
+ # if device empty → just push all
+ if not have_lines:
+ return [{"op": "set", "path": base_path + [line]} for line in want_lines]
+
+ # append missing lines only
+ commands = []
+ for line in want_lines:
+ if line not in have_lines:
+ commands.append(
+ {
+ "op": "set",
+ "path": base_path + [line],
+ },
+ )
+
+ return commands
+
+ # if state == "merged":
+ # # create parent path if missing
+
+ # for line in want_lines:
+ # if line not in have_lines:
+ # commands.append({
+ # "op": "set",
+ # "path": base_path + [line]
+ # })
+
+ # return commands
+
+ # --------------------------------------------------------
+ # replaced / overridden
+ # --------------------------------------------------------
+ if state in ["replaced", "overridden"]:
+ # if existing lines differ from desired lines, delete first
+ if banner_exists and want_lines != have_lines:
+ commands.append(
+ {
+ "op": "delete",
+ "path": base_path,
+ },
+ )
+ # create parent path again before setting lines
+ if want_lines:
+ commands.append(
+ {
+ "op": "set",
+ "path": base_path,
+ "value": "",
+ },
+ )
+
+ # set all desired lines
+ for line in want_lines:
+ commands.append(
+ {
+ "op": "set",
+ "path": base_path + [line],
+ },
+ )
+
+ return commands
+
+ return commands
+
+
+# ------------------------------------------------------------
+# Main
+# ------------------------------------------------------------
+
+
+def main():
+
+ argument_spec = dict(
+ config=dict(
+ type="dict",
+ required=True,
+ options=dict(
+ banner=dict(
+ type="str",
+ required=True,
+ choices=["pre-login", "post-login"],
+ ),
+ text=dict(type="str"),
+ ),
+ ),
+ state=dict(
+ default="merged",
+ choices=[
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ ],
+ ),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or {}
+
+ banner = config.get("banner")
+
+ if not banner:
+ module.fail_json(msg="banner is required in config")
+
+ have = get_running_config(vyos, banner)
+
+ # --------------------------------------------------------
+ # gathered
+ # --------------------------------------------------------
+
+ if state == "gathered":
+ module.exit_json(
+ changed=False,
+ gathered=have,
+ )
+
+ want = {
+ "banner": banner,
+ "text": config.get("text"),
+ }
+
+ commands = build_commands(want, have, state)
+
+ if module.check_mode:
+ module.exit_json(
+ changed=bool(commands),
+ commands=commands,
+ )
+ if commands:
+
+ response = vyos.apply_commands(commands)
+ saved = vyos.save_config()
+
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=want,
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(
+ changed=False,
+ before=have,
+ after=have,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_hostname.py b/plugins/modules/vyos_hostname.py
index f1c901e..9d70649 100644
--- a/plugins/modules/vyos_hostname.py
+++ b/plugins/modules/vyos_hostname.py
@@ -20,6 +20,7 @@ def get_running_config(vyos):
return {"hostname": hostname}
+
def build_commands(want, have, state):
commands = []
@@ -30,18 +31,22 @@ def build_commands(want, have, state):
if state in ["merged", "replaced", "overridden"]:
if want_host and want_host != have_host:
- commands.append({
- "op": "set",
- "path": ["system", "host-name", want_host]
- })
+ commands.append(
+ {
+ "op": "set",
+ "path": ["system", "host-name", want_host],
+ },
+ )
elif state == "deleted":
if have_host:
- commands.append({
- "op": "delete",
- "path": ["system", "host-name"]
- })
+ commands.append(
+ {
+ "op": "delete",
+ "path": ["system", "host-name"],
+ },
+ )
return commands
@@ -52,8 +57,8 @@ def main():
config=dict(
type="dict",
options=dict(
- hostname=dict(type="str")
- )
+ hostname=dict(type="str"),
+ ),
),
state=dict(
default="merged",
@@ -82,7 +87,7 @@ def main():
if state == "gathered":
module.exit_json(
changed=False,
- gathered=have
+ gathered=have,
)
commands = build_commands(want, have, state)
@@ -90,7 +95,7 @@ def main():
if module.check_mode:
module.exit_json(
changed=bool(commands),
- commands=commands
+ commands=commands,
)
if commands:
@@ -106,15 +111,15 @@ def main():
after=want,
commands=commands,
saved=saved,
- response=response
+ response=response,
)
module.exit_json(
changed=False,
before=have,
- after=have
+ after=have,
)
if __name__ == "__main__":
- main() \ No newline at end of file
+ main()
diff --git a/plugins/modules/vyos_ntp_global.py b/plugins/modules/vyos_ntp_global.py
index 746838c..d970142 100644
--- a/plugins/modules/vyos_ntp_global.py
+++ b/plugins/modules/vyos_ntp_global.py
@@ -3,22 +3,25 @@
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
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+
# ------------------------------------------------------------
# Helpers
# ------------------------------------------------------------
+
def normalize_config(config):
result = {
"allow_clients": sorted(config.get("allow_clients", [])),
"listen_addresses": sorted(config.get("listen_addresses", [])),
- "servers": {}
+ "servers": {},
}
for s in config.get("servers", []):
@@ -29,6 +32,7 @@ def normalize_config(config):
return result
+
def normalize_servers(value):
result = {}
@@ -64,7 +68,7 @@ def get_running_config(vyos):
result = {
"allow_clients": [],
"listen_addresses": [],
- "servers": {}
+ "servers": {},
}
if not raw:
@@ -81,6 +85,7 @@ def get_running_config(vyos):
return result
+
def build_commands(desired, existing, state):
cmds = []
@@ -100,25 +105,27 @@ def build_commands(desired, existing, state):
state = "merged"
cmds += diff_list(
- "allow-client", "address", # singular
+ "allow-client",
+ "address", # singular
desired["allow_clients"],
existing["allow_clients"],
- state
+ state,
)
# listen_addresses
cmds += diff_list(
- "listen-address", None,
+ "listen-address",
+ None,
desired["listen_addresses"],
existing["listen_addresses"],
- state
+ state,
)
# servers
cmds += diff_servers(
desired["servers"],
existing["servers"],
- state
+ state,
)
return cmds
@@ -183,7 +190,7 @@ def diff_servers(desired, existing, state):
for opt in desired_opts - existing_opts:
cmds.append(
- ("set", ["service", "ntp", "server", server, opt])
+ ("set", ["service", "ntp", "server", server, opt]),
)
# remove options (replaced only)
@@ -192,7 +199,7 @@ def diff_servers(desired, existing, state):
for opt in existing_opts - desired_opts:
cmds.append(
- ("delete", ["service", "ntp", "server", server, opt])
+ ("delete", ["service", "ntp", "server", server, opt]),
)
# delete
@@ -210,7 +217,7 @@ def parse_running_config(text):
result = {
"allow_clients": [],
"listen_addresses": [],
- "servers": {}
+ "servers": {},
}
for line in text.splitlines():
@@ -246,8 +253,8 @@ def render_commands(config):
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 la in config["listen_addresses"]:
+ cmds.append(f"set service ntp listen-address {la}")
for server, opts in config["servers"].items():
@@ -259,14 +266,15 @@ def render_commands(config):
return cmds
+
# ------------------------------------------------------------
# Main
# ------------------------------------------------------------
+
def main():
argument_spec = dict(
-
config=dict(
type="dict",
options=dict(
@@ -277,14 +285,12 @@ def main():
elements="dict",
options=dict(
server=dict(type="str", required=True),
- options=dict(type="list", elements="str")
- )
- )
- )
+ options=dict(type="list", elements="str"),
+ ),
+ ),
+ ),
),
-
running_config=dict(type="str"),
-
state=dict(
default="merged",
choices=[
@@ -294,9 +300,9 @@ def main():
"deleted",
"gathered",
"rendered",
- "parsed"
- ]
- )
+ "parsed",
+ ],
+ ),
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
@@ -307,7 +313,7 @@ def main():
config = module.params.get("config") or {}
result = {
- "changed": False
+ "changed": False,
}
# --------------------------------------------------------
@@ -349,7 +355,7 @@ def main():
desired = {
"allow_clients": [],
"listen_addresses": [],
- "servers": {}
+ "servers": {},
}
# --------------------------------------------------------
@@ -376,4 +382,4 @@ def main():
if __name__ == "__main__":
- main() \ No newline at end of file
+ main()