diff options
| author | omnom62 <omnom62@outlook.com> | 2026-03-20 21:50:13 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-03-20 21:50:13 +1000 |
| commit | 7599ba96bc3b659cb2a5f00b1be46ab6f55f48a6 (patch) | |
| tree | 35b39a126fe94f8917352aa2c229d3022d1223e4 /plugins/modules | |
| parent | 9dc03bb92e08455812e0969505495309456a4bfb (diff) | |
| download | rest.vyos-7599ba96bc3b659cb2a5f00b1be46ab6f55f48a6.tar.gz rest.vyos-7599ba96bc3b659cb2a5f00b1be46ab6f55f48a6.zip | |
precommit changes
Diffstat (limited to 'plugins/modules')
| -rw-r--r-- | plugins/modules/vyos_banner.py | 366 | ||||
| -rw-r--r-- | plugins/modules/vyos_hostname.py | 35 | ||||
| -rw-r--r-- | plugins/modules/vyos_ntp_global.py | 58 |
3 files changed, 418 insertions, 41 deletions
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() |
