diff options
| author | omnom62 <omnom62@outlook.com> | 2026-08-23 08:39:18 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-08-23 08:39:18 +1000 |
| commit | ef3ded9ace9d7c650e15104698acdf629c57f797 (patch) | |
| tree | 0c06e3e6d565f0295b2a3bb7c0778c8da5a27889 /plugins | |
| parent | ff5224728484eb4f3bbf6ae52fe0b79f64959660 (diff) | |
| download | rest.vyos-ef3ded9ace9d7c650e15104698acdf629c57f797.tar.gz rest.vyos-ef3ded9ace9d7c650e15104698acdf629c57f797.zip | |
T8989: vyos_hostname rework
Diffstat (limited to 'plugins')
| -rw-r--r-- | plugins/module_utils/vyos.py | 102 | ||||
| -rw-r--r-- | plugins/modules/vyos_hostname.py | 294 |
2 files changed, 128 insertions, 268 deletions
diff --git a/plugins/module_utils/vyos.py b/plugins/module_utils/vyos.py index a37a0c2..bdbc2c4 100644 --- a/plugins/module_utils/vyos.py +++ b/plugins/module_utils/vyos.py @@ -19,85 +19,6 @@ from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( # --------------------------------------------------------------------------- -# Legacy dynamic config utilities (used by Wave 1-3 modules) -# --------------------------------------------------------------------------- - - -def _kebab_to_snake(s): - """Convert kebab-case string to snake_case.""" - return s.replace("-", "_") - - -def _snake_to_kebab(s): - """Convert snake_case string to kebab-case.""" - return s.replace("_", "-") - - -def normalize(raw): - """Recursively normalize an API response dict to snake_case keys.""" - if isinstance(raw, dict): - return {_kebab_to_snake(k): normalize(v) for k, v in raw.items()} - if isinstance(raw, list): - return [normalize(v) for v in raw] - return raw - - -def denormalize_path(path): - """Convert a snake_case path list to kebab-case for the API.""" - return [_snake_to_kebab(p) for p in path] - - -def _diff_value(want_val, have_val, path, cmds, delete_missing): - if isinstance(want_val, dict): - if not want_val: - if have_val is None: - cmds.append(("set", denormalize_path(path))) - else: - have_dict = have_val if isinstance(have_val, dict) else {} - _diff_dict(want_val, have_dict, path, cmds, delete_missing) - elif isinstance(want_val, list): - have_set = set(have_val) if isinstance(have_val, list) else set() - for item in want_val: - if item not in have_set: - cmds.append(("set", denormalize_path(path + [str(item)]))) - if delete_missing: - want_set = set(str(i) for i in want_val) - for item in have_val or []: - if str(item) not in want_set: - cmds.append(("delete", denormalize_path(path + [str(item)]))) - else: - if want_val != have_val: - cmds.append(("set", denormalize_path(path + [str(want_val)]))) - - -def _diff_dict(want, have, path, cmds, delete_missing): - for key, want_val in want.items(): - _diff_value(want_val, have.get(key), path + [key], cmds, delete_missing) - if delete_missing: - for key in have: - if key not in want: - cmds.append(("delete", denormalize_path(path + [key]))) - - -def diff_configs(want, have, base_path, delete_missing=False): - """Diff two normalized config dicts and return API command tuples. - - Args: - want (dict): Desired configuration (snake_case keys). - have (dict): Current configuration (snake_case keys). - base_path (list): Base API path for commands. - delete_missing (bool): Generate delete commands for keys in - ``have`` absent from ``want``. - - Returns: - list: Tuples of ``("set", path)`` or ``("delete", path)``. - """ - cmds = [] - _diff_dict(want, have, base_path, cmds, delete_missing) - return cmds - - -# --------------------------------------------------------------------------- # Generic dict diff engine (used by Wave 4+ modules) # # Design principles: @@ -229,7 +150,10 @@ def cast_by_spec(entry, options): continue spec_type = spec.get("type") if spec_type == "int": - entry[key] = int(entry[key]) + val = entry[key] + if isinstance(val, list): + val = val[0] if val else None + entry[key] = int(val) if val is not None else None elif spec_type == "dict": cast_by_spec(entry[key], spec.get("options")) elif spec_type == "list": @@ -380,6 +304,24 @@ class VyOSModule: except VyOSRestError: return {} + def get_value(self, path): + """Retrieve a single scalar leaf value at *path*. + + Uses VyOS's dedicated "returnValue" retrieve operation -- + genuinely distinct from get_config's "showConfig" operation, + which returns a config subtree rather than a single value. + Appropriate for a plain leafNode (e.g. "system host-name"), + not a container. + + Errors genuinely propagate rather than being swallowed into a + misleading "absent" result: a transient failure here must not + be indistinguishable from the value legitimately being unset, + since a module could otherwise decide to overwrite a value + that's actually already correct. + """ + result = self._client.retrieve_return_value(path) + return result.get("data") or "" + def apply_commands(self, commands): if not commands: return [] diff --git a/plugins/modules/vyos_hostname.py b/plugins/modules/vyos_hostname.py index 86444ad..3218333 100644 --- a/plugins/modules/vyos_hostname.py +++ b/plugins/modules/vyos_hostname.py @@ -12,83 +12,51 @@ DOCUMENTATION = r""" module: vyos_hostname short_description: Manage the system hostname on a VyOS device via the REST API. description: - - Manages the C(set system host-name) configuration on a VyOS device - using the HTTPS REST API. - - Mirrors the behaviour of C(vyos.vyos.vyos_hostname) but uses the HTTP - API instead of SSH/network_cli. - - The states C(replaced), C(overridden) behave identically to C(merged) - for this single-value resource. + - Manages the C(system host-name) configuration on a VyOS device using the HTTPS REST API. version_added: "1.0.0" author: - VyOS Community (@vyos) options: config: - description: - - Hostname configuration. + description: Hostname configuration. type: dict suboptions: hostname: - description: - - System hostname (max 63 characters, no underscores). + description: System hostname (max 63 characters, no underscores). type: str required: true - running_config: - description: - - Used only with state C(parsed). - - The value should be the output of - B(show configuration commands | grep host-name) from the device. - type: str state: description: - C(merged) - Ensure the hostname is set to the value in I(config). - - C(replaced) - Identical to C(merged) for this single-value resource. - - C(overridden) - Identical to C(merged) for this single-value resource. - - C(deleted) - Remove the configured hostname (resets to default). - - C(gathered) - Read the current hostname from the device and return it - in I(gathered) without making changes. - - C(rendered) - Return the CLI commands for the given config without - connecting to the device. - - C(parsed) - Parse the C(running_config) string and return structured - data without connecting to the device. + - >- + C(replaced) and C(overridden) behave identically to C(merged) for + this single-value resource -- there is nothing else to distinctly + replace or override when there is only one field. + - C(deleted) - Remove the configured hostname. + - C(gathered) - Read the current hostname from the device without making changes. type: str - choices: - - merged - - replaced - - overridden - - deleted - - gathered - - rendered - - parsed + choices: [merged, replaced, overridden, deleted, gathered] default: merged - hostname: - description: - - IP address or FQDN of the VyOS device (not needed with httpapi inventory). - type: str - port: - description: - - HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: - - API key configured on the device. - type: str - timeout: - description: - - Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: - - Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ seealso: - module: vyos.vyos.vyos_hostname """ +EXAMPLES = r""" +- name: Set hostname + vyos.rest.vyos_hostname: + config: + hostname: vyos-core-01 + state: merged + +- name: Gather current hostname + vyos.rest.vyos_hostname: + state: gathered + +- name: Delete hostname configuration + vyos.rest.vyos_hostname: + state: deleted +""" + RETURN = r""" before: description: Configuration on the device before the module ran. @@ -102,173 +70,123 @@ gathered: description: Hostname read from the device (state=gathered only). returned: when state is gathered type: dict -rendered: - description: CLI commands for the provided config (state=rendered only). - returned: when state is rendered - type: list -parsed: - description: Structured data parsed from running_config (state=parsed only). - returned: when state is parsed - type: dict commands: - description: REST API commands dispatched. + description: List of API command tuples sent to the device. returned: always type: list +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict """ -EXAMPLES = r""" -- name: Set hostname - vyos.rest.vyos_hostname: - config: - hostname: vyos-core-01 - state: merged - -- name: Replace hostname - vyos.rest.vyos_hostname: - config: - hostname: vyos-core-02 - state: replaced - -- name: Gather current hostname - vyos.rest.vyos_hostname: - state: gathered - register: result - -- name: Delete hostname configuration - vyos.rest.vyos_hostname: - state: deleted - -- name: Render commands without connecting - vyos.rest.vyos_hostname: - config: - hostname: vyos-core-01 - state: rendered - -- name: Parse running config - vyos.rest.vyos_hostname: - running_config: "set system host-name 'vyos'" - state: parsed -""" - -import re - from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, +from ansible_collections.vyos.rest.plugins.module_utils.vyos import ( + VyOSModule, VyOSRestError, ) -_PATH = ["system", "host-name"] +_BASE = ["system", "host-name"] -def _get_hostname(client): - try: - result = client.retrieve_return_value(_PATH) - return result.get("data", "") - except VyOSRestError: - return "" +def get_running_config(vyos): + """ "system host-name" is a plain leafNode, not a container -- a + single scalar value, not a config subtree. get_value (VyOS's + "returnValue" retrieve operation) is the correct fetch for this, + distinct from get_config's "showConfig" operation used everywhere + else in this collection for genuine nested config sections. + """ + return vyos.get_value(_BASE) -def _parse_hostname(running_config): - """Parse hostname from 'show configuration commands | grep host-name' output.""" - match = re.search(r"host-name\s+['\"]?(\S+?)['\"]?\s*$", running_config, re.M) - return match.group(1) if match else "" +def build_commands(config, current, state): + if state == "deleted": + return [("delete", _BASE)] if current else [] + desired = (config or {}).get("hostname") + if not desired or desired == current: + return [] + return [("set", _BASE + [desired])] -def main(): - argument_spec = dict( - config=dict( - type="dict", - options=dict( - hostname=dict(type="str", required=True), - ), - ), - running_config=dict(type="str"), - state=dict( - type="str", - default="merged", - choices=[ - "merged", - "replaced", - "overridden", - "deleted", - "gathered", - "rendered", - "parsed", - ], + +ARGUMENT_SPEC = dict( + config=dict( + type="dict", + options=dict( + hostname=dict(type="str", required=True), ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) + ), + state=dict( + type="str", + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered"], + ), +) + +def main(): module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[["config", "running_config"]], + ARGUMENT_SPEC, required_if=[ ("state", "merged", ["config"]), ("state", "replaced", ["config"]), ("state", "overridden", ["config"]), - ("state", "rendered", ["config"]), - ("state", "parsed", ["running_config"]), ], supports_check_mode=True, ) + vyos = VyOSModule(module) + # Collapsed states: replaced/overridden are identical to merged for + # this single-value resource -- there is nothing else to distinctly + # replace or override when there's only one field. state = module.params["state"] - - # rendered — offline, no device connection needed - if state == "rendered": - hostname = module.params["config"]["hostname"] - module.exit_json( - rendered=["set system host-name '{h}'".format(h=hostname)], - commands=[], - ) - - # parsed — offline, no device connection needed - if state == "parsed": - hostname = _parse_hostname(module.params["running_config"] or "") - module.exit_json( - parsed={"hostname": hostname}, - commands=[], - ) - - # collapsed states — replaced and overridden are identical to merged if state in ("replaced", "overridden"): state = "merged" - client = VyOSRestClient(module) - commands = [] - changed = False - - current = _get_hostname(client) - before = {"hostname": current} + config = module.params.get("config") or {} - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) + try: + current = get_running_config(vyos) + except VyOSRestError as exc: + module.fail_json(msg="failed to read current hostname: {e}".format(e=str(exc))) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) + have = {"hostname": current} - try: - if state == "merged": - desired = module.params["config"]["hostname"] - if current != desired: - client.configure_set(_PATH, desired) - commands.append("set system host-name '{h}'".format(h=desired)) - changed = True + if state == "gathered": + module.exit_json(changed=False, gathered=have, commands=[]) - elif state == "deleted": - if current: - client.configure_delete(_PATH) - commands.append("delete system host-name") - changed = True + commands = build_commands(config, current, state) - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have, after=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + try: + after_current = get_running_config(vyos) + except VyOSRestError as exc: + module.fail_json( + msg="hostname change applied but failed to read back result: {e}".format( + e=str(exc), + ), + ) + after = {"hostname": after_current} + module.exit_json( + changed=True, + before=have, + after=after, + commands=commands, + saved=saved, + response=response, + ) - after = {"hostname": _get_hostname(client)} if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) + module.exit_json(changed=False, before=have, after=have, commands=[]) if __name__ == "__main__": |
