diff options
8 files changed, 269 insertions, 56 deletions
diff --git a/docs/vyos.rest.vyos_banner_module.rst b/docs/vyos.rest.vyos_banner_module.rst index b459bfb..4e0e3ab 100644 --- a/docs/vyos.rest.vyos_banner_module.rst +++ b/docs/vyos.rest.vyos_banner_module.rst @@ -73,7 +73,6 @@ Parameters <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> <span style="color: purple">string</span> - / <span style="color: red">required</span> </div> </td> <td> @@ -157,8 +156,8 @@ Parameters <div>Desired state of the banner configuration.</div> <div><code>merged</code> - set the banner if it differs from the current value.</div> <div><code>replaced</code> - replace the banner text unconditionally.</div> - <div><code>deleted</code> - remove the banner.</div> - <div><code>gathered</code> - return the current banner in <em>gathered</em> without making any changes.</div> + <div><code>deleted</code> - remove the banner. If <code>config.banner</code> is omitted, both pre-login and post-login banners are removed.</div> + <div><code>gathered</code> - return the current banner in <em>gathered</em> without making any changes. If <code>config.banner</code> is omitted, all banners are returned.</div> </td> </tr> <tr> @@ -231,12 +230,16 @@ Examples text: "Welcome. Authorised access only." state: replaced - - name: Remove pre-login banner + - name: Remove pre-login banner only vyos.rest.vyos_banner: config: banner: pre-login state: deleted + - name: Remove all banners + vyos.rest.vyos_banner: + state: deleted + - name: Read current pre-login banner without changing it vyos.rest.vyos_banner: config: @@ -244,6 +247,11 @@ Examples state: gathered register: result + - name: Read all banners + vyos.rest.vyos_banner: + state: gathered + register: result + - name: Print gathered banner ansible.builtin.debug: msg: "Current banner: {{ result.gathered.text }}" diff --git a/docs/vyos.rest.vyos_hostname_module.rst b/docs/vyos.rest.vyos_hostname_module.rst index b8fb5fd..c8ea1c5 100644 --- a/docs/vyos.rest.vyos_hostname_module.rst +++ b/docs/vyos.rest.vyos_hostname_module.rst @@ -19,6 +19,7 @@ Synopsis -------- - Manages the ``set system host-name`` configuration on a VyOS device using the HTTPS REST API. - Mirrors the behaviour of ``vyos.vyos.vyos_hostname`` but uses the HTTP API instead of SSH/network_cli. +- The states ``replaced``, ``overridden`` behave identically to ``merged`` for this single-value resource. @@ -47,7 +48,6 @@ Parameters <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> <span style="color: purple">string</span> - / <span style="color: red">required</span> </div> </td> <td> @@ -96,13 +96,12 @@ Parameters <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> <span style="color: purple">string</span> - / <span style="color: red">required</span> </div> </td> <td> </td> <td> - <div>IP address or FQDN of the VyOS device.</div> + <div>IP address or FQDN of the VyOS device (not needed with httpapi inventory).</div> </td> </tr> <tr> @@ -124,6 +123,22 @@ Parameters <tr> <td colspan="2"> <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>running_config</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Used only with state <code>parsed</code>.</div> + <div>The value should be the output of <b>show configuration commands | grep host-name</b> from the device.</div> + </td> + </tr> + <tr> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> <b>state</b> <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> <div style="font-size: small"> @@ -133,14 +148,22 @@ Parameters <td> <ul style="margin: 0; padding: 0"><b>Choices:</b> <li><div style="color: blue"><b>merged</b> ←</div></li> + <li>replaced</li> + <li>overridden</li> <li>deleted</li> <li>gathered</li> + <li>rendered</li> + <li>parsed</li> </ul> </td> <td> <div><code>merged</code> - Ensure the hostname is set to the value in <em>config</em>.</div> + <div><code>replaced</code> - Identical to <code>merged</code> for this single-value resource.</div> + <div><code>overridden</code> - Identical to <code>merged</code> for this single-value resource.</div> <div><code>deleted</code> - Remove the configured hostname (resets to default).</div> <div><code>gathered</code> - Read the current hostname from the device and return it in <em>gathered</em> without making changes.</div> + <div><code>rendered</code> - Return the CLI commands for the given config without connecting to the device.</div> + <div><code>parsed</code> - Parse the <code>running_config</code> string and return structured data without connecting to the device.</div> </td> </tr> <tr> @@ -203,6 +226,12 @@ Examples 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 @@ -212,6 +241,17 @@ Examples 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 + Return Values @@ -286,6 +326,36 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late <br/> </td> </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>parsed</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td>when state is parsed</td> + <td> + <div>Structured data parsed from running_config (state=parsed only).</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>rendered</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + </div> + </td> + <td>when state is rendered</td> + <td> + <div>CLI commands for the provided config (state=rendered only).</div> + <br/> + </td> + </tr> </table> <br/><br/> diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py index b9df27d..4f734cd 100644 --- a/plugins/modules/vyos_banner.py +++ b/plugins/modules/vyos_banner.py @@ -31,7 +31,6 @@ options: description: - Which banner to configure. type: str - required: true choices: - pre-login - post-login @@ -47,9 +46,11 @@ options: - Desired state of the banner configuration. - C(merged) - set the banner if it differs from the current value. - C(replaced) - replace the banner text unconditionally. - - C(deleted) - remove the banner. + - C(deleted) - remove the banner. If C(config.banner) is omitted, + both pre-login and post-login banners are removed. - C(gathered) - return the current banner in I(gathered) without - making any changes. + making any changes. If C(config.banner) is omitted, all banners + are returned. type: str default: merged choices: @@ -97,12 +98,16 @@ EXAMPLES = r""" text: "Welcome. Authorised access only." state: replaced -- name: Remove pre-login banner +- name: Remove pre-login banner only vyos.rest.vyos_banner: config: banner: pre-login state: deleted +- name: Remove all banners + vyos.rest.vyos_banner: + state: deleted + - name: Read current pre-login banner without changing it vyos.rest.vyos_banner: config: @@ -110,6 +115,11 @@ EXAMPLES = r""" state: gathered register: result +- name: Read all banners + vyos.rest.vyos_banner: + state: gathered + register: result + - name: Print gathered banner ansible.builtin.debug: msg: "Current banner: {{ result.gathered.text }}" @@ -184,7 +194,7 @@ def main(): options=dict( banner=dict( type="str", - required=True, + required=False, choices=["pre-login", "post-login"], ), text=dict(type="str"), @@ -203,17 +213,47 @@ def main(): required_if=[ ("state", "merged", ["config"]), ("state", "replaced", ["config"]), - ("state", "deleted", ["config"]), - ("state", "gathered", ["config"]), ], supports_check_mode=True, ) client = VyOSRestClient(module) state = module.params["state"] - config = module.params["config"] - banner_type = config["banner"] + config = module.params["config"] or {} + banner_type = config.get("banner") desired_text = config.get("text") or "" + + # deleted without banner_type — delete all banners + if state == "deleted" and not banner_type: + commands = [] + changed = False + for bt in ["pre-login", "post-login"]: + current = _get_current(client, bt) + if current.get("text"): + if not module.check_mode: + try: + client.configure_delete(_BANNER_PATH[bt]) + except VyOSRestError as exc: + module.fail_json(msg=str(exc)) + commands.append("delete {p}".format(p=" ".join(_BANNER_PATH[bt]))) + changed = True + module.exit_json(changed=changed, commands=commands) + + # gathered without banner_type — return all banners + if state == "gathered" and not banner_type: + gathered = {} + for bt in ["pre-login", "post-login"]: + current = _get_current(client, bt) + if current.get("text"): + gathered[bt] = current + module.exit_json(changed=False, gathered=gathered, commands=[]) + + # all other states require banner_type + if not banner_type: + module.fail_json( + msg="config.banner is required for state={0}".format(state), + ) + path = _BANNER_PATH[banner_type] commands = [] changed = False diff --git a/plugins/modules/vyos_hostname.py b/plugins/modules/vyos_hostname.py index 10255b5..461a552 100644 --- a/plugins/modules/vyos_hostname.py +++ b/plugins/modules/vyos_hostname.py @@ -16,6 +16,8 @@ description: 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. version_added: "1.0.0" author: - VyOS Community (@vyos) @@ -27,42 +29,59 @@ options: suboptions: hostname: description: - - System hostname (max 63 characters, no underscores). + - 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. type: str - choices: [merged, deleted, gathered] + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed default: merged hostname: description: - - IP address or FQDN of the VyOS device. + - IP address or FQDN of the VyOS device (not needed with httpapi inventory). type: str - required: true port: description: - - HTTPS port for the REST API. + - HTTPS port for the REST API. type: int default: 443 api_key: description: - - API key configured on the device. + - API key configured on the device. type: str - required: true no_log: true timeout: description: - - Request timeout in seconds. + - Request timeout in seconds. type: int default: 30 verify_ssl: description: - - Validate the device's TLS certificate. + - Validate the device's TLS certificate. type: bool default: false requirements: @@ -84,13 +103,20 @@ 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. returned: always type: list """ - EXAMPLES = r""" - name: Set hostname vyos.rest.vyos_hostname: @@ -98,6 +124,12 @@ EXAMPLES = r""" 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 @@ -106,8 +138,21 @@ EXAMPLES = r""" - 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, @@ -127,6 +172,12 @@ def _get_hostname(client): return "" +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 main(): argument_spec = dict( config=dict( @@ -135,22 +186,59 @@ def main(): hostname=dict(type="str", required=True), ), ), + running_config=dict(type="str"), state=dict( type="str", default="merged", - choices=["merged", "deleted", "gathered"], + choices=[ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], ), ) argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) module = AnsibleModule( argument_spec=argument_spec, - required_if=[("state", "merged", ["config"])], + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ("state", "merged", ["config"]), + ("state", "replaced", ["config"]), + ("state", "overridden", ["config"]), + ("state", "rendered", ["config"]), + ("state", "parsed", ["running_config"]), + ], supports_check_mode=True, ) - client = VyOSRestClient(module) 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 @@ -168,15 +256,15 @@ def main(): desired = module.params["config"]["hostname"] if current != desired: client.configure_set(_PATH, desired) - commands.append( - "set system host-name '{h}'".format(h=desired), - ) + commands.append("set system host-name '{h}'".format(h=desired)) changed = True + elif state == "deleted": if current: client.configure_delete(_PATH) commands.append("delete system host-name") changed = True + except VyOSRestError as exc: module.fail_json(msg=str(exc)) diff --git a/tests/integration/targets/vyos_banner/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_banner/tests/httpapi/_populate_config.yaml index 2557b70..7bd2698 100644 --- a/tests/integration/targets/vyos_banner/tests/httpapi/_populate_config.yaml +++ b/tests/integration/targets/vyos_banner/tests/httpapi/_populate_config.yaml @@ -1,11 +1,14 @@ --- -- name: Populate banner config for testing +- name: Populate pre-login banner for testing vyos.rest.vyos_banner: config: - banners: - - banner: pre-login - text: "Ansible test pre-login banner" - - banner: post-login - text: "Ansible test post-login banner" + banner: pre-login + text: "Ansible test pre-login banner" + state: merged + +- name: Populate post-login banner for testing + vyos.rest.vyos_banner: + config: + banner: post-login + text: "Ansible test post-login banner" state: merged - ignore_errors: true diff --git a/tests/integration/targets/vyos_banner/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_banner/tests/httpapi/_remove_config.yaml index cc56d27..2b43847 100644 --- a/tests/integration/targets/vyos_banner/tests/httpapi/_remove_config.yaml +++ b/tests/integration/targets/vyos_banner/tests/httpapi/_remove_config.yaml @@ -1,5 +1,5 @@ --- -- name: Remove pre-existing banner config +- name: Remove all banners vyos.rest.vyos_banner: state: deleted ignore_errors: true diff --git a/tests/integration/targets/vyos_banner/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_banner/tests/httpapi/deleted.yaml index 5d0db34..82e0f6e 100644 --- a/tests/integration/targets/vyos_banner/tests/httpapi/deleted.yaml +++ b/tests/integration/targets/vyos_banner/tests/httpapi/deleted.yaml @@ -6,16 +6,18 @@ - include_tasks: _populate_config.yaml - block: - - name: Delete all banner configuration + - name: Delete pre-login banner register: result vyos.rest.vyos_banner: &id001 + config: + banner: pre-login state: deleted - assert: that: - result.changed == true - - name: Delete banner configuration (IDEMPOTENT) + - name: Delete pre-login banner (IDEMPOTENT) register: result vyos.rest.vyos_banner: *id001 diff --git a/tests/integration/targets/vyos_banner/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_banner/tests/httpapi/merged.yaml index 027702a..96a984e 100644 --- a/tests/integration/targets/vyos_banner/tests/httpapi/merged.yaml +++ b/tests/integration/targets/vyos_banner/tests/httpapi/merged.yaml @@ -1,32 +1,34 @@ --- - debug: - msg: START vyos_banner merged integration tests on connection={{ ansible_connection }} + msg: START vyos_banner gathered integration tests on connection={{ ansible_connection }} - include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml - block: - - name: Merge banner configuration + - name: Gather all banners (no config specified) register: result - vyos.rest.vyos_banner: &id001 - config: - banners: - - banner: pre-login - text: "Ansible test pre-login banner" - state: merged + vyos.rest.vyos_banner: + state: gathered - assert: that: - - result.changed == true + - result.changed == false + - result.gathered is defined + - "'pre-login' in result.gathered" + - "'post-login' in result.gathered" - - name: Merge banner configuration (IDEMPOTENT) + - name: Gather specific pre-login banner register: result - vyos.rest.vyos_banner: *id001 + vyos.rest.vyos_banner: + config: + banner: pre-login + state: gathered - - name: Assert idempotent - assert: + - assert: that: - result.changed == false - - result.commands == [] + - result.gathered.text == "Ansible test pre-login banner" always: - include_tasks: _remove_config.yaml |
