diff --git a/plugins/httpapi/nd.py b/plugins/httpapi/nd.py index 533473b5..33932534 100644 --- a/plugins/httpapi/nd.py +++ b/plugins/httpapi/nd.py @@ -72,6 +72,7 @@ def __init__(self, *args, **kwargs): self.path = "" self.status = -1 self.info = {} + self.version = None def get_platform(self): return self.platform @@ -93,7 +94,14 @@ def set_backup_hosts(self): # TODO Add support for dynamically returning platform versions. def get_version(self, platform="ndfc"): if platform == "ndfc": - return 12 + if self.version is None: + self.version = 12 + return self.version + elif platform == "nd": + if self.version is None: + response_json = self._send_nd_request("GET", "/version.json", self.headers) + self.version = ".".join(str(response_json.get("body")[key]) for key in ["major", "minor", "maintenance"]) + return self.version else: raise ValueError("Unknown platform type: {0}".format(platform)) @@ -326,11 +334,14 @@ def send_file_request(self, method, path, file=None, data=None, remote_path=None raise ConnectionError(json.dumps(self._verify_response(None, method, path, None))) try: + fields = None # create fields for MultipartEncoder if remote_path: fields = dict(rdir=remote_path, name=(filename, open(file, "rb"), mimetypes.guess_type(filename))) - elif file_key == "importfile": - fields = dict(spec=(json.dumps(data)), importfile=(filename, open(file, "rb"), mimetypes.guess_type(filename))) + elif file_key in ["importfile", "files"]: + fields = {file_key: (filename, open(file, "rb"), mimetypes.guess_type(filename))} + if file_key == "importfile": + fields["spec"] = json.dumps(data) else: fields = dict(data=("data.json", data_str, "application/json"), file=(filename, open(file, "rb"), mimetypes.guess_type(filename))) @@ -342,6 +353,7 @@ def send_file_request(self, method, path, file=None, data=None, remote_path=None mp_encoder = MultipartEncoder(fields=fields) multiheader = {"Content-Type": mp_encoder.content_type, "Accept": "*/*", "Accept-Encoding": "gzip, deflate, br"} + self.connection.queue_message("info", "send_file_request() - connection.send({0}, {1}, {2}, {3})".format(path, method, fields, multiheader)) response, rdata = self.connection.send(path, mp_encoder.to_string(), method=method, headers=multiheader) except Exception as e: self.error = dict(code=self.status, message="ND HTTPAPI MultipartEncoder Exception: {0} - {1} ".format(e, traceback.format_exc())) diff --git a/plugins/module_utils/nd.py b/plugins/module_utils/nd.py index cca3ed42..96c7195c 100644 --- a/plugins/module_utils/nd.py +++ b/plugins/module_utils/nd.py @@ -223,11 +223,28 @@ def __init__(self, module): self.status = None self.url = None self.httpapi_logs = list() + self.connection = None + self.version = None + + # Set Connection plugin + self.set_connection() + + # Set ND version + self.set_version() if self.module._debug: self.module.warn("Enable debug output because ANSIBLE_DEBUG was set.") self.params["output_level"] = "debug" + def set_version(self): + if self.version is None: + self.version = self.connection.get_version("nd") + + def set_connection(self): + if self.connection is None: + self.connection = Connection(self.module._socket_path) + self.connection.set_params(self.params) + def request( self, path, method=None, data=None, file=None, qs=None, prefix="", file_key="file", output_format="json", ignore_not_found_error=False, file_ext=None ): @@ -241,8 +258,6 @@ def request( if method == "PATCH" and not data: return {} - conn = Connection(self.module._socket_path) - conn.set_params(self.params) uri = self.path if prefix != "": uri = "{0}/{1}".format(prefix, self.path) @@ -250,16 +265,16 @@ def request( uri = uri + update_qs(qs) try: if file is not None: - info = conn.send_file_request(method, uri, file, data, None, file_key, file_ext) + info = self.connection.send_file_request(method, uri, file, data, None, file_key, file_ext) else: if data: - info = conn.send_request(method, uri, json.dumps(data)) + info = self.connection.send_request(method, uri, json.dumps(data)) else: - info = conn.send_request(method, uri) + info = self.connection.send_request(method, uri) self.result["data"] = data self.url = info.get("url") - self.httpapi_logs.extend(conn.pop_messages()) + self.httpapi_logs.extend(self.connection.pop_messages()) info.pop("date", None) except Exception as e: try: diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py new file mode 100644 index 00000000..01b25163 --- /dev/null +++ b/plugins/module_utils/utils.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +def snake_to_camel(snake_str, upper_case_components=None): + if snake_str is not None and "_" in snake_str: + if upper_case_components is None: + upper_case_components = [] + components = snake_str.split("_") + camel_case_str = components[0] + + for component in components[1:]: + if component in upper_case_components: + camel_case_str += component.upper() + else: + camel_case_str += component.title() + + return camel_case_str + else: + return snake_str diff --git a/plugins/modules/nd_backup.py b/plugins/modules/nd_backup.py index 0634a452..67260de4 100644 --- a/plugins/modules/nd_backup.py +++ b/plugins/modules/nd_backup.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2023, Shreyas Srish (@shrsr) +# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function @@ -19,6 +20,7 @@ - Manages backup of the cluster configuration. author: - Shreyas Srish (@shrsr) +- Sabari Jaganathan (@sajagana) options: name: description: @@ -28,24 +30,44 @@ encryption_key: description: - The encryption_key for a backup file. + - A minimum of 8 alphanumeric characters is required. type: str file_location: description: - The download path and file name for a backup. + - When O(file_location) is specified, the backup will be created and automatically downloaded to the local machine at the designated path. type: str backup_key: description: - The key generated by ND during creation of a backup. - This key is required when querying or deleting a backup among multiple backups that have the same name. - This key can be obtained by querying the backup. + - This parameter is not supported on ND v3.2.1 and later. type: str + remote_location: + description: + - The name of the remote storage location. This parameter is only supported on ND v3.2.1 and later. + - If the O(remote_location) parameter is not specified or O(remote_location="") during backup creation, a local backup will be created. + type: str + backup_type: + description: + - This parameter is only supported on ND v3.2.1 and later. + - The O(backup_type=config_only) option creates a snapshot that specifically captures the configuration settings of the Nexus Dashboard. + - The O(backup_type=full) option creates a complete snapshot of the entire Nexus Dashboard. + type: str + choices: [ config_only, full ] + default: config_only + aliases: [ type ] state: description: - - Use C(backup) for creating a backup of the cluster config. - - Use C(query) for listing all the backed up files. - - Use C(absent) for deleting a backup job. + - Use O(state=backup) for creating and downloading a backup of the cluster config for the ND versions < 3.2.1. + - Use O(state=backup) to create a cluster configuration backup. Automatic download is not supported for the ND versions >= 3.2.1. + - After creation, use O(state=download) to download the backup file. + - Use O(state=download) downloading a backup to the local machine, the O(state=download) is only supported on ND v3.2.1 and later. + - Use O(state=query) for listing all the backed up files. + - Use O(state=absent) for deleting a backup job. type: str - choices: [ backup, query, absent ] + choices: [ backup, download, query, absent ] default: backup extends_documentation_fragment: - cisco.nd.modules @@ -53,13 +75,39 @@ """ EXAMPLES = r""" -- name: Create a Backup +- name: Create a backup for ND versions < 3.2.1 cisco.nd.nd_backup: name: nexus encryption_key: testtest file_location: ./nexus.tgz state: backup +- name: Create a remote backup for ND versions >= 3.2.1 + cisco.nd.nd_backup: + name: nexus + encryption_key: testtest1 + remote_location: remote_machine + state: backup + +- name: Create a local backup for ND versions >= 3.2.1 + cisco.nd.nd_backup: + name: nexus + encryption_key: testtest1 + state: backup + +- name: Create a backup and download it to the local machine for ND versions >= 3.2.1 + cisco.nd.nd_backup: + name: nexus + file_location: ./nexus.tgz + encryption_key: testtest1 + state: backup + +- name: Download a local/remote backup for ND versions >= 3.2.1 + cisco.nd.nd_backup: + name: nexus + state: download + file_location: ./nexus.tgz + - name: Query a Backup job cisco.nd.nd_backup: name: nexus @@ -83,6 +131,7 @@ from ansible.module_utils._text import to_bytes from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule, nd_argument_spec, write_file +from ansible_collections.cisco.nd.plugins.module_utils.utils import snake_to_camel def main(): @@ -92,15 +141,18 @@ def main(): encryption_key=dict(type="str", no_log=False), file_location=dict(type="str"), backup_key=dict(type="str", no_log=False), - state=dict(type="str", default="backup", choices=["backup", "query", "absent"]), + remote_location=dict(type="str"), + backup_type=dict(type="str", default="config_only", choices=["config_only", "full"], aliases=["type"]), + state=dict(type="str", default="backup", choices=["backup", "download", "query", "absent"]), ) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, required_if=[ - ["state", "backup", ["name", "encryption_key", "file_location"]], + ["state", "backup", ["name", "encryption_key"]], ["state", "absent", ["name"]], + ["state", "download", ["name", "file_location"]], ], ) @@ -110,8 +162,75 @@ def main(): encryption_key = nd.params.get("encryption_key") backup_key = nd.params.get("backup_key") file_location = nd.params.get("file_location") + remote_location = nd.params.get("remote_location") + backup_type = nd.params.get("backup_type") state = nd.params.get("state") + if nd.version < "3.2.1": + if not file_location and state in ["backup", "download"]: + nd.fail_json("Parameter 'file_location' is required when state is 'backup|download' for ND versions < 3.2.1.") + nd_backup_before_3_2_1(module, nd, name, encryption_key, file_location, backup_key, state) + elif nd.version >= "3.2.1": + nd_backup_from_3_2_1(module, nd, name, encryption_key, file_location, remote_location, backup_type, state) + + nd.exit_json() + + +def nd_backup_from_3_2_1(module, nd, name, encryption_key, file_location, remote_location, backup_type, state): + if encryption_key is not None: + if len(encryption_key) < 8: + nd.fail_json("Please provide a minimum of 8 alphanumeric characters for the encryption key.") + elif not (any(char.isalpha() for char in encryption_key) and any(char.isdigit() for char in encryption_key) and encryption_key.isalnum()): + nd.fail_json("The encryption_key must contain at least one letter and one number, and have a minimum length of 8 characters.") + + path = "/api/v1/infra/backups" + backups = nd.query_obj(path) + if name and backups: + for backup in backups.get("backups", []): + if backup.get("name") == name: + nd.existing = backup + break + else: + nd.existing = backups.get("backups", []) + + if state == "absent" and nd.existing: + nd.previous = nd.existing + if not module.check_mode: + nd.request("{0}/{1}".format(path, name), method="DELETE") + nd.existing = {} + + elif state == "backup": + if not nd.existing: + payload = { + "name": name, + "type": snake_to_camel(backup_type), + "destination": remote_location if remote_location else "", + "encryptionKey": encryption_key, + } + nd.sanitize(payload, collate=True) + + if not module.check_mode: + # Creates backup file and returns None + nd.request(path, method="POST", data=payload) + + # Fetching the backup object details to set module current value + nd.existing = nd.request("{0}/{1}".format(path, name), method="GET") + + if file_location: + response = nd.request("{0}/{1}/actions/download".format(path, name), method="GET", data=None, output_format="raw") + write_file(module, file_location, to_bytes(response)) + elif module.check_mode: + nd.existing = nd.proposed + else: + nd.previous = nd.existing + + elif state == "download" and file_location and nd.existing: + if not module.check_mode: + response = nd.request("{0}/{1}/actions/download".format(path, name), method="GET", data=None, output_format="raw") + write_file(module, file_location, to_bytes(response)) + + +def nd_backup_before_3_2_1(module, nd, name, encryption_key, file_location, backup_key, state): if encryption_key is not None and len(encryption_key) < 8: nd.fail_json("Please provide a minimum of 8 characters for the encryption key.") @@ -158,8 +277,6 @@ def main(): write_file(module, file_location, to_bytes(response)) nd.existing = nd.proposed - nd.exit_json() - if __name__ == "__main__": main() diff --git a/plugins/modules/nd_backup_restore.py b/plugins/modules/nd_backup_restore.py index fdcfd429..a42e1755 100644 --- a/plugins/modules/nd_backup_restore.py +++ b/plugins/modules/nd_backup_restore.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2023, Shreyas Srish (@shrsr) +# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function @@ -19,6 +20,7 @@ - Manages importing the cluster configuration using a backup. author: - Shreyas Srish (@shrsr) +- Sabari Jaganathan (@sajagana) options: name: description: @@ -41,6 +43,24 @@ - This key is required when querying or deleting a restored job among multiple restored jobs that have the same name. - This key can be obtained by querying a restored job. type: str + ignore_persistent_ips: + description: + - When the O(ignore_persistent_ips=true), will overwrite the existing external service IP addresses configured on the Nexus Dashboard. + type: bool + aliases: [ ignore_external_service_ip_configuration ] + restore_type: + description: + - This parameter is only supported on ND v3.2.1 and later. + - The O(restore_type=config_only) option restores only configuration settings of the Nexus Dashboard. + - The O(restore_type=full) option restores the entire settings of the Nexus Dashboard. + type: str + choices: [ config_only, full ] + default: config_only + aliases: [ type ] + remote_location: + description: + - The name of the remote storage location. This parameter is only supported on ND v3.2.1 and later. + type: str state: description: - Use C(restore) for importing a backup of the cluster config. @@ -84,6 +104,8 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule, nd_argument_spec +from ansible_collections.cisco.nd.plugins.module_utils.utils import snake_to_camel +import time def main(): @@ -94,14 +116,16 @@ def main(): file_location=dict(type="str"), restore_key=dict(type="str", no_log=False), state=dict(type="str", default="restore", choices=["restore", "query", "absent"]), + ignore_persistent_ips=dict(type="bool", aliases=["ignore_external_service_ip_configuration"]), + restore_type=dict(type="str", default="config_only", choices=["config_only", "full"], aliases=["type"]), + remote_location=dict(type="str"), ) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, required_if=[ - ["state", "restore", ["name", "encryption_key", "file_location"]], - ["state", "absent", ["name"]], + ["state", "restore", ["encryption_key"]], ], ) @@ -112,10 +136,90 @@ def main(): restore_key = nd.params.get("restore_key") file_location = nd.params.get("file_location") state = nd.params.get("state") + ignore_persistent_ips = nd.params.get("ignore_persistent_ips") + restore_type = snake_to_camel(nd.params.get("restore_type")) + remote_location = nd.params.get("remote_location") if encryption_key is not None and len(encryption_key) < 8: nd.fail_json("The encryption key must have a minium of 8 characters.") + if nd.version < "3.2.1": + nd_backup_restore_before_3_2_1(nd, name, encryption_key, file_location, restore_key, state, module) + elif nd.version >= "3.2.1": + nd_backup_restore_from_3_2_1(nd, name, encryption_key, file_location, state, module, ignore_persistent_ips, restore_type, remote_location) + nd.exit_json() + + +def nd_backup_restore_from_3_2_1(nd, name, encryption_key, file_location, state, module, ignore_persistent_ips, restore_type, remote_location): + if name and (remote_location or file_location): + nd.fail_json("The parameters name and (remote_location or file_location) cannot be specified at the same time.") + + nd.existing = nd.query_obj("/api/v1/infra/backups/status") + import_path = "/api/v1/infra/backups/actions/import" + + # Remove backup status (not idempotent) + if state == "absent" and (not nd.existing or nd.existing.get("state") != "processing"): + nd.previous = nd.existing + if not module.check_mode: + nd.request(import_path, method="DELETE") + + # Restore from backup (not idempotent) + elif state == "restore" and (not nd.existing or nd.existing.get("state") != "processing"): + + if not module.check_mode: # Need to delete the imported file before starting the restore process + nd.request(import_path, method="DELETE") + + import_payload = {"encryptionKey": encryption_key} + + if remote_location and file_location and not name: + import_payload.update({"source": remote_location, "path": file_location}) + elif not (remote_location and name) and file_location: + # Local file upload + if not module.check_mode: + import_payload["path"] = nd.request( + "/api/action/class/backuprestore/file-upload", method="POST", data=None, file=file_location, file_key="files", output_format="raw" + ) + elif name: + import_payload["name"] = name.split(".")[0] # Restore operation requires only name of the backup file + + nd.sanitize(import_payload, collate=True) + + restore_payload = { + "ignorePersistentIPs": ignore_persistent_ips + or False, # add note to the document saying that ignore_persistent_ips set to false when it is not specified + "type": restore_type or "configOnly", # add note to the document saying that restore_type set to configOnly when it is not specified + } + nd_payload = { + "fileUploadPayload": {"fileLocation": file_location}, + "importPayload": import_payload, + "restorePayload": restore_payload, + } + nd.sanitize(nd_payload, collate=True) + + if not module.check_mode: + nd.request(import_path, method="POST", data=import_payload) + time.sleep(10) + nd.request("/api/v1/infra/backups/actions/restore", method="POST", data=restore_payload) + nd.existing = nd.query_obj("/api/v1/infra/backups/status") + else: + nd.existing = nd.proposed + + # Operation not allowed if backup status is processing + elif state != "query" and nd.existing and nd.existing.get("state") == "processing": + nd.fail_json( + msg="The {0} operation could not proceed because a system {1} is in progress ({2}% complete).".format( + state, nd.existing.get("operation"), nd.existing.get("details", {}).get("progress") + ) + ) + + +def nd_backup_restore_before_3_2_1(nd, name, encryption_key, file_location, restore_key, state, module): + if state == "restore" and not (name and encryption_key and file_location): + nd.fail_json("state is restore but all/one of the following are missing: name, encryption_key, file_location") + + if state == "absent" and not name: + nd.fail_json("state is absent but all of the following are missing: name") + path = "/nexus/infra/api/platform/v1/imports" # The below path for GET operation is to be replaced by an official documented API endpoint once it becomes available. restored_objs = nd.query_obj("/api/config/class/imports") @@ -156,8 +260,6 @@ def main(): nd.request(path, method="POST", data=payload, file=file_location, file_key="importfile", output_format="raw") nd.existing = nd.proposed - nd.exit_json() - if __name__ == "__main__": main() diff --git a/plugins/modules/nd_rest.py b/plugins/modules/nd_rest.py index d5a73e8e..1e91e978 100644 --- a/plugins/modules/nd_rest.py +++ b/plugins/modules/nd_rest.py @@ -48,6 +48,10 @@ - The file path containing the body of the HTTP request. type: path aliases: [ config_file ] + ignore_previous_state: + description: + - This parameter ignores the object's previous state. + type: bool extends_documentation_fragment: - cisco.nd.modules - cisco.nd.check_mode @@ -212,6 +216,7 @@ def main(): ), content=dict(type="raw", aliases=["payload"]), file_path=dict(type="path", aliases=["config_file"]), + ignore_previous_state=dict(type="bool"), ) module = AnsibleModule( @@ -222,6 +227,7 @@ def main(): content = module.params.get("content") path = module.params.get("path") file_path = module.params.get("config_file") + ignore_previous_state = module.params.get("ignore_previous_state") nd = NDModule(module) @@ -251,7 +257,8 @@ def main(): # Append previous state of the object if method in ("PUT", "DELETE", "PATCH"): - nd.existing = nd.previous = sanitize(nd.query_obj(path, ignore_not_found_error=True), ND_REST_KEYS_TO_SANITIZE) + if not ignore_previous_state: + nd.existing = nd.previous = sanitize(nd.query_obj(path, ignore_not_found_error=True), ND_REST_KEYS_TO_SANITIZE) nd.result["previous"] = nd.previous # Perform request diff --git a/plugins/modules/nd_version.py b/plugins/modules/nd_version.py index 66b2d98c..eed2000a 100644 --- a/plugins/modules/nd_version.py +++ b/plugins/modules/nd_version.py @@ -61,6 +61,7 @@ def main(): # Query for nd.existing object nd.existing = nd.query_obj(path) + nd.existing["platformVersion"] = "{0}.{1}.{2}".format(nd.existing.get("major"), nd.existing.get("minor"), nd.existing.get("maintenance")) nd.exit_json() diff --git a/tests/integration/targets/nd_backup/tasks/main.yml b/tests/integration/targets/nd_backup/tasks/main.yml index 8e5efe23..9f04798f 100644 --- a/tests/integration/targets/nd_backup/tasks/main.yml +++ b/tests/integration/targets/nd_backup/tasks/main.yml @@ -1,7 +1,13 @@ # Test code for the ND modules # Copyright: (c) 2023, Shreyas Srish (@shrsr) +# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +- name: Set vars + ansible.builtin.set_fact: + nd_info: &nd_info + output_level: "info" + timeout: 90 - name: Create a directory if it does not exist ansible.builtin.file: @@ -9,173 +15,623 @@ state: directory mode: 0775 -- name: Create a backup nexus in check mode - cisco.nd.nd_backup: - output_level: debug - name: nexus - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: backup - check_mode: true - register: cm_add_nexus - -- name: Create a backup nexus - cisco.nd.nd_backup: - output_level: debug - name: nexus - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: backup - register: add_nexus - -- name: Create a backup nexus2 - cisco.nd.nd_backup: - output_level: debug - name: nexus2 - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus2_{{ ansible_host }}.tgz" - state: backup - register: add_nexus2 - -- name: Find backup nexus to ensure that it was created - ansible.builtin.find: - paths: "{{ role_path }}/backups" - patterns: "nexus_{{ ansible_host }}.tgz" - register: find_nexus - -- name: Find backup nexus2 to ensure that it was created - ansible.builtin.find: - paths: "{{ role_path }}/backups" - patterns: "nexus2_{{ ansible_host }}.tgz" - register: find_nexus2 - -- name: Query backup nexus2 - cisco.nd.nd_backup: - output_level: debug - name: nexus2 +# QUERY VERSION +- name: Query ND version + cisco.nd.nd_version: state: query - register: query_nexus2 + register: version -- name: Query all backups - cisco.nd.nd_backup: - output_level: debug - state: query - register: query_all - -- name: Delete backup nexus in check mode - cisco.nd.nd_backup: - output_level: debug - name: nexus - state: absent - check_mode: true - register: cm_remove_nexus - -- name: Delete backup nexus - cisco.nd.nd_backup: - output_level: debug - name: nexus - state: absent - register: delete_nexus - -- name: Delete backup nexus again - cisco.nd.nd_backup: - output_level: debug - name: nexus - state: absent - register: delete_nexus_again - -- name: Delete backup nexus2 - cisco.nd.nd_backup: - output_level: debug - name: nexus2 - state: absent - register: delete_nexus2 - -- name: Create backups with the same name - cisco.nd.nd_backup: - output_level: debug - name: "{{ item }}" - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus2_{{ ansible_host }}.tgz" - state: backup - loop: - - nexus - - nexus - -# Failure scenarios -- name: Create backup with password having a length < 8 characters - cisco.nd.nd_backup: - output_level: debug - name: nexus - encryption_key: test - file_location: "{{ role_path }}/backups/nexus2_{{ ansible_host }}.tgz" - state: backup - ignore_errors: true - register: add_nexus_incorrect_pwd_length - -- name: Try querying backup nexus - cisco.nd.nd_backup: - output_level: debug - name: nexus - state: query - ignore_errors: true - register: query_duplicate_nexus - -- name: Try deleting backup nexus - cisco.nd.nd_backup: - output_level: debug - name: nexus - state: absent - ignore_errors: true - register: delete_duplicate_nexus - -- name: Delete Backup with the wrong backup key - cisco.nd.nd_backup: - output_level: debug - name: nexus - backup_key: 0 - state: absent - ignore_errors: true - register: delete_wrong_key_nexus - -- name: Verify all assertions - ansible.builtin.assert: - that: - - cm_add_nexus.changed is true - - cm_add_nexus.current.spec.description == "nexus" - - cm_add_nexus.current.spec.password == "testtest" - - add_nexus.changed is true - - add_nexus.current.spec.description == "nexus" - - add_nexus.current.spec.password == "testtest" - - add_nexus2.changed is true - - add_nexus2.current.spec.description == "nexus2" - - add_nexus2.current.spec.password == "testtest" - - find_nexus.matched == 1 - - find_nexus2.matched == 1 - - query_nexus2.changed is false - - query_nexus2.current.description == "nexus2" - - query_all.changed is false - - query_all.current | length >= 2 - - cm_remove_nexus.changed is true - - delete_nexus.changed is true - - delete_nexus_again.changed is false - - delete_nexus2.changed is true - - cm_remove_nexus.current == delete_nexus.current == delete_nexus2.current == delete_nexus_again.current == {} - - add_nexus_incorrect_pwd_length.msg == "Please provide a minimum of 8 characters for the encryption key." - - query_duplicate_nexus.msg == delete_duplicate_nexus.msg == "Multiple backups with the name 'nexus' found. Please provide a backup key for the corresponding backup." - - delete_wrong_key_nexus.msg == "Provided key for the backup 'nexus' not found. Please provide a valid backup key by querying all the backups and looking up the desired backup key." - -- name: Query all backups for deleting them - cisco.nd.nd_backup: - output_level: debug - state: query - register: backups - -- name: Delete all backups to clean up environment - cisco.nd.nd_backup: - output_level: debug - name: nexus - backup_key: "{{ item }}" - state: absent - loop: "{{ backups.current | map(attribute='key') | list }}" - \ No newline at end of file +- name: Execute tasks only for ND version >= 3.2.1 + when: version.current.platformVersion is version('3.2.1', '>=') + block: + - name: Ensure local ND backup does not exist + cisco.nd.nd_backup: + <<: *nd_info + name: localbackup + state: absent + + - name: Ensure remote ND backup does not exist + cisco.nd.nd_backup: + <<: *nd_info + name: remotebackup + state: absent + + - name: Create local ND backup (check mode) + cisco.nd.nd_backup: + name: localbackup + file_location: "{{ role_path }}/backups/localbackup0_{{ ansible_host }}.tgz" + encryption_key: localbackup1 + state: backup + output_level: debug + check_mode: true + register: cm_add_local_backup + + - name: Create local ND backup + cisco.nd.nd_backup: + name: localbackup + encryption_key: localbackup1 + file_location: "{{ role_path }}/backups/localbackup0_{{ ansible_host }}.tgz" + state: backup + output_level: debug + register: add_local_backup + + - name: Query local ND backup to check the backup status + cisco.nd.nd_backup: + <<: *nd_info + name: localbackup + state: query + register: query_local_backup_status + until: + - query_local_backup_status.current is defined + - query_local_backup_status.current != {} + - query_local_backup_status.current.status == "completed" + retries: 40 + delay: 20 + + - name: Create local ND backup again + cisco.nd.nd_backup: + name: localbackup + encryption_key: localbackup1 + file_location: "{{ role_path }}/backups/localbackup0_{{ ansible_host }}.tgz" + state: backup + register: add_local_backup_again + + - name: Assertion check for create local ND backup + ansible.builtin.assert: + that: + - cm_add_local_backup is changed + - cm_add_local_backup.current.destination == "" + - cm_add_local_backup.current.encryptionKey == "localbackup1" + - cm_add_local_backup.current.name == "localbackup" + - cm_add_local_backup.current.type == "configOnly" + - cm_add_local_backup.previous == {} + - cm_add_local_backup.proposed.destination == "" + - cm_add_local_backup.proposed.encryptionKey == "localbackup1" + - cm_add_local_backup.proposed.name == "localbackup" + - cm_add_local_backup.proposed.type == "configOnly" + - add_local_backup is changed + - add_local_backup.current.name == "localbackup" + - add_local_backup.current.path == "localbackup.tar.gz" + - add_local_backup.current.schedule == "" + - add_local_backup.current.size == 0 + - add_local_backup.current.endTime == "0001-01-01T00:00:00Z" + - add_local_backup.current.startTime is defined + - add_local_backup.current.status == "inProgress" + - add_local_backup.current.type == "configOnly" + - add_local_backup.current.user is defined + - add_local_backup.previous == {} + - add_local_backup.proposed.destination == "" + - add_local_backup.proposed.encryptionKey == "localbackup1" + - add_local_backup.proposed.name == "localbackup" + - add_local_backup.proposed.type == "configOnly" + - add_local_backup_again is not changed + - add_local_backup_again.current.destination == "" + - add_local_backup_again.current.details.progress == 100 + - add_local_backup_again.current.name == "localbackup" + - add_local_backup_again.current.path == "localbackup.tar.gz" + - add_local_backup_again.current.schedule == "" + - add_local_backup_again.current.size is defined + - add_local_backup_again.current.startTime is defined + - add_local_backup_again.current.status == "completed" + - add_local_backup_again.current.type == "configOnly" + - add_local_backup_again.current.user is defined + - add_local_backup_again.previous.destination == "" + - add_local_backup_again.previous.details.progress == 100 + - add_local_backup_again.previous.name == "localbackup" + - add_local_backup_again.previous.path == "localbackup.tar.gz" + - add_local_backup_again.previous.schedule == "" + - add_local_backup_again.previous.size is defined + - add_local_backup_again.previous.startTime is defined + - add_local_backup_again.previous.status == "completed" + - add_local_backup_again.previous.type == "configOnly" + - add_local_backup_again.previous.user is defined + + - name: Query local ND backup + cisco.nd.nd_backup: + name: localbackup + state: query + register: query_local_backup + + - name: Download local ND backup file + cisco.nd.nd_backup: + name: localbackup + file_location: "{{ role_path }}/backups/localbackup1_{{ ansible_host }}.tgz" + state: download + register: download_local_backup + + - name: Create remote ND backup (check mode) + cisco.nd.nd_backup: + name: remotebackup + encryption_key: remotebackup1 + remote_location: test + state: backup + output_level: debug + check_mode: true + register: cm_add_remote_backup + + - name: Create remote ND backup + cisco.nd.nd_backup: + name: remotebackup + encryption_key: remotebackup1 + remote_location: test + state: backup + output_level: debug + register: add_remote_backup + + - name: Query remote ND backup to check the backup status + cisco.nd.nd_backup: + <<: *nd_info + name: remotebackup + state: query + register: query_remote_backup_status + until: + - query_remote_backup_status.current is defined + - query_remote_backup_status.current != {} + - query_remote_backup_status.current.status == "completed" + retries: 40 + delay: 20 + + - name: Create remote ND backup again + cisco.nd.nd_backup: + name: remotebackup + encryption_key: remotebackup1 + remote_location: test + state: backup + register: add_remote_backup_again + + - name: Assertion check for create remote ND backup + ansible.builtin.assert: + that: + - cm_add_remote_backup is changed + - cm_add_remote_backup.current.destination == "test" + - cm_add_remote_backup.current.encryptionKey == "remotebackup1" + - cm_add_remote_backup.current.name == "remotebackup" + - cm_add_remote_backup.current.type == "configOnly" + - cm_add_remote_backup.previous == {} + - cm_add_remote_backup.proposed.destination == "test" + - cm_add_remote_backup.proposed.encryptionKey == "remotebackup1" + - cm_add_remote_backup.proposed.name == "remotebackup" + - cm_add_remote_backup.proposed.type == "configOnly" + - add_remote_backup is changed + - add_remote_backup.current.destination == "test" + - add_remote_backup.current.name == "remotebackup" + - add_remote_backup.current.path is defined + - add_remote_backup.current.schedule == "" + - add_remote_backup.current.size is defined + - add_remote_backup.current.startTime is defined + - add_remote_backup.current.status == "inProgress" + - add_remote_backup.current.type == "configOnly" + - add_remote_backup.current.user is defined + - add_remote_backup.previous == {} + - add_remote_backup.proposed.destination == "test" + - add_remote_backup.proposed.encryptionKey == "remotebackup1" + - add_remote_backup.proposed.name == "remotebackup" + - add_remote_backup.proposed.type == "configOnly" + - add_remote_backup_again is not changed + - add_remote_backup_again.current.destination == "test" + - add_remote_backup_again.current.details.progress == 100 + - add_remote_backup_again.current.name == "remotebackup" + - add_remote_backup_again.current.path is defined + - add_remote_backup_again.current.schedule == "" + - add_remote_backup_again.current.size is defined + - add_remote_backup_again.current.startTime is defined + - add_remote_backup_again.current.status == "completed" + - add_remote_backup_again.current.type == "configOnly" + - add_remote_backup_again.current.user is defined + - add_remote_backup_again.previous.destination == "test" + - add_remote_backup_again.previous.details.progress == 100 + - add_remote_backup_again.previous.name == "remotebackup" + - add_remote_backup_again.previous.path is defined + - add_remote_backup_again.previous.schedule == "" + - add_remote_backup_again.previous.size is defined + - add_remote_backup_again.previous.startTime is defined + - add_remote_backup_again.previous.status == "completed" + - add_remote_backup_again.previous.type == "configOnly" + - add_remote_backup_again.previous.user is defined + + - name: Query remote ND backup + cisco.nd.nd_backup: + name: remotebackup + state: query + register: query_remote_backup + + - name: Query all ND backups + cisco.nd.nd_backup: + state: query + register: query_all_backups + + - name: Assertion check for query ND backup + ansible.builtin.assert: + that: + - query_local_backup is not changed + - query_local_backup.current.destination == "" + - query_local_backup.current.details.progress == 100 + - query_local_backup.current.name == "localbackup" + - query_local_backup.current.path == "localbackup.tar.gz" + - query_local_backup.current.schedule == "" + - query_local_backup.current.size is defined + - query_local_backup.current.startTime is defined + - query_local_backup.current.endTime is defined + - query_local_backup.current.status == "completed" + - query_local_backup.current.type == "configOnly" + - query_local_backup.current.user is defined + - query_remote_backup is not changed + - query_remote_backup.current.destination == "test" + - query_remote_backup.current.details.progress == 100 + - query_remote_backup.current.name == "remotebackup" + - query_remote_backup.current.path is defined + - query_remote_backup.current.schedule == "" + - query_remote_backup.current.size is defined + - query_remote_backup.current.startTime is defined + - query_remote_backup.current.endTime is defined + - query_remote_backup.current.status == "completed" + - query_remote_backup.current.type == "configOnly" + - query_remote_backup.current.user is defined + - query_all_backups is not changed + - query_all_backups.current | length >= 2 + - "'localbackup' in query_all_backups.current | map(attribute='name') | list" + - "'remotebackup' in query_all_backups.current | map(attribute='name') | list" + + - name: Download remote ND backup file + cisco.nd.nd_backup: + name: remotebackup + file_location: "{{ role_path }}/backups/remotebackup_{{ ansible_host }}.tgz" + state: download + register: download_remote_backup + + - name: Assertion check for download ND backup files + ansible.builtin.assert: + that: + - download_local_backup is changed + - download_local_backup.current.destination == "" + - download_local_backup.current.details.progress == 100 + - download_local_backup.current.name == "localbackup" + - download_local_backup.current.path == "localbackup.tar.gz" + - download_local_backup.current.schedule == "" + - download_local_backup.current.size is defined + - download_local_backup.current.startTime is defined + - download_local_backup.current.endTime is defined + - download_local_backup.current.status == "completed" + - download_local_backup.current.type == "configOnly" + - download_local_backup.current.user == "admin" + - download_remote_backup is changed + - download_remote_backup.current.destination == "test" + - download_remote_backup.current.details.progress == 100 + - download_remote_backup.current.name == "remotebackup" + - download_remote_backup.current.path is defined + - download_remote_backup.current.schedule == "" + - download_remote_backup.current.status == "completed" + - download_remote_backup.current.type == "configOnly" + + - name: Find local backup to ensure that it was downloaded + ansible.builtin.find: + paths: "{{ role_path }}/backups" + patterns: "localbackup0_{{ ansible_host }}.tgz" + register: find_local_backup0 + + - name: Find local backup to ensure that it was downloaded + ansible.builtin.find: + paths: "{{ role_path }}/backups" + patterns: "localbackup1_{{ ansible_host }}.tgz" + register: find_local_backup1 + + - name: Find remote backup to ensure that it was downloaded + ansible.builtin.find: + paths: "{{ role_path }}/backups" + patterns: "remotebackup_{{ ansible_host }}.tgz" + register: find_remote_backup + + - name: Assertion check for find ND backups to ensure that it was downloaded + ansible.builtin.assert: + that: + - find_local_backup0 is not changed + - find_local_backup0.examined == 3 + - find_local_backup0.files.0.path is match( ".+/tests/integration/targets/nd_backup/backups/localbackup0_.+.tgz") + - find_local_backup0.matched == 1 + - find_local_backup1 is not changed + - find_local_backup1.examined == 3 + - find_local_backup1.files.0.path is match( ".+/tests/integration/targets/nd_backup/backups/localbackup1_.+.tgz") + - find_local_backup1.matched == 1 + - find_remote_backup is not changed + - find_remote_backup.examined == 3 + - find_remote_backup.files.0.path is match (".+/tests/integration/targets/nd_backup/backups/remotebackup_.+.tgz") + - find_remote_backup.matched == 1 + + # ERROR + - name: Negative test for create ND backup with encryption_key < 8 characters + cisco.nd.nd_backup: + name: ntlocalbackup + encryption_key: local + state: backup + register: nt_add_backup_with_invalid_encryption_key1 + ignore_errors: true + + - name: Negative test for create ND backup with string only encryption_key + cisco.nd.nd_backup: + name: ntlocalbackup + encryption_key: localbackup + state: backup + register: nt_add_backup_with_invalid_encryption_key2 + ignore_errors: true + + - name: Assertion check for negative test for create ND backup with string only encryption_key + ansible.builtin.assert: + that: + - nt_add_backup_with_invalid_encryption_key1 is not changed + - nt_add_backup_with_invalid_encryption_key1.msg == "Please provide a minimum of 8 alphanumeric characters for the encryption key." + - nt_add_backup_with_invalid_encryption_key2 is not changed + - nt_add_backup_with_invalid_encryption_key2.msg == "The encryption_key must contain at least one letter and one number, and have a minimum length of 8 characters." + + - name: Ensure local ND backup does not exist (check_mode) + cisco.nd.nd_backup: + <<: *nd_info + name: localbackup + state: absent + register: cm_rm_local_backup + check_mode: true + + - name: Ensure remote ND backup does not exist (check_mode) + cisco.nd.nd_backup: + <<: *nd_info + name: remotebackup + state: absent + register: cm_rm_remote_backup + check_mode: true + + - name: Ensure local ND backup does not exist + cisco.nd.nd_backup: + <<: *nd_info + name: localbackup + state: absent + register: rm_local_backup + + - name: Ensure remote ND backup does not exist + cisco.nd.nd_backup: + <<: *nd_info + name: remotebackup + state: absent + register: rm_remote_backup + + - name: Ensure local ND backup does not exist again + cisco.nd.nd_backup: + <<: *nd_info + name: localbackup + state: absent + register: rm_local_backup_again + + - name: Ensure remote ND backup does not exist again + cisco.nd.nd_backup: + <<: *nd_info + name: remotebackup + state: absent + register: rm_remote_backup_again + + - name: Assertion check for remove ND backupa + ansible.builtin.assert: + that: + - cm_rm_local_backup is changed + - cm_rm_local_backup.current == {} + - cm_rm_local_backup.previous.destination == "" + - cm_rm_local_backup.previous.details.progress == 100 + - cm_rm_local_backup.previous.name == "localbackup" + - cm_rm_local_backup.previous.path == "localbackup.tar.gz" + - cm_rm_local_backup.previous.schedule == "" + - cm_rm_local_backup.previous.status == "completed" + - cm_rm_local_backup.previous.type == "configOnly" + - cm_rm_remote_backup is changed + - cm_rm_remote_backup.current == {} + - cm_rm_remote_backup.previous.destination == "test" + - cm_rm_remote_backup.previous.details.progress == 100 + - cm_rm_remote_backup.previous.name == "remotebackup" + - cm_rm_remote_backup.previous.path is defined + - cm_rm_remote_backup.previous.schedule == "" + - cm_rm_remote_backup.previous.status == "completed" + - cm_rm_remote_backup.previous.type == "configOnly" + - rm_local_backup is changed + - rm_local_backup.current == {} + - rm_local_backup.previous.destination == "" + - rm_local_backup.previous.details.progress == 100 + - rm_local_backup.previous.name == "localbackup" + - rm_local_backup.previous.path == "localbackup.tar.gz" + - rm_local_backup.previous.schedule == "" + - rm_local_backup.previous.status == "completed" + - rm_local_backup.previous.type == "configOnly" + - rm_remote_backup is changed + - rm_remote_backup.current == {} + - rm_remote_backup.previous.destination == "test" + - rm_remote_backup.previous.details.progress == 100 + - rm_remote_backup.previous.name == "remotebackup" + - rm_remote_backup.previous.path is defined + - rm_remote_backup.previous.schedule == "" + - rm_remote_backup.previous.status == "completed" + - rm_remote_backup.previous.type == "configOnly" + - rm_local_backup_again is not changed + - rm_local_backup_again.current == {} + - rm_local_backup_again.previous == {} + - rm_remote_backup_again is not changed + - rm_remote_backup_again.current == {} + - rm_remote_backup_again.previous == {} + +- name: Execute tasks only for ND version < 3.2.1 + when: version.current.platformVersion is version('3.2.1', '<') + block: + - name: Create a backup nexus in check mode + cisco.nd.nd_backup: + output_level: debug + name: nexus + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: backup + check_mode: true + register: cm_add_nexus + + - name: Create a backup nexus + cisco.nd.nd_backup: + output_level: debug + name: nexus + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: backup + register: add_nexus + + - name: Create a backup nexus2 + cisco.nd.nd_backup: + output_level: debug + name: nexus2 + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus2_{{ ansible_host }}.tgz" + state: backup + register: add_nexus2 + + - name: Find backup nexus to ensure that it was created + ansible.builtin.find: + paths: "{{ role_path }}/backups" + patterns: "nexus_{{ ansible_host }}.tgz" + register: find_nexus + + - name: Find backup nexus2 to ensure that it was created + ansible.builtin.find: + paths: "{{ role_path }}/backups" + patterns: "nexus2_{{ ansible_host }}.tgz" + register: find_nexus2 + + - name: Query backup nexus2 + cisco.nd.nd_backup: + output_level: debug + name: nexus2 + state: query + register: query_nexus2 + + - name: Query all backups + cisco.nd.nd_backup: + output_level: debug + state: query + register: query_all + + - name: Delete backup nexus in check mode + cisco.nd.nd_backup: + output_level: debug + name: nexus + state: absent + check_mode: true + register: cm_remove_nexus + + - name: Delete backup nexus + cisco.nd.nd_backup: + output_level: debug + name: nexus + state: absent + register: delete_nexus + + - name: Delete backup nexus again + cisco.nd.nd_backup: + output_level: debug + name: nexus + state: absent + register: delete_nexus_again + + - name: Delete backup nexus2 + cisco.nd.nd_backup: + output_level: debug + name: nexus2 + state: absent + register: delete_nexus2 + + - name: Create backups with the same name + cisco.nd.nd_backup: + output_level: debug + name: "{{ item }}" + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus2_{{ ansible_host }}.tgz" + state: backup + loop: + - nexus + - nexus + + # Failure scenarios + - name: Create backup with password having a length < 8 characters + cisco.nd.nd_backup: + output_level: debug + name: nexus + encryption_key: test + file_location: "{{ role_path }}/backups/nexus2_{{ ansible_host }}.tgz" + state: backup + ignore_errors: true + register: add_nexus_incorrect_pwd_length + + - name: Create backup without file_location + cisco.nd.nd_backup: + output_level: debug + name: nexus + encryption_key: test + state: backup + ignore_errors: true + register: add_nexus_without_file_location + + - name: Try querying backup nexus + cisco.nd.nd_backup: + output_level: debug + name: nexus + state: query + ignore_errors: true + register: query_duplicate_nexus + + - name: Try deleting backup nexus + cisco.nd.nd_backup: + output_level: debug + name: nexus + state: absent + ignore_errors: true + register: delete_duplicate_nexus + + - name: Delete Backup with the wrong backup key + cisco.nd.nd_backup: + output_level: debug + name: nexus + backup_key: 0 + state: absent + ignore_errors: true + register: delete_wrong_key_nexus + + - name: Verify all assertions + ansible.builtin.assert: + that: + - cm_add_nexus.changed is true + - cm_add_nexus.current.spec.description == "nexus" + - cm_add_nexus.current.spec.password == "testtest" + - add_nexus.changed is true + - add_nexus.current.spec.description == "nexus" + - add_nexus.current.spec.password == "testtest" + - add_nexus2.changed is true + - add_nexus2.current.spec.description == "nexus2" + - add_nexus2.current.spec.password == "testtest" + - find_nexus.matched == 1 + - find_nexus2.matched == 1 + - query_nexus2.changed is false + - query_nexus2.current.description == "nexus2" + - query_all.changed is false + - query_all.current | length >= 2 + - cm_remove_nexus.changed is true + - delete_nexus.changed is true + - delete_nexus_again.changed is false + - delete_nexus2.changed is true + - cm_remove_nexus.current == delete_nexus.current == delete_nexus2.current == delete_nexus_again.current == {} + - add_nexus_incorrect_pwd_length.msg == "Please provide a minimum of 8 characters for the encryption key." + - query_duplicate_nexus.msg == delete_duplicate_nexus.msg == "Multiple backups with the name 'nexus' found. Please provide a backup key for the corresponding backup." + - delete_wrong_key_nexus.msg == "Provided key for the backup 'nexus' not found. Please provide a valid backup key by querying all the backups and looking up the desired backup key." + - add_nexus_without_file_location is not changed + - add_nexus_without_file_location.msg == "Parameter 'file_location' is required when state is 'backup|download' for ND versions < 3.2.1." + + - name: Query all backups for deleting them + cisco.nd.nd_backup: + output_level: debug + state: query + register: backups + + - name: Delete all backups to clean up environment + cisco.nd.nd_backup: + output_level: debug + name: nexus + backup_key: "{{ item }}" + state: absent + loop: "{{ backups.current | map(attribute='key') | list }}" diff --git a/tests/integration/targets/nd_backup_restore/tasks/main.yml b/tests/integration/targets/nd_backup_restore/tasks/main.yml index cfb9c712..6bbbc84c 100644 --- a/tests/integration/targets/nd_backup_restore/tasks/main.yml +++ b/tests/integration/targets/nd_backup_restore/tasks/main.yml @@ -1,7 +1,19 @@ # Test code for the ND modules # Copyright: (c) 2023, Shreyas Srish (@shrsr) +# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +- name: Set vars + ansible.builtin.set_fact: + nd_info: &nd_info + output_level: "info" + timeout: 90 + +# QUERY VERSION +- name: Query ND version + cisco.nd.nd_version: + state: query + register: version - name: Create a directory if it does not exist ansible.builtin.file: @@ -9,241 +21,692 @@ state: directory mode: 0775 -- name: Create a route to have some config in the route cluster configuration - cisco.nd.nd_cluster_config_route: - output_level: debug - destination_ip: 12.23.45.68/32 - target_network: data - state: present - -- name: Create a backup nexus - cisco.nd.nd_backup: - output_level: debug - name: nexus - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: backup - -- name: Query all routes - cisco.nd.nd_cluster_config_route: - output_level: debug - state: query - register: query_all_routes - -- name: Delete all routes to change the route configuration - cisco.nd.nd_cluster_config_route: - output_level: debug - destination_ip: "{{ item }}" - state: absent - loop: "{{ query_all_routes.current | map(attribute='spec.destination') | list }}" - -- name: Import the backup file nexus with restore job's name called nexus in check mode - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: restore - check_mode: true - register: restore_nexus_cm - -- name: Import the backup file nexus with restore job's name called nexus - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: restore - register: restore_nexus - -- name: Query all routes again - cisco.nd.nd_cluster_config_route: - output_level: debug - state: query - register: query_all_routes_again - -- name: Delete all routes again to add another restore job - cisco.nd.nd_cluster_config_route: - output_level: debug - destination_ip: "{{ item }}" - state: absent - loop: "{{ query_all_routes.current | map(attribute='spec.destination') | list }}" - -- name: Import the backup file nexus again with a different restore name nexus2 - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus2 - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: restore - register: restore_nexus2 - -- name: Query all routes after second import - cisco.nd.nd_cluster_config_route: - output_level: debug - state: query - register: query_all_routes_after_second_import - -- name: Delete all routes again to add a third restore job - cisco.nd.nd_cluster_config_route: - output_level: debug - destination_ip: "{{ item }}" - state: absent - loop: "{{ query_all_routes.current | map(attribute='spec.destination') | list }}" - -- name: Import the backup file with the name nexus to have multiple restore jobs with the same name - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: restore - register: restore_nexus_again - -- name: Query all routes after third import - cisco.nd.nd_cluster_config_route: - output_level: debug - state: query - register: query_all_routes_after_third_import - -- name: Query a restore job - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus2 - state: query - register: query_nexus2 - -- name: Query all restore jobs - cisco.nd.nd_backup_restore: - output_level: debug - state: query - register: query_all - -- name: Delete a restore job in check mode - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus2 - state: absent - check_mode: true - register: delete_nexus2_cm - -- name: Delete a restore job - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus2 - state: absent - register: delete_nexus2 - -- name: Delete a restore job again - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus2 - state: absent - register: delete_nexus2_again - -# Failure Scenarios -- name: Try querying restore nexus - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - state: query - ignore_errors: true - register: query_duplicate_nexus - -- name: Try deleting restore nexus - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - state: absent - ignore_errors: true - register: delete_duplicate_nexus - -- name: Delete restore with the wrong restore key - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - restore_key: 0 - state: absent - ignore_errors: true - register: delete_wrong_key_nexus - -- name: Import backup with password having a length < 8 characters - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - encryption_key: test - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: restore - ignore_errors: true - register: import_nexus_incorrect_pwd_length - -- name: Import backup to ND which already has the routes configured - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - encryption_key: testtest - file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" - state: restore - ignore_errors: true - register: import_nexus_existing - -- name: Gather and sort the list of specs - ansible.builtin.set_fact: - query_all_routes_original: "{{ query_all_routes.current | map(attribute='spec') | list | sort(attribute='destination') }}" - query_all_routes_restored1: "{{ query_all_routes_again.current | map(attribute='spec') | list | sort(attribute='destination') }}" - query_all_routes_restored2: "{{ query_all_routes_after_second_import.current | map(attribute='spec') | list | sort(attribute='destination') }}" - query_all_routes_restored3: "{{ query_all_routes_after_third_import.current | map(attribute='spec') | list | sort(attribute='destination') }}" - -- name: Verify all assertions - ansible.builtin.assert: - that: - - restore_nexus_cm.changed is true - - restore_nexus.changed is true - - restore_nexus2.changed is true - - restore_nexus_again.changed is true - - query_all_routes_original == query_all_routes_restored1 == query_all_routes_restored2 == query_all_routes_restored3 - - query_nexus2.changed is false - - query_nexus2.current.description == "nexus2" - - query_all.current | length >= 3 - - delete_nexus2.changed is true - - delete_nexus2_cm.changed is true - - delete_nexus2_again.changed is false - - delete_nexus2.current == delete_nexus2_cm.current == delete_nexus2_again.current == {} - - import_nexus_incorrect_pwd_length.msg == "The encryption key must have a minium of 8 characters." - - '"Import of configuration failed. System is already configured with routes/persistent-ip-pools/syslogs" in import_nexus_existing.raw' - - query_duplicate_nexus.msg == delete_duplicate_nexus.msg == "Multiple restore jobs with the name 'nexus' found. Please provide a restore key for the corresponding restored job." - - delete_wrong_key_nexus.msg == "Provided key for the restore 'nexus' not found. Please provide a valid restore key by querying all the restored jobs and looking up the desired restore key." - -- name: Query all restored jobs for deleting them - cisco.nd.nd_backup_restore: - output_level: debug - state: query - register: restores - -- name: Delete the other restored jobs to clean up the environment - cisco.nd.nd_backup_restore: - output_level: debug - name: nexus - restore_key: "{{ item }}" - state: absent - loop: "{{ restores.current | map(attribute='key') | list }}" - -- name: Query all backups for deleting them - cisco.nd.nd_backup: - output_level: debug - state: query - register: backups - -- name: Delete all backup jobs to clean up environment - cisco.nd.nd_backup: - output_level: debug - name: nexus - backup_key: "{{ item }}" - state: absent - loop: "{{ backups.current | map(attribute='key') | list }}" - -- name: Delete the route created in the beginning of this test - cisco.nd.nd_cluster_config_route: - output_level: debug - destination_ip: 12.23.45.68/32 - state: absent +- name: Execute tasks only for ND version >= 3.2.1 + when: version.current.platformVersion is version('3.2.1', '>=') + block: + # CLEAN TEST ENVIRONMENT + - name: Query all routes + cisco.nd.nd_rest: + method: get + path: /api/config/class/routes/ + register: query_all_routes + + - name: Delete all Data Routes + cisco.nd.nd_rest: + method: delete + ignore_previous_state: true + path: "/api/config/routes/{{ item.destination }}" + loop: "{{ query_all_routes.current | list }}" + + - name: Query all backups + cisco.nd.nd_backup: + output_level: debug + state: query + register: query_all_backups + + - name: Delete all backups + cisco.nd.nd_backup: + output_level: debug + name: "{{ item.name }}" + state: absent + loop: "{{ query_all_backups.current | list }}" + delay: 5 + + # Local backup restore test + - name: Add Data Route before ND local backup + cisco.nd.nd_rest: + path: /api/config/routes/ + method: post + content: + target: Data + destination: 12.23.45.68/32 + + - name: Create ND local backup + cisco.nd.nd_backup: + output_level: debug + name: localbackup + encryption_key: testtest1 + state: backup + + - name: Restore ND backup with name and file_location + cisco.nd.nd_backup_restore: + output_level: debug + name: localbackup + encryption_key: testtest1 + file_location: "/tmp/localbackup.tar.gz" + state: restore + register: nt_name_and_file_location + ignore_errors: true + + - name: Restore ND backup when backup is in progress + cisco.nd.nd_backup_restore: + <<: *nd_info + output_level: debug + name: localbackup + encryption_key: testtest1 + state: restore + register: nt_restore + ignore_errors: true + + - name: Query ND local backup status + cisco.nd.nd_backup: + <<: *nd_info + name: localbackup + state: query + register: query_local_backup_status + until: + - query_local_backup_status.current is defined + - query_local_backup_status.current != {} + - query_local_backup_status.current.status == "completed" + retries: 80 + delay: 20 + + - name: Delete Data Route after ND local backup + cisco.nd.nd_rest: + ignore_previous_state: true + method: delete + path: /api/config/routes/12.23.45.68%2f32 + + - name: Restore ND local backup from the ND backup list (check_mode) + cisco.nd.nd_backup_restore: + output_level: debug + name: localbackup + encryption_key: testtest1 + state: restore + check_mode: true + register: cm_restore_local_backup + + - name: Restore ND local backup from the ND backup list + cisco.nd.nd_backup_restore: + <<: *nd_info + output_level: debug + name: localbackup + encryption_key: testtest1 + state: restore + register: restore_local_backup + + - name: Query ND local backup restore status + cisco.nd.nd_backup_restore: + <<: *nd_info + state: query + register: query_local_backup_restore_status + until: + - query_local_backup_restore_status.current is defined + - query_local_backup_restore_status.current != {} + - query_local_backup_restore_status.current.state == "completed" + retries: 200 + delay: 20 + + - name: Query Data Route after ND local backup restore + cisco.nd.nd_rest: + method: get + path: /api/config/class/routes/ + register: query_all_routes + + - name: Assertion check for ND local backup restore + ansible.builtin.assert: + that: + - cm_restore_local_backup is changed + - cm_restore_local_backup.current.fileUploadPayload.fileLocation is none + - cm_restore_local_backup.current.importPayload.name == "localbackup" + - cm_restore_local_backup.current.restorePayload.ignorePersistentIPs == false + - cm_restore_local_backup.current.restorePayload.type == "configOnly" + - restore_local_backup is changed + - restore_local_backup.current != {} + - restore_local_backup.current.type == "configOnly" + - restore_local_backup.current.state == "processing" + - restore_local_backup.current.operation == "restore" + - restore_local_backup.current.restoreConfiguration.path == "localbackup.tar.gz" + - restore_local_backup.current.restoreConfiguration.source == "" + - restore_local_backup.current.restoreConfiguration.type == "configOnly" + - query_all_routes is not changed + - query_all_routes.current | length == 1 + - query_all_routes.current.0.target == "Data" + - query_all_routes.current.0.destination == "12.23.45.68/32" + - nt_name_and_file_location is failed + - nt_name_and_file_location.msg == "The parameters name and (remote_location or file_location) cannot be specified at the same time." + - nt_restore is failed + - nt_restore.msg is match ("The restore operation could not proceed because a system backup is in progress (.+% complete).") + + - name: Delete all Data Routes + cisco.nd.nd_rest: + method: delete + ignore_previous_state: true + path: "/api/config/routes/{{ item.destination }}" + loop: "{{ query_all_routes.current | list }}" + + - name: Query all backups + cisco.nd.nd_backup: + output_level: debug + state: query + register: query_all_backups + + - name: Delete all backups + cisco.nd.nd_backup: + output_level: debug + name: "{{ item.name }}" + state: absent + loop: "{{ query_all_backups.current | list }}" + delay: 5 + + # Remote backup restore test + - name: Add Data Route before ND remote backup + cisco.nd.nd_rest: + output_level: debug + path: /api/config/routes/ + method: post + content: "{{ item }}" + loop: + - target: Data + destination: 12.23.45.68/32 + - target: Data + destination: 12.23.45.69/32 + + - name: Create ND remote backup + cisco.nd.nd_backup: + output_level: debug + name: remotebackup + encryption_key: testtest1 + remote_location: test + state: backup + + - name: Query ND remote backup status + cisco.nd.nd_backup: + <<: *nd_info + name: remotebackup + state: query + register: query_remote_backup_status + until: + - query_remote_backup_status.current is defined + - query_remote_backup_status.current != {} + - query_remote_backup_status.current.status == "completed" + retries: 80 + delay: 20 + + - name: Delete Data Route after ND remote backup + cisco.nd.nd_rest: + output_level: debug + ignore_previous_state: true + method: delete + path: "{{ item }}" + loop: + - /api/config/routes/12.23.45.68%2f32 + - /api/config/routes/12.23.45.69%2f32 + + - name: Restore ND remote backup from the remote storage location (check_mode) + cisco.nd.nd_backup_restore: + output_level: debug + remote_location: test # Remote storage location name + encryption_key: testtest1 + file_location: "/home/backup/{{ansible_host | replace('.', '_')}}/remotebackup.tar.gz" + state: restore + check_mode: true + register: cm_restore_remote_backup + + - name: Restore ND remote backup from the remote storage location + cisco.nd.nd_backup_restore: + <<: *nd_info + output_level: debug + remote_location: test # Remote storage location name + encryption_key: testtest1 + file_location: "/home/backup/{{ansible_host | replace('.', '_')}}/remotebackup.tar.gz" + state: restore + register: restore_remote_backup + + - name: Query ND remote backup restore status + cisco.nd.nd_backup_restore: + <<: *nd_info + state: query + register: query_remote_backup_restore_status + until: + - query_remote_backup_restore_status.current is defined + - query_remote_backup_restore_status.current != {} + - query_remote_backup_restore_status.current.state == "completed" + retries: 200 + delay: 20 + + - name: Query Data Route after ND remote backup restore + cisco.nd.nd_rest: + output_level: debug + method: get + path: /api/config/class/routes/ + register: query_all_routes + + - name: Assertion check for ND remote backup restore + ansible.builtin.assert: + that: + - cm_restore_remote_backup is changed + - cm_restore_remote_backup.current.fileUploadPayload.fileLocation == "/home/backup/{{ansible_host | replace('.', '_')}}/remotebackup.tar.gz" + - cm_restore_remote_backup.current.importPayload.path == "/home/backup/{{ansible_host | replace('.', '_')}}/remotebackup.tar.gz" + - cm_restore_remote_backup.current.importPayload.source == "test" + - cm_restore_remote_backup.current.restorePayload.ignorePersistentIPs == false + - cm_restore_remote_backup.current.restorePayload.type == "configOnly" + - restore_remote_backup is changed + - restore_remote_backup.current != {} + - restore_remote_backup.current.type == "configOnly" + - restore_remote_backup.current.state == "processing" + - restore_remote_backup.current.operation == "restore" + - restore_remote_backup.current.restoreConfiguration.path == "/home/backup/{{ansible_host | replace('.', '_')}}/remotebackup.tar.gz" + - restore_remote_backup.current.restoreConfiguration.source == "test" # Remote storage location name + - restore_remote_backup.current.restoreConfiguration.type == "configOnly" + - query_all_routes is not changed + - query_all_routes.current | length == 2 + - query_all_routes.current.0.target == "Data" + - query_all_routes.current.0.destination == "12.23.45.68/32" + - query_all_routes.current.1.target == "Data" + - query_all_routes.current.1.destination == "12.23.45.69/32" + + - name: Delete all Data Routes + cisco.nd.nd_rest: + output_level: debug + method: delete + ignore_previous_state: true + path: "/api/config/routes/{{ item.destination }}" + loop: "{{ query_all_routes.current | list }}" + + - name: Query all backups + cisco.nd.nd_backup: + output_level: debug + state: query + register: query_all_backups + + - name: Delete all backups + cisco.nd.nd_backup: + output_level: debug + name: "{{ item.name }}" + state: absent + loop: "{{ query_all_backups.current | list }}" + delay: 5 + + # Local backup and download restore test + - name: Add Data Route before ND local backup download + cisco.nd.nd_rest: + output_level: debug + path: /api/config/routes/ + method: post + content: "{{ item }}" + loop: + - target: Data + destination: 12.23.45.68/32 + - target: Data + destination: 12.23.45.69/32 + - target: Data + destination: 12.23.45.70/32 + + - name: Create ND local backup + cisco.nd.nd_backup: + output_level: debug + name: localbackupbownload + encryption_key: testtest1 + state: backup + + - name: Query ND local backup status + cisco.nd.nd_backup: + <<: *nd_info + name: localbackupbownload + state: query + register: query_local_backup_status + until: + - query_local_backup_status.current is defined + - query_local_backup_status.current != {} + - query_local_backup_status.current.status == "completed" + retries: 80 + delay: 20 + + - name: Download ND local backup file + cisco.nd.nd_backup: + name: localbackupbownload + file_location: "{{ role_path }}/backups/localbackupbownload.tar.gz" + state: download + + - name: Delete Data Route after ND local and download + cisco.nd.nd_rest: + output_level: debug + method: delete + ignore_previous_state: true + path: "{{ item }}" + loop: + - /api/config/routes/12.23.45.68%2f32 + - /api/config/routes/12.23.45.69%2f32 + - /api/config/routes/12.23.45.70%2f32 + + - name: Restore ND local downloaded backup from the local machine (check_mode) + cisco.nd.nd_backup_restore: + output_level: debug + encryption_key: testtest1 + file_location: "{{ role_path }}/backups/localbackupbownload.tar.gz" + state: restore + check_mode: true + register: cm_restore_downloaded_local_backup + + - name: Restore ND local downloaded backup from the local machine + cisco.nd.nd_backup_restore: + <<: *nd_info + output_level: debug + encryption_key: testtest1 + file_location: "{{ role_path }}/backups/localbackupbownload.tar.gz" + state: restore + register: restore_downloaded_local_backup + + - name: Query ND downloaded local backup restore status + cisco.nd.nd_backup_restore: + <<: *nd_info + state: query + register: query_downloaded_backup_restore_status + until: + - query_downloaded_backup_restore_status.current is defined + - query_downloaded_backup_restore_status.current != {} + - query_downloaded_backup_restore_status.current.state == "completed" + retries: 200 + delay: 20 + + - name: Query Data Route after ND downloaded local backup file restore # ND4.1 routes API is not published officially + cisco.nd.nd_rest: + output_level: debug + method: get + path: /api/config/class/routes/ + register: query_all_routes + + - name: Assertion check for ND remote backup restore + ansible.builtin.assert: + that: + - cm_restore_downloaded_local_backup is changed + - cm_restore_downloaded_local_backup.current.fileUploadPayload.fileLocation is match (".+/localbackupbownload.tar.gz") + - cm_restore_downloaded_local_backup.current.importPayload.name is not defined + - cm_restore_downloaded_local_backup.current.importPayload.source is not defined + - cm_restore_downloaded_local_backup.current.importPayload.path is not defined + - cm_restore_downloaded_local_backup.current.restorePayload.ignorePersistentIPs == false + - cm_restore_downloaded_local_backup.current.restorePayload.type == "configOnly" + - restore_downloaded_local_backup is changed + - restore_downloaded_local_backup.current != {} + - restore_downloaded_local_backup.current.type == "configOnly" + - restore_downloaded_local_backup.current.state == "processing" + - restore_downloaded_local_backup.current.operation == "restore" + - restore_downloaded_local_backup.current.restoreConfiguration.path is defined + - restore_downloaded_local_backup.current.restoreConfiguration.source == "" + - restore_downloaded_local_backup.current.restoreConfiguration.type == "configOnly" + - query_all_routes is not changed + - query_all_routes.current | length == 3 + - query_all_routes.current.0.target == "Data" + - query_all_routes.current.0.destination == "12.23.45.68/32" + - query_all_routes.current.1.target == "Data" + - query_all_routes.current.1.destination == "12.23.45.69/32" + - query_all_routes.current.2.target == "Data" + - query_all_routes.current.2.destination == "12.23.45.70/32" + + - name: Delete all Data Routes + cisco.nd.nd_rest: + output_level: debug + method: delete + ignore_previous_state: true + path: "/api/config/routes/{{ item.destination }}" + loop: "{{ query_all_routes.current | list }}" + + - name: Delete imported backup file (check_mode) + cisco.nd.nd_backup_restore: + <<: *nd_info + state: absent + check_mode: true + register: cm_rm_imported_backup + + - name: Delete imported backup file + cisco.nd.nd_backup_restore: + <<: *nd_info + state: absent + check_mode: true + register: rm_imported_backup + + - name: Query all backups + cisco.nd.nd_backup: + output_level: debug + state: query + register: query_all_backups + + - name: Delete all backups + cisco.nd.nd_backup: + output_level: debug + name: "{{ item.name }}" + state: absent + loop: "{{ query_all_backups.current | list }}" + delay: 5 + +- name: Execute tasks only for ND version < 3.2.1 + when: version.current.platformVersion is version('3.2.1', '<') + block: + - name: Create a route to have some config in the route cluster configuration + cisco.nd.nd_cluster_config_route: + output_level: debug + destination_ip: 12.23.45.68/32 + target_network: data + state: present + + - name: Create a backup nexus + cisco.nd.nd_backup: + output_level: debug + name: nexus + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: backup + + - name: Query all routes + cisco.nd.nd_cluster_config_route: + output_level: debug + state: query + register: query_all_routes + + - name: Delete all routes to change the route configuration + cisco.nd.nd_cluster_config_route: + output_level: debug + destination_ip: "{{ item }}" + state: absent + loop: "{{ query_all_routes.current | map(attribute='spec.destination') | list }}" + + - name: Import the backup file nexus with restore job's name called nexus in check mode + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: restore + check_mode: true + register: restore_nexus_cm + + - name: Import the backup file nexus with restore job's name called nexus + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: restore + register: restore_nexus + + - name: Query all routes again + cisco.nd.nd_cluster_config_route: + output_level: debug + state: query + register: query_all_routes_again + + - name: Delete all routes again to add another restore job + cisco.nd.nd_cluster_config_route: + output_level: debug + destination_ip: "{{ item }}" + state: absent + loop: "{{ query_all_routes.current | map(attribute='spec.destination') | list }}" + + - name: Import the backup file nexus again with a different restore name nexus2 + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus2 + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: restore + register: restore_nexus2 + + - name: Query all routes after second import + cisco.nd.nd_cluster_config_route: + output_level: debug + state: query + register: query_all_routes_after_second_import + + - name: Delete all routes again to add a third restore job + cisco.nd.nd_cluster_config_route: + output_level: debug + destination_ip: "{{ item }}" + state: absent + loop: "{{ query_all_routes.current | map(attribute='spec.destination') | list }}" + + - name: Import the backup file with the name nexus to have multiple restore jobs with the same name + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: restore + register: restore_nexus_again + + - name: Query all routes after third import + cisco.nd.nd_cluster_config_route: + output_level: debug + state: query + register: query_all_routes_after_third_import + + - name: Query a restore job + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus2 + state: query + register: query_nexus2 + + - name: Query all restore jobs + cisco.nd.nd_backup_restore: + output_level: debug + state: query + register: query_all + + - name: Delete a restore job in check mode + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus2 + state: absent + check_mode: true + register: delete_nexus2_cm + + - name: Delete a restore job + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus2 + state: absent + register: delete_nexus2 + + - name: Delete a restore job again + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus2 + state: absent + register: delete_nexus2_again + + # Failure Scenarios + - name: Try querying restore nexus + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + state: query + ignore_errors: true + register: query_duplicate_nexus + + - name: Try deleting restore nexus + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + state: absent + ignore_errors: true + register: delete_duplicate_nexus + + - name: Delete restore with the wrong restore key + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + restore_key: 0 + state: absent + ignore_errors: true + register: delete_wrong_key_nexus + + - name: Import backup with password having a length < 8 characters + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + encryption_key: test + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: restore + ignore_errors: true + register: import_nexus_incorrect_pwd_length + + - name: Import backup to ND which already has the routes configured + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + encryption_key: testtest + file_location: "{{ role_path }}/backups/nexus_{{ ansible_host }}.tgz" + state: restore + ignore_errors: true + register: import_nexus_existing + + - name: Gather and sort the list of specs + ansible.builtin.set_fact: + query_all_routes_original: "{{ query_all_routes.current | map(attribute='spec') | list | sort(attribute='destination') }}" + query_all_routes_restored1: "{{ query_all_routes_again.current | map(attribute='spec') | list | sort(attribute='destination') }}" + query_all_routes_restored2: "{{ query_all_routes_after_second_import.current | map(attribute='spec') | list | sort(attribute='destination') }}" + query_all_routes_restored3: "{{ query_all_routes_after_third_import.current | map(attribute='spec') | list | sort(attribute='destination') }}" + + - name: Verify all assertions + ansible.builtin.assert: + that: + - restore_nexus_cm.changed is true + - restore_nexus.changed is true + - restore_nexus2.changed is true + - restore_nexus_again.changed is true + - query_all_routes_original == query_all_routes_restored1 == query_all_routes_restored2 == query_all_routes_restored3 + - query_nexus2.changed is false + - query_nexus2.current.description == "nexus2" + - query_all.current | length >= 3 + - delete_nexus2.changed is true + - delete_nexus2_cm.changed is true + - delete_nexus2_again.changed is false + - delete_nexus2.current == delete_nexus2_cm.current == delete_nexus2_again.current == {} + - import_nexus_incorrect_pwd_length.msg == "The encryption key must have a minium of 8 characters." + - query_duplicate_nexus.msg == delete_duplicate_nexus.msg == "Multiple restore jobs with the name 'nexus' found. Please provide a restore key for the corresponding restored job." + - delete_wrong_key_nexus.msg == "Provided key for the restore 'nexus' not found. Please provide a valid restore key by querying all the restored jobs and looking up the desired restore key." + + - name: Query all restored jobs for deleting them + cisco.nd.nd_backup_restore: + output_level: debug + state: query + register: restores + + - name: Delete the other restored jobs to clean up the environment + cisco.nd.nd_backup_restore: + output_level: debug + name: nexus + restore_key: "{{ item }}" + state: absent + loop: "{{ restores.current | map(attribute='key') | list }}" + + - name: Query all backups for deleting them + cisco.nd.nd_backup: + output_level: debug + state: query + register: backups + + - name: Delete all backup jobs to clean up environment + cisco.nd.nd_backup: + output_level: debug + name: nexus + backup_key: "{{ item }}" + state: absent + loop: "{{ backups.current | map(attribute='key') | list }}" + + - name: Delete the route created in the beginning of this test + cisco.nd.nd_cluster_config_route: + output_level: debug + destination_ip: 12.23.45.68/32 + state: absent diff --git a/tests/integration/targets/nd_version/tasks/main.yml b/tests/integration/targets/nd_version/tasks/main.yml index 6e4b39d4..a04f0aa8 100644 --- a/tests/integration/targets/nd_version/tasks/main.yml +++ b/tests/integration/targets/nd_version/tasks/main.yml @@ -14,38 +14,41 @@ - name: Verify cm_query_version ansible.builtin.assert: that: - - cm_query_version is not changed - - cm_query_version.current.major is defined - - cm_query_version.current.minor is defined - - cm_query_version.current.maintenance is defined - - cm_query_version.current.patch is defined - - cm_query_version.current.commit_id is defined - - cm_query_version.current.product_name == "Nexus Dashboard" + - cm_query_version is not changed + - cm_query_version.current.major is defined + - cm_query_version.current.minor is defined + - cm_query_version.current.maintenance is defined + - cm_query_version.current.patch is defined + - cm_query_version.current.commit_id is defined + - cm_query_version.current.product_name == "Nexus Dashboard" + - cm_query_version.current.platformVersion is defined - name: Query ND version (normal mode) cisco.nd.nd_version: <<: *nd_query register: nm_query_version # environment: - # http_proxy: http://localhost:8080 - # https_proxy: http://localhost:8080 + # http_proxy: http://localhost:8080 + # https_proxy: http://localhost:8080 - name: Verify nm_query_version ansible.builtin.assert: that: - - nm_query_version is not changed - - nm_query_version.current.major is defined - - nm_query_version.current.minor is defined - - nm_query_version.current.maintenance is defined - - nm_query_version.current.patch is defined - - nm_query_version.current.commit_id is defined - - nm_query_version.current.product_name == "Nexus Dashboard" - - nm_query_version.current.major == cm_query_version.current.major - - nm_query_version.current.minor == cm_query_version.current.minor - - nm_query_version.current.maintenance == cm_query_version.current.maintenance - - nm_query_version.current.patch == cm_query_version.current.patch - - nm_query_version.current.commit_id == cm_query_version.current.commit_id - - nm_query_version.current.product_name == cm_query_version.current.product_name + - nm_query_version is not changed + - nm_query_version.current.major is defined + - nm_query_version.current.minor is defined + - nm_query_version.current.maintenance is defined + - nm_query_version.current.patch is defined + - nm_query_version.current.commit_id is defined + - nm_query_version.current.platformVersion is defined + - nm_query_version.current.product_name == "Nexus Dashboard" + - nm_query_version.current.major == cm_query_version.current.major + - nm_query_version.current.minor == cm_query_version.current.minor + - nm_query_version.current.maintenance == cm_query_version.current.maintenance + - nm_query_version.current.patch == cm_query_version.current.patch + - nm_query_version.current.commit_id == cm_query_version.current.commit_id + - nm_query_version.current.product_name == cm_query_version.current.product_name + - nm_query_version.current.platformVersion == cm_query_version.current.platformVersion # USE A NON-EXISTING STATE - name: Non-existing state for version (check_mode) @@ -66,10 +69,10 @@ - name: Verify non_existing_state ansible.builtin.assert: that: - - cm_non_existing_state is not changed - - nm_non_existing_state is not changed - - cm_non_existing_state == nm_non_existing_state - - cm_non_existing_state.msg == nm_non_existing_state.msg == "value of state must be one of{{':'}} query, got{{':'}} non-existing-state" + - cm_non_existing_state is not changed + - nm_non_existing_state is not changed + - cm_non_existing_state == nm_non_existing_state + - cm_non_existing_state.msg == nm_non_existing_state.msg == "value of state must be one of{{':'}} query, got{{':'}} non-existing-state" - name: Query ND version by global parameters (check_mode) cisco.nd.nd_version: @@ -91,6 +94,7 @@ - nm_query_global_params.current.maintenance is defined - nm_query_global_params.current.patch is defined - nm_query_global_params.current.commit_id is defined + - nm_query_global_params.current.platformVersion is defined - nm_query_global_params.current.product_name == "Nexus Dashboard" - nm_query_global_params.current.major == cm_query_global_params.current.major - nm_query_global_params.current.minor == cm_query_global_params.current.minor @@ -98,3 +102,4 @@ - nm_query_global_params.current.patch == cm_query_global_params.current.patch - nm_query_global_params.current.commit_id == cm_query_global_params.current.commit_id - nm_query_global_params.current.product_name == cm_query_global_params.current.product_name + - nm_query_global_params.current.platformVersion == cm_query_global_params.current.platformVersion