diff --git a/plugins/httpapi/dcnm.py b/plugins/httpapi/dcnm.py index 5cde03360..f4b361977 100644 --- a/plugins/httpapi/dcnm.py +++ b/plugins/httpapi/dcnm.py @@ -62,6 +62,12 @@ def get_version(self): def set_version(self, version): self.version = version + def set_token(self, token): + self.token = token + + def get_token(self): + return self.token + def _login_old(self, username, password, method, path): """DCNM Helper Function to login to DCNM version 11.""" # Ansible expresses the persistent_connect_timeout in seconds. @@ -88,6 +94,7 @@ def _login_old(self, username, password, method, path): } self.login_succeeded = True self.set_version(11) + self.set_token(self.connection._auth) except Exception as e: self.login_fail_msg.append( @@ -124,6 +131,7 @@ def _login_latestv1(self, username, password, login_domain, method, path): } self.login_succeeded = True self.set_version(12) + self.set_token(self.connection._auth) except Exception as e: self.login_fail_msg.append( @@ -160,6 +168,7 @@ def _login_latestv2(self, username, password, login_domain, method, path): } self.login_succeeded = True self.set_version(12) + self.set_token(self.connection._auth) except Exception as e: self.login_fail_msg.append( @@ -275,6 +284,9 @@ def check_url_connection(self): ) raise ConnectionError(str(e) + msg) + def get_url_connection(self): + return self.connection._url + def send_request(self, method, path, json=None): """This method handles all DCNM REST API requests other then login""" diff --git a/plugins/module_utils/network/dcnm/dcnm.py b/plugins/module_utils/network/dcnm/dcnm.py index 458199a69..7ffd74bae 100644 --- a/plugins/module_utils/network/dcnm/dcnm.py +++ b/plugins/module_utils/network/dcnm/dcnm.py @@ -23,6 +23,13 @@ from ansible.module_utils.common import validation from ansible.module_utils.connection import Connection +# Any third party module must be imported as shown. If not ansible sanity tests will fail +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + def validate_ip_address_format(type, item, invalid_params): @@ -386,7 +393,7 @@ def dcnm_reset_connection(module): conn = Connection(module._socket_path) - conn.logout() + # conn.logout() return conn.login( conn.get_option("remote_user"), conn.get_option("password") ) @@ -429,7 +436,7 @@ def dcnm_version_supported(module): supported = int(mo.group(1)) if supported is None: - msg = "Unable to determine the DCNM/NDFC Software Version" + msg = "Unable to determine the DCNM/NDFC Software Version, " + "RESP = " + str(response) module.fail_json(msg=msg) return supported @@ -517,3 +524,32 @@ def dcnm_get_url(module, fabric, path, items, module_name): iter += 1 return attach_objects + + +def dcnm_get_protocol_and_address(module): + + conn = Connection(module._socket_path) + + url_prefix = conn.get_url_connection() + split_url = url_prefix.split(":") + + return [split_url[0], split_url[1]] + + +def dcnm_get_auth_token(module): + + conn = Connection(module._socket_path) + return (conn.get_token()) + + +def dcnm_post_request(path, hdrs, verify_flag, upload_files): + + resp = requests.post(path, headers=hdrs, verify=verify_flag, files=upload_files) + json_resp = resp.json() + if json_resp: + json_resp["RETURN_CODE"] = resp.status_code + json_resp["DATA"] = json_resp["message"] + json_resp["METHOD"] = "POST" + json_resp["REQUEST_PATH"] = path + json_resp.pop("message") + return json_resp diff --git a/plugins/modules/dcnm_image_upload.py b/plugins/modules/dcnm_image_upload.py new file mode 100644 index 000000000..2d5cb93c7 --- /dev/null +++ b/plugins/modules/dcnm_image_upload.py @@ -0,0 +1,777 @@ +#!/usr/bin/python +# +# Copyright (c) 2020-2022 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Mallik Mudigonda" + +DOCUMENTATION = """ +--- +module: dcnm_image_upload +short_description: DCNM Ansible Module for managing images. +version_added: "2.1.0" +description: + - "DCNM Ansible Module for the following image management operations" + - "Upload, Delete, and Display NXOS images from the controller" + +author: Mallik Mudigonda(@mmudigon) +options: + state: + description: + - The required state of the configuration after module completion. + type: str + choices: ['merged', 'overridden', 'deleted', 'query'] + default: merged + files: + description: + - A dictionary of images and other related information that is required to download the same. + type: list + elements: dict + suboptions: + path: + description: + - Full path to the image that is being uploaded to the controller. For deleting an image + - the exact image name must be provided. + type: str + required: true + source: + description: + - Protocol to be used to download the image from the controller. + type: str + choices: ['scp', 'sftp', 'local'] + default: local + remote_server: + description: + - IP address of the server hosting the image. This parameter is required only if source is 'scp' + - or 'sftp'. + type: str + required: true + user_name: + description: + - User name to be used to log into the image hosting server. This parameter is required only if source is 'scp' + - or 'sftp'. + type: str + required: true + password: + description: + - Password to be used to log into the image hosting server. This parameter is required only if source is 'scp' + - or 'sftp'. + type: str + required: true +""" + +EXAMPLES = """ + +# States: +# This module supports the following states: +# +# Merged: +# Images defined in the playbook will be merged into the controller. +# +# The images listed in the playbook will be created if not already present on the server +# server. If the image is already present and the configuration information included +# in the playbook is either different or not present in server, then the corresponding +# information is added to the server. If an image mentioned in playbook +# is already present on the server and there is no difference in configuration, no operation +# will be performed for such interface. +# +# Overridden: +# Images defined in the playbook will be overridden in the controller. +# +# The state of the images listed in the playbook will serve as source of truth for all +# the images on the controller. Additions and deletions will be done to bring +# the images on the controller to the state listed in the playbook. All images other than the +# ones mentioned in the playbook will be deleted. +# Note: Override will work on the all the images present in the controller. +# +# Deleted: +# Images defined in the playbook will be deleted from the controller. +# +# Deletes the list of images specified in the playbook. If the playbook does not include +# any image information, then all images from the controller will be deleted. +# +# Query: +# Returns the current state for the images listed in the playbook. + +# UPLOAD IMAGES + +- name: Upload images to controller + cisco.dcnm.dcnm_image_upload: &img_upload + state: merged # choose form [merged, deleted, overridden, query], default is merged + files: + - path: "full/path/to/image1" # Full path to the image on the server + source: scp # choose from [local, scp, sftp], default is local + remote_server: "192.168.1.1" # mandatory when the source is scp or sftp + username: "image_upload" # mandatory when source is scp or sftp + password: "image_upload" # mandatory when source is scp or sftp + + - path: "full/path/to/image2" # Full path to image on local host + source: local # choose from [local, scp, sftp], default is local + + - path: "full/path/to/image3" # Full path to the image on the server + source: sftp # choose from [local, scp, sftp], default is local + remote_server: "192.168.1.1" # mandatory when the source is scp or sftp + username: "image_upload" # mandatory when source is scp or sftp + password: "image_upload" # mandatory when source is scp or sftp + +# DELETE IMAGES + +- name: Delete an image + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + files: + - name: "nxos.9.3.8.bin" # Name of the image on the controller + +- name: Delete an image - without explicitly including any config + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + +# OVERRIDE IMAGES + +- name: Override without any config + cisco.dcnm.dcnm_image_upload: + state: overridden # choose form [merged, deleted, overridden, query], default is merged + +- name: Override with a new config + cisco.dcnm.dcnm_image_upload: &image_override + state: overridden # choose form [merged, deleted, overridden, query], default is merged + files: + - path: "full/path/to/image4" # Full path to the image on local server + source: local # choose from [local, scp, sftp], default is local + +# QUERY IMAGES + +- name: Query for existing image + cisco.dcnm.dcnm_image_upload: + state: query # choose form [merged, deleted, overridden, query], default is merged + files: + - name: "nxos.9.3.8.bin" # Name of the image to be used to filter the output + +- name: Query without any filters + cisco.dcnm.dcnm_image_upload: + state: query # choose form [merged, deleted, overridden, query], default is merged +""" + +# +# WARNING: +# This file is automatically generated. Take a backup of your changes to this file before +# manually running cg_run.py script to generate it again +# + +import os +import json +import copy + +from ansible.module_utils.connection import Connection +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, + validate_list_of_dicts, + dcnm_reset_connection, + dcnm_version_supported, + dcnm_get_protocol_and_address, + dcnm_get_auth_token, + dcnm_post_request, +) + + +# Resource Class object which includes all the required methods and data to configure and maintain Image_upload +class DcnmImageUpload: + dcnm_image_upload_paths = { + 11: { + "DCNM_CREATE_IMAGE_LOCAL": "/imageupload/smart-image-upload", + "DCNM_CREATE_IMAGE_SCP": "/rest/imageupload/scp-upload", + "DCNM_CREATE_IMAGE_SFTP": "/rest/imageupload/sftp-upload", + "DCNM_DELETE_IMAGE": "/rest/imageupload/smart-image", + "DCNM_GET_IMAGE_LIST": "/rest/imageupload/uploaded-images-table", + }, + 12: { + "DCNM_CREATE_IMAGE_LOCAL": "/appcenter/cisco/ndfc/api/v1/imagemanagement/imageupload/smart-image-upload", + "DCNM_CREATE_IMAGE_SCP": "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/scp-upload", + "DCNM_CREATE_IMAGE_SFTP": "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/sftp-upload", + "DCNM_DELETE_IMAGE": "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/smart-image", + "DCNM_GET_IMAGE_LIST": "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + }, + } + + def __init__(self, module): + self.module = module + self.params = module.params + self.files = copy.deepcopy(module.params.get("files", [])) + self.image_upload_info = [] + self.want = [] + self.have = [] + self.diff_create = [] + self.diff_delete = [] + self.fd = None + self.changed_dict = [ + {"merged": [], "deleted": [], "query": [], "debugs": []} + ] + + self.dcnm_version = dcnm_version_supported(self.module) + + self.paths = self.dcnm_image_upload_paths[self.dcnm_version] + self.result = dict(changed=False, diff=[], response=[]) + + def log_msg(self, msg): + + if self.fd is None: + self.fd = open("dcnm_image_upload.log", "a+") + if self.fd is not None: + self.fd.write(msg) + self.fd.write("\n") + self.fd.flush() + + def dcnm_image_upload_get_info_from_dcnm(self): + + """ + Routine to get existing information from DCNM which matches the given object. + + Parameters: + None + + Returns: + resp["DATA"] (dict): image_upload informatikon obtained from the DCNM server if it exists + [] otherwise + """ + + path = self.paths["DCNM_GET_IMAGE_LIST"] + + resp = dcnm_send(self.module, "GET", path) + + if ( + resp + and (resp["RETURN_CODE"] == 200) + and resp["MESSAGE"] == "OK" + and resp["DATA"] + and resp["DATA"]["lastOperDataObject"] + ): + return resp["DATA"]["lastOperDataObject"] + else: + return [] + + def dcnm_image_upload_get_diff_deleted(self): + + """ + Routine to get a list of payload information that will be used to delete Image_upload. + This routine updates self.diff_delete with payloads that are used to delete Image_upload + from the server. + + Parameters: + None + + Returns: + None + """ + + del_payload = {"deleteTasksList": []} + + dcnm_image_list = self.dcnm_image_upload_get_info_from_dcnm() + + if self.image_upload_info == []: + # No image names included. Delete all images + for img in dcnm_image_list: + delem = { + "platform": img["platform"], + "version": img["version"], + "imageType": img["imageType"], + "imagename": img["imageName"], + "osType": img["osType"], + } + del_payload["deleteTasksList"].append(delem) + self.changed_dict[0]["deleted"].append(delem) + + for elem in self.image_upload_info: + match_elem = [ + img + for img in dcnm_image_list + if ((os.path.basename(elem["name"]) == img["imageName"])) + ] + + for melem in match_elem: + delem = { + "platform": melem["platform"], + "version": melem["version"], + "imageType": melem["imageType"], + "imagename": melem["imageName"], + "osType": melem["osType"], + } + del_payload["deleteTasksList"].append(delem) + self.changed_dict[0]["deleted"].append(delem) + + if del_payload["deleteTasksList"] != []: + self.diff_delete.append(del_payload) + + def dcnm_image_upload_compare_want_and_have(self, want): + + # Check if the image is already present. If present, do not try to upload again + match_have = [ + elem + for elem in self.have + if os.path.basename(want["filePath"]) == elem["imageName"] + ] + + if match_have: + # Have found a matching image on the controller. No need to create again + return "DCNM_IMAGE_UPLOAD_EXIST", [], match_have[0] + else: + return "DCNM_IMAGE_UPLOAD_CREATE", [], [] + + def dcnm_image_upload_get_diff_merge(self): + + """ + Routine to populate a list of payload information in self.diff_create to create/update Image_upload. + + Parameters: + None + + Returns: + None + """ + + if not self.want: + return + + for elem in self.want: + + rc, reasons, have = self.dcnm_image_upload_compare_want_and_have( + elem + ) + + if rc == "DCNM_IMAGE_UPLOAD_CREATE": + # Object does not exists, create a new one. + if elem not in self.diff_create: + self.changed_dict[0]["merged"].append(elem) + self.diff_create.append(elem) + + def dcnm_image_upload_get_diff_overridden(self, files): + + # Get all the images that are already present. + dcnm_image_list = self.dcnm_image_upload_get_info_from_dcnm() + + del_payload = {"deleteTasksList": []} + + if files: + # User has included some imahe names in the playbook. Check if the file is already present. If + # yes then do not try to create it again. Also remove this file from the list of files to be deleted. + for elem in self.want: + match_elem = [ + img + for img in dcnm_image_list + if ( + ( + os.path.basename(elem["filePath"]) + == img["imageName"] + ) + ) + ] + + if match_elem == []: + # The image that user is trying to create is not present in the image list. + # Add the image from the playbook to create list so that it is created. + self.diff_create.append(elem) + self.changed_dict[0]["merged"].append(elem) + else: + # There is a match. User is trying to create an image that is already existing. So remove the matching + # image from the image list so that is is not deleted in the following block. + dcnm_image_list.remove(match_elem[0]) + + # Delete all files from image list + for img in dcnm_image_list: + delem = { + "platform": img["platform"], + "version": img["version"], + "imageType": img["imageType"], + "imagename": img["imageName"], + "osType": img["osType"], + } + del_payload["deleteTasksList"].append(delem) + self.changed_dict[0]["deleted"].append(delem) + self.diff_delete.append(del_payload) + + def dcnm_image_upload_get_diff_query(self): + + dcnm_image_list = self.dcnm_image_upload_get_info_from_dcnm() + + if self.image_upload_info == []: + # No filters specified. Add all images to the output + self.result["response"].extend(dcnm_image_list) + + for elem in self.image_upload_info: + # Image names a re provided as filters. Filter the output as required + match_elem = [ + img + for img in dcnm_image_list + if ((os.path.basename(elem["name"]) == img["imageName"])) + ] + if match_elem: + self.result["response"].append(match_elem[0]) + + def dcnm_image_upload_get_want(self): + + """ + This routine updates self.want with the payload information based on the playbook configuration. + + Parameters: + None + + Returns: + None + """ + + if [] is self.files: + return + + if not self.image_upload_info: + return + + for elem in self.image_upload_info: + + payload = self.dcnm_image_upload_get_payload(elem) + if payload not in self.want: + self.want.append(payload) + + def dcnm_image_upload_get_have(self): + + """ + Routine to get exisitng image_upload information from DCNM that matches information in self.want. + This routine updates self.have with all the image_upload that match the given playbook configuration + + Parameters: + None + + Returns: + None + """ + + if self.want == []: + return + + dcnm_image_list = self.dcnm_image_upload_get_info_from_dcnm() + + # Compare the images from want and dcnm_image_list. Keep only those that match + for want in self.want: + match_have = [ + elem + for elem in dcnm_image_list + if os.path.basename(want["filePath"]) == elem["imageName"] + ] + + if match_have and match_have[0]["imageName"] not in self.have: + self.have.append(match_have[0]) + + def dcnm_image_upload_validate_query_state_input(self, cfg): + + """ + Playbook input will be different for differnt states. This routine validates the query state + input. This routine updates self.image_upload_info with validated playbook information related + to query state. + + Parameters: + cfg (dict): The config from playbook + + Returns: + None + """ + + arg_spec = dict(name=dict(required=True, type="str")) + + image_upload_info, invalid_params = validate_list_of_dicts( + cfg, arg_spec + ) + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.module.fail_json(msg=mesg) + + if image_upload_info: + self.image_upload_info.extend(image_upload_info) + + def dcnm_image_upload_validate_input(self, cfg): + + ldicts = [] + arg_spec = dict( + path=dict(required=True, type="str"), + source=dict(type="str", default="local"), + ) + + source = cfg[0].get("source", None) + + if source != "local" and source is not None: + arg_spec["remote_server"] = dict(required=True, type="str") + arg_spec["username"] = dict(required=True, type="str") + arg_spec["password"] = dict(required=True, type="str") + + for elem in arg_spec: + image_upload_info, invalid_params = validate_list_of_dicts( + cfg, arg_spec + ) + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format( + invalid_params + ) + self.module.fail_json(msg=mesg) + + if image_upload_info: + self.image_upload_info.extend(image_upload_info) + + def dcnm_image_upload_validate_deleted_state_input(self, cfg): + + arg_spec = dict(name=dict(required=True, type="str")) + + for elem in arg_spec: + image_upload_info, invalid_params = validate_list_of_dicts( + cfg, arg_spec + ) + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format( + invalid_params + ) + self.module.fail_json(msg=mesg) + + if image_upload_info: + self.image_upload_info.extend(image_upload_info) + + def dcnm_image_upload_validate_all_input(self): + + """ + Routine to validate playbook input based on the state. Since each state has a different + config structure, this routine handles the validation based on the given state + + Parameters: + None + + Returns: + None + """ + + if [] is self.files: + return + + cfg = [] + for item in self.files: + + citem = copy.deepcopy(item) + + cfg.append(citem) + + if self.module.params["state"] == "query": + # config for query state is different. So validate query state differently + self.dcnm_image_upload_validate_query_state_input(cfg) + elif self.module.params["state"] == "deleted": + # config for deleted state is different. So validate deleted state differently + self.dcnm_image_upload_validate_deleted_state_input(cfg) + else: + self.dcnm_image_upload_validate_input(cfg) + cfg.remove(citem) + + def dcnm_image_upload_get_payload(self, image_upload_info): + + """ + This routine builds the complete object payload based on the information in self.want + + Parameters: + image_upload_info (dict): Object information + + Returns: + image_upload_payload (dict): Object payload information populated with appropriate data from playbook config + """ + + if image_upload_info["source"] != "local": + image_upload_payload = { + # Fill in the parameters that are required for creating/replacing image_upload object + "server": image_upload_info["remote_server"], + "filePath": image_upload_info["path"], + "userName": image_upload_info["username"], + "password": image_upload_info["password"], + "acceptHostKey": "false", + "source": image_upload_info["source"], + } + else: + image_upload_payload = { + # Fill in the parameters that are required for creating/replacing image_upload object + "filePath": image_upload_info["path"], + "source": image_upload_info["source"], + } + + return image_upload_payload + + def dcnm_image_upload_handle_local_file_transfer(self, elem): + + """ + Routine to read a local file specified in the playbook and transfer the file to DCNM controller + + Parameters: + elem (dict): A dict containing complete path to local file + + Returns: + True if file successfully transfered + False otherwise + """ + + protocol, address = dcnm_get_protocol_and_address(self.module) + + path = protocol + ":" + address + self.paths["DCNM_CREATE_IMAGE_LOCAL"] + + # We should use the authentication token already obtained to send data over this connection. + # So update the headers with appropriate token details + headers = {} + auth_token = dcnm_get_auth_token(self.module) + headers.update(auth_token) + + file_path = elem.get("filePath", "") + + if file_path: + upload_files = {"file": open(file_path, "rb")} + + resp = dcnm_post_request(path, headers, False, upload_files) + + self.result["response"].append(resp) + + if resp["DATA"] == "Successfully uploaded selected image file(s).": + resp["MESSAGE"] = "OK" + return True + else: + resp["MESSAGE"] = "" + return False + else: + return False + + def dcnm_image_upload_send_message_to_dcnm(self): + + """ + Routine to push payloads to DCNM server. This routine implements required error checks and retry mechanisms to handle + transient errors. This routine checks self.diff_create, self.diff_delete lists and push appropriate requests to DCNM. + + Parameters: + None + + Returns: + None + """ + + resp = None + create_flag = False + delete_flag = False + + for elem in self.diff_delete: + path = self.paths["DCNM_DELETE_IMAGE"] + json_payload = json.dumps(elem) + + resp = dcnm_send(self.module, "DELETE", path, json_payload) + + if resp != []: + self.result["response"].append(resp) + + if resp and resp.get("RETURN_CODE") != 200: + resp["CHANGED"] = self.changed_dict[0] + self.module.fail_json(msg=resp) + else: + delete_flag = True + + for elem in self.diff_create: + source = elem.pop("source") + + if source in ["scp", "sftp"]: + path = self.paths["DCNM_CREATE_IMAGE_" + source.upper()] + json_payload = json.dumps(elem) + resp = dcnm_send(self.module, "POST", path, json_payload) + + if resp != []: + self.result["response"].append(resp) + if resp and resp.get("RETURN_CODE") != 200: + resp["CHANGED"] = self.changed_dict[0] + self.module.fail_json(msg=resp) + else: + create_flag = True + else: + # Source is local and so the file is present on the local host. Read the file + # and tranfer the contents. + + create_flag = self.dcnm_image_upload_handle_local_file_transfer( + elem + ) + + self.result["changed"] = create_flag or delete_flag + + +def main(): + + """ main entry point for module execution + """ + element_spec = dict( + files=dict(required=False, type="list", elements="dict", default=[]), + state=dict( + type="str", + default="merged", + choices=["merged", "deleted", "overridden", "query"], + ), + ) + + module = AnsibleModule( + argument_spec=element_spec, supports_check_mode=True + ) + + dcnm_image_upload = DcnmImageUpload(module) + + state = module.params["state"] + + if [] is dcnm_image_upload.files: + if state == "merged": + module.fail_json( + msg="'files' element is mandatory for state '{0}', given = '{1}'".format( + state, dcnm_image_upload.config + ) + ) + dcnm_image_upload.dcnm_image_upload_validate_all_input() + + if ( + module.params["state"] != "query" + and module.params["state"] != "deleted" + ): + dcnm_image_upload.dcnm_image_upload_get_want() + dcnm_image_upload.dcnm_image_upload_get_have() + + if module.params["state"] == "merged": + dcnm_image_upload.dcnm_image_upload_get_diff_merge() + + if module.params["state"] == "deleted": + dcnm_image_upload.dcnm_image_upload_get_diff_deleted() + + if module.params["state"] == "overridden": + dcnm_image_upload.dcnm_image_upload_get_diff_overridden( + dcnm_image_upload.files + ) + + if module.params["state"] == "query": + dcnm_image_upload.dcnm_image_upload_get_diff_query() + + dcnm_image_upload.result["diff"] = dcnm_image_upload.changed_dict + + if dcnm_image_upload.diff_create or dcnm_image_upload.diff_delete: + dcnm_image_upload.result["changed"] = True + + if module.check_mode: + dcnm_image_upload.result["changed"] = False + module.exit_json(**dcnm_image_upload.result) + + dcnm_image_upload.dcnm_image_upload_send_message_to_dcnm() + + module.exit_json(**dcnm_image_upload.result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/dcnm_image_upload/defaults/main.yaml b/tests/integration/targets/dcnm_image_upload/defaults/main.yaml new file mode 100644 index 000000000..5f709c5aa --- /dev/null +++ b/tests/integration/targets/dcnm_image_upload/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/tests/integration/targets/dcnm_image_upload/meta/main.yaml b/tests/integration/targets/dcnm_image_upload/meta/main.yaml new file mode 100644 index 000000000..32cf5dda7 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upload/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/dcnm_image_upload/tasks/dcnm.yaml b/tests/integration/targets/dcnm_image_upload/tasks/dcnm.yaml new file mode 100644 index 000000000..881b81cb6 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upload/tasks/dcnm.yaml @@ -0,0 +1,20 @@ +--- +- name: collect dcnm test cases + find: + paths: "{{ role_path }}/tests/dcnm" + patterns: "{{ testcase }}.yaml" + connection: local + register: dcnm_cases + +- set_fact: + test_cases: + files: "{{ dcnm_cases.files }}" + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=httpapi) + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/dcnm_image_upload/tasks/main.yaml b/tests/integration/targets/dcnm_image_upload/tasks/main.yaml new file mode 100644 index 000000000..78c5fb834 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upload/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: dcnm.yaml, tags: ['dcnm'] } \ No newline at end of file diff --git a/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_delete.yaml b/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_delete.yaml new file mode 100644 index 000000000..e78f891b2 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_delete.yaml @@ -0,0 +1,253 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_image_upload.log + +- name: Delete all images from controller + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Upload images to controller + cisco.dcnm.dcnm_image_upload: + state: merged # choose form [merged, deleted, overridden, query], default is merged + files: + - path: "{{ IMAGE_3_PATH }}" # Full path to the image on local server + source: local # choose from [local, scp, sftp], default is local + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Successfully uploaded selected image file(s)." in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## DELETE ## +############################################## + + - name: Delete an image + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + files: + - name: "{{ IMAGE_3_NAME }}" # Name of the image on the controller + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Image(s) Deleted Successfully" in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## IDEMPOTENCE ## +############################################## + + - name: Delete Image - Idempotence + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + +############################################## +## MERGE ## +############################################## + + - name: Upload images to controller + cisco.dcnm.dcnm_image_upload: + files: + - path: "{{ IMAGE_3_PATH }}" # Name of the image on local server + source: local # choose from [local, scp, sftp], default is local + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Successfully uploaded selected image file(s)." in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## DELETE ## +############################################## + + - name: Delete an image - without explicitly including any config + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Image(s) Deleted Successfully" in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## DELETE ## +############################################## + + - name: Delete an image that does not exist + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + files: + - name: no-such-image.bin # Name of an image that does not exist on controller + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result.response | length) == 0' + +############################################## +## MERGE ## +############################################## + + - name: Upload images to controller + cisco.dcnm.dcnm_image_upload: + files: + - path: "{{ IMAGE_3_PATH }}" # Full path to the image on local server + source: local # choose from [local, scp, sftp], default is local + + - path: "{{ IMAGE_2_PATH }}" # Full path to the image on the server + source: sftp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 2' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Successfully uploaded selected image file(s)." in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## DELETE ## +############################################## + + - name: Delete one image from existing list + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + files: + - name: "{{ IMAGE_3_NAME }}" # Name of the image on the controller + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Image(s) Deleted Successfully" in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## QUERY ## +############################################## + + - name: Query and check if one image is still present + cisco.dcnm.dcnm_image_upload: + state: query # choose form [merged, deleted, overridden, query], default is merged + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result.response | length) == 1' + +############################################## +## DELETE ## +############################################## + + - name: Delete one existing image and one non-existing image + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + files: + - name: "{{ IMAGE_2_NAME }}" # Name of the image on the controller + - name: no-such-image.bin # Name of an image that does not exist on controller + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Image(s) Deleted Successfully" in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all images from controller + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_merge.yaml b/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_merge.yaml new file mode 100644 index 000000000..db30c00bd --- /dev/null +++ b/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_merge.yaml @@ -0,0 +1,134 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_image_upload.log + +- name: Delete all images from controller + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Upload images to controller + cisco.dcnm.dcnm_image_upload: &img_upload + state: merged # choose form [merged, deleted, overridden, query], default is merged + files: + - path: "{{ IMAGE_1_PATH }}" # Full path to the image on the server + source: scp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + + - path: "{{ IMAGE_3_PATH }}" # Full path to image on local host + source: local # choose from [local, scp, sftp], default is local + + - path: "{{ IMAGE_2_PATH }}" # Full path to the image on the server + source: sftp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 3' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Successfully uploaded selected image file(s)." in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## IDEMPOTENCE ## +############################################## + + - name: Image upload - Idempotence + cisco.dcnm.dcnm_image_upload: *img_upload + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + +############################################## +## DELETE ## +############################################## + + - name: Delete an image + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + files: + - name: "{{ IMAGE_3_NAME }}" # Name of the image on the controller + source: local # choose from [local, scp, sftp], default is local + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Image(s) Deleted Successfully" in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## MERGE ## +############################################## + + - name: Upload images to controller - without mentioning state + cisco.dcnm.dcnm_image_upload: + files: + - path: "{{ IMAGE_3_PATH }}" # Full path to image on the local host + source: local # choose from [local, scp, sftp], default is local + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Successfully uploaded selected image file(s)." in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all images from controller + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_override.yaml b/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_override.yaml new file mode 100644 index 000000000..8c17f730e --- /dev/null +++ b/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_override.yaml @@ -0,0 +1,199 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_image_upload.log + +- name: Delete all images from controller + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Upload images to controller + cisco.dcnm.dcnm_image_upload: + state: merged # choose form [merged, deleted, overridden, query], default is merged + files: + - path: "{{ IMAGE_2_PATH }}" # Full path to the image on the server + source: sftp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + + - path: "{{ IMAGE_3_PATH }}" # Full path to the image on local server + source: local # choose from [local, scp, sftp], default is local + + - path: "{{ IMAGE_1_PATH }}" # Full path to the image on the server + source: scp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 3' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Successfully uploaded selected image file(s)." in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## OVERRIDDEN ## +############################################## + + - name: Override without any config + cisco.dcnm.dcnm_image_upload: + state: overridden # choose form [merged, deleted, overridden, query], default is merged + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 3' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Image(s) Deleted Successfully" in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## OVERRIDDEN ## +############################################## + + - name: Override with a new config + cisco.dcnm.dcnm_image_upload: &image_override + state: overridden # choose form [merged, deleted, overridden, query], default is merged + files: + - path: "{{ IMAGE_3_PATH }}" # Full path to the image on local server + source: local # choose from [local, scp, sftp], default is local + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Successfully uploaded selected image file(s)." in item["DATA"]' + loop: '{{ result.response }}' + +############################################## +## IDEMPOTENCE ## +############################################## + + - name: Override - Idempotence + cisco.dcnm.dcnm_image_upload: *image_override + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + +############################################## +## OVERRIDDEN ## +############################################## + + - name: Override with new images + cisco.dcnm.dcnm_image_upload: + state: overridden # choose form [merged, deleted, overridden, query], default is merged + files: + - path: "{{ IMAGE_2_PATH }}" # Full path to the image on the server + source: sftp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + + - path: "{{ IMAGE_1_PATH }}" # Full path to the image on the server + source: scp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 2' + - '(result["diff"][0]["deleted"] | length) == 1' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + +############################################## +## OVERRIDDEN ## +############################################## + + - name: Override with new images including existing images + cisco.dcnm.dcnm_image_upload: + state: overridden # choose form [merged, deleted, overridden, query], default is merged + files: + - path: "{{ IMAGE_2_PATH }}" # Full path to the image on the server + source: sftp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + + - path: "{{ IMAGE_3_PATH }}" # Full path to the image on local server + source: local # choose from [local, scp, sftp], default is local + + - path: "{{ IMAGE_1_PATH }}" # Full path to the image on the server + source: scp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all images from controller + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_query.yaml b/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_query.yaml new file mode 100644 index 000000000..6e3066784 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upload/tests/dcnm/dcnm_image_upload_query.yaml @@ -0,0 +1,165 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_image_upload.log + +- name: Delete all images from controller + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Upload images to controller + cisco.dcnm.dcnm_image_upload: + state: merged + files: + - path: "{{ IMAGE_2_PATH }}" # Full path to the image on the server + source: sftp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + + - path: "{{ IMAGE_3_PATH }}" # Full path to the image on local server + source: local # choose from [local, scp, sftp], default is local + + - path: "{{ IMAGE_1_PATH }}" # Full path to the image on the server + source: scp # choose from [local, scp, sftp], default is local + remote_server: "{{ SERVER_IP }}" # mandatory when the source is scp or sftp + username: "{{ USERNAME }}" # mandatory when source is scp or sftp + password: "{{ PASSWORD }}" # mandatory when source is scp or sftp + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 3' + - '(result["diff"][0]["deleted"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - '"Successfully uploaded selected image file(s)." in item["DATA"]' + loop: '{{ result.response }}' + + +############################################## +## QUERY ## +############################################## + + - name: Query for existing image + cisco.dcnm.dcnm_image_upload: + state: query # choose form [merged, deleted, overridden, query], default is merged + files: + - name: "{{ IMAGE_3_NAME }}" # Name of the image to be used to filter the output + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result.response | length) == 1' + +############################################## +## QUERY ## +############################################## + + - name: Query without any filters + cisco.dcnm.dcnm_image_upload: + state: query # choose form [merged, deleted, overridden, query], default is merged + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result.response | length) == 3' + +############################################## +## QUERY ## +############################################## + + - name: Query with specific names + cisco.dcnm.dcnm_image_upload: + state: query # choose form [merged, deleted, overridden, query], default is merged + files: + - name: "{{ IMAGE_2_NAME }}" # Name of the image to be used to filter the output + - name: "{{ IMAGE_3_NAME }}" # Name of the image to be used to filter the output + - name: "{{ IMAGE_1_NAME }}" # Name of the image to be used to filter the output + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result.response | length) == 3' + +############################################## +## QUERY ## +############################################## + + - name: Query with non existing image name + cisco.dcnm.dcnm_image_upload: + state: query # choose form [merged, deleted, overridden, query], default is merged + files: + - name: no-such-image.bin # Name of a non-existing image + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result.response | length) == 0' + +############################################## +## QUERY ## +############################################## + + - name: Query with existing and non existing image name + cisco.dcnm.dcnm_image_upload: + state: query # choose form [merged, deleted, overridden, query], default is merged + files: + - name: no-such-image.bin # Name of a non-existing image + - name: "{{ IMAGE_1_NAME }}" # Name of the image to be used to filter the output + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result.response | length) == 1' + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all images from controller + cisco.dcnm.dcnm_image_upload: + state: deleted # choose form [merged, deleted, overridden, query], default is merged + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index 504210f69..9fc9dca9f 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -10,6 +10,7 @@ plugins/modules/dcnm_service_route_peering.py validate-modules:missing-gplv3-lic plugins/modules/dcnm_service_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_resource_manager.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.8!skip diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_image_upload_configs.json b/tests/unit/modules/dcnm/fixtures/dcnm_image_upload_configs.json new file mode 100644 index 000000000..be2ff50d4 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_image_upload_configs.json @@ -0,0 +1,254 @@ +{ + "image_upload_merge_without_source_config": [ + { + "path": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm" + }], + + "image_upload_merge_all_config": [ + { + "source": "scp", + "path": "/opt/file/nxos/nxos.9.3.10.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }, + { + "path": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "source": "local" + }, + { + "source": "sftp", + "path": "/opt/file/nxos/nxos.9.3.8.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_merge_9_3_10_config": [ + { + "source": "scp", + "path": "/opt/file/nxos/nxos.9.3.10.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_merge_9_3_8_config": [ + { + "source": "sftp", + "path": "/opt/file/nxos/nxos.9.3.8.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_merge_rpm_config": [ + { + "path": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "source": "local" + }], + + "image_upload_merge_9_3_8_and_rpm_config": [ + { + "path": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "source": "local" + }, + { + "source": "sftp", + "path": "/opt/file/nxos/nxos.9.3.8.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_merge_9_3_8_and_9_3_10_config": [ + { + "source": "scp", + "path": "/opt/file/nxos/nxos.9.3.10.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }, + { + "source": "sftp", + "path": "/opt/file/nxos/nxos.9.3.8.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_merge_9_3_10_and_rpm_config": [ + { + "source": "scp", + "path": "/opt/file/nxos/nxos.9.3.10.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }, + { + "path": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "source": "local" + }], + + "image_upload_delete_rpm_config": [ + { + "name": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm" + }], + + "image_upload_delete_9_3_10_config": [ + { + "name": "nxos.9.3.10.bin" + }], + + "image_upload_delete_9_3_8_config": [ + { + "name": "nxos.9.3.8.bin" + }], + + "image_upload_delete_all_config": [ + { + "name": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm" + }, + { + "name": "nxos.9.3.10.bin" + }, + { + "name": "nxos.9.3.8.bin" + }], + + "image_upload_delete_9_3_10_and_rpm_config": [ + { + "name": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm" + }, + { + "name": "nxos.9.3.10.bin" + }], + + "image_upload_delete_9_3_8_and_rpm_config": [ + { + "name": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm" + }, + { + "name": "nxos.9.3.8.bin" + }], + + "image_upload_delete_9_3_8_and_9_3_10_config": [ + { + "name": "nxos.9.3.10.bin" + }, + { + "name": "nxos.9.3.8.bin" + }], + + "image_upload_delete_non_exist_config": [ + { + "name": "no-such-image.bin" + }], + + "image_upload_override_all_new_config": [ + { + "source": "scp", + "path": "/opt/file/nxos/nxos.9.3.10.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }, + { + "path": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "source": "local" + }, + { + "source": "sftp", + "path": "/opt/file/nxos/nxos.9.3.8.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_override_9_3_10_config": [ + { + "source": "scp", + "path": "/opt/file/nxos/nxos.9.3.10.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_override_9_3_8_config": [ + { + "source": "sftp", + "path": "/opt/file/nxos/nxos.9.3.8.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_override_rpm_config": [ + { + "path": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "source": "local" + }], + + "image_upload_override_9_3_8_and_9_3_10_config": [ + { + "source": "scp", + "path": "/opt/file/nxos/nxos.9.3.10.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }, + { + "source": "sftp", + "path": "/opt/file/nxos/nxos.9.3.8.bin", + "password": "ins3965!", + "remote_server": "10.195.225.200", + "username": "shdu" + }], + + "image_upload_query_all_config": [ + { + "name": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm" + }, + { + "name": "nxos.9.3.10.bin" + }, + { + "name": "nxos.9.3.8.bin" + }], + + "image_upload_query_rpm_config": [ + { + "name": "/Users/mmudigon/NXOS-Images/nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm" + }], + + "image_upload_query_9_3_8_config": [ + { + "name": "nxos.9.3.8.bin" + }], + + "image_upload_query_9_3_10_config": [ + { + "name": "nxos.9.3.8.bin" + }], + + "image_upload_query_9_3_8_and_9_3_10_config": [ + { + "name": "nxos.9.3.10.bin" + }, + { + "name": "nxos.9.3.8.bin" + }], + + "image_upload_query_exist_and_non_exist_config": [ + { + "name": "nxos.9.3.8.bin" + }, + { + "name": "no-such-image.bin" + }], + + "image_upload_query_non_exist_config": [ + { + "name": "no-such-image.bin" + }] +} diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_image_upload_payloads.json b/tests/unit/modules/dcnm/fixtures/dcnm_image_upload_payloads.json new file mode 100644 index 000000000..847825c6a --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_image_upload_payloads.json @@ -0,0 +1,287 @@ +{ + "create_response" : + { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/sftp-upload", + "MESSAGE": "OK", + "DATA": "Successfully uploaded selected image file(s)." + }, + + "delete_response": + { + "RETURN_CODE": 200, + "METHOD": "DELETE", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/smart-image", + "MESSAGE": "OK", + "DATA": "Image(s) Deleted Successfully" + }, + + "image_list_no_images_response": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + "MESSAGE": "OK", + "DATA": + { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + + "image_list_all_images_response": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + "MESSAGE": "OK", + "DATA": + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "platform": "N9K/N3K", + "name": "nxos.9.3.10.bin", + "fileVersion": "9.3.10", + "version": "9.3.10", + "imageName": "nxos.9.3.10.bin", + "imageType": "image", + "filetype": "nxos", + "size": "1966000640", + "checksum": "2dcae3f31446213f9a610240796a7826", + "osType": "32bit", + "refCount": 0 + }, + { + "platform": "N9K/N3K", + "name": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "fileVersion": "9.3.6", + "version": "1.0.0", + "imageName": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "imageType": "rpm", + "filetype": "Patch", + "size": "19061", + "checksum": "678aca6d523a81f38e9ed070ffcee2c9", + "osType": "32bit", + "refCount": 0 + }, + { + "platform": "N9K/N3K", + "name": "nxos.9.3.8.bin", + "fileVersion": "9.3.8", + "version": "9.3.8", + "imageName": "nxos.9.3.8.bin", + "imageType": "image", + "filetype": "nxos", + "size": "1956867584", + "checksum": "cd21df22e7c4df02e0d5178c0e6f6d48", + "osType": "32bit", + "refCount": 0 + }], + "message": "" + } + }, + + "image_list_9_3_10_image_response": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + "MESSAGE": "OK", + "DATA": + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "platform": "N9K/N3K", + "name": "nxos.9.3.10.bin", + "fileVersion": "9.3.10", + "version": "9.3.10", + "imageName": "nxos.9.3.10.bin", + "imageType": "image", + "filetype": "nxos", + "size": "1966000640", + "checksum": "2dcae3f31446213f9a610240796a7826", + "osType": "32bit", + "refCount": 0 + }], + "message": "" + } + }, + + "image_list_rpm_image_response": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + "MESSAGE": "OK", + "DATA": + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "platform": "N9K/N3K", + "name": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "fileVersion": "9.3.6", + "version": "1.0.0", + "imageName": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "imageType": "rpm", + "filetype": "Patch", + "size": "19061", + "checksum": "678aca6d523a81f38e9ed070ffcee2c9", + "osType": "32bit", + "refCount": 0 + }], + "message": "" + } + }, + + "image_list_9_3_8_image_response": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + "MESSAGE": "OK", + "DATA": + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "platform": "N9K/N3K", + "name": "nxos.9.3.8.bin", + "fileVersion": "9.3.8", + "version": "9.3.8", + "imageName": "nxos.9.3.8.bin", + "imageType": "image", + "filetype": "nxos", + "size": "1956867584", + "checksum": "cd21df22e7c4df02e0d5178c0e6f6d48", + "osType": "32bit", + "refCount": 0 + }], + "message": "" + } + }, + + "image_list_9_3_8_and_9_3_10_image_response": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + "MESSAGE": "OK", + "DATA": + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "platform": "N9K/N3K", + "name": "nxos.9.3.10.bin", + "fileVersion": "9.3.10", + "version": "9.3.10", + "imageName": "nxos.9.3.10.bin", + "imageType": "image", + "filetype": "nxos", + "size": "1966000640", + "checksum": "2dcae3f31446213f9a610240796a7826", + "osType": "32bit", + "refCount": 0 + }, + { + "platform": "N9K/N3K", + "name": "nxos.9.3.8.bin", + "fileVersion": "9.3.8", + "version": "9.3.8", + "imageName": "nxos.9.3.8.bin", + "imageType": "image", + "filetype": "nxos", + "size": "1956867584", + "checksum": "cd21df22e7c4df02e0d5178c0e6f6d48", + "osType": "32bit", + "refCount": 0 + }], + "message": "" + } + }, + + "image_list_9_3_10_and_rpm_image_response": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + "MESSAGE": "OK", + "DATA": + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "platform": "N9K/N3K", + "name": "nxos.9.3.10.bin", + "fileVersion": "9.3.10", + "version": "9.3.10", + "imageName": "nxos.9.3.10.bin", + "imageType": "image", + "filetype": "nxos", + "size": "1966000640", + "checksum": "2dcae3f31446213f9a610240796a7826", + "osType": "32bit", + "refCount": 0 + }, + { + "platform": "N9K/N3K", + "name": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "fileVersion": "9.3.6", + "version": "1.0.0", + "imageName": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "imageType": "rpm", + "filetype": "Patch", + "size": "19061", + "checksum": "678aca6d523a81f38e9ed070ffcee2c9", + "osType": "32bit", + "refCount": 0 + }], + "message": "" + } + }, + + "image_list_9_3_8_and_rpm_image_response": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.195.225.193:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupload/uploaded-images-table", + "MESSAGE": "OK", + "DATA": + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "platform": "N9K/N3K", + "name": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "fileVersion": "9.3.6", + "version": "1.0.0", + "imageName": "nxos.CSCvw89875-n9k_ALL-1.0.0-9.3.6.lib32_n9000.rpm", + "imageType": "rpm", + "filetype": "Patch", + "size": "19061", + "checksum": "678aca6d523a81f38e9ed070ffcee2c9", + "osType": "32bit", + "refCount": 0 + }, + { + "platform": "N9K/N3K", + "name": "nxos.9.3.8.bin", + "fileVersion": "9.3.8", + "version": "9.3.8", + "imageName": "nxos.9.3.8.bin", + "imageType": "image", + "filetype": "nxos", + "size": "1956867584", + "checksum": "cd21df22e7c4df02e0d5178c0e6f6d48", + "osType": "32bit", + "refCount": 0 + }], + "message": "" + } + } +} diff --git a/tests/unit/modules/dcnm/test_dcnm_image_upload.py b/tests/unit/modules/dcnm/test_dcnm_image_upload.py new file mode 100644 index 000000000..df8daed8c --- /dev/null +++ b/tests/unit/modules/dcnm/test_dcnm_image_upload.py @@ -0,0 +1,857 @@ +# Copyright (c) 2020-2022 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from unittest.mock import patch + +from ansible_collections.cisco.dcnm.plugins.modules import dcnm_image_upload +from .dcnm_module import TestDcnmModule, set_module_args, loadPlaybookData + +import json +import copy + + +class TestDcnmImageUploadModule(TestDcnmModule): + + module = dcnm_image_upload + fd = None + + def init_data(self): + self.fd = None + + def log_msg(self, msg): + + if self.fd is None: + self.fd = open("image-upload-ut.log", "a+") + self.fd.write(msg) + + def setUp(self): + + super(TestDcnmImageUploadModule, self).setUp() + + self.mock_dcnm_send = patch( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upload.dcnm_send" + ) + self.run_dcnm_send = self.mock_dcnm_send.start() + + self.mock_dcnm_version_supported = patch( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upload.dcnm_version_supported" + ) + self.run_dcnm_version_supported = ( + self.mock_dcnm_version_supported.start() + ) + + self.mock_dcnm_get_protocol_and_address = patch( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upload.dcnm_get_protocol_and_address" + ) + + self.run_dcnm_get_protocol_and_address = ( + self.mock_dcnm_get_protocol_and_address.start() + ) + + self.mock_dcnm_get_auth_token = patch( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upload.dcnm_get_auth_token" + ) + + self.run_dcnm_get_auth_token = self.mock_dcnm_get_auth_token.start() + + self.mock_dcnm_post_request = patch( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upload.dcnm_post_request" + ) + + self.run_dcnm_post_request = self.mock_dcnm_post_request.start() + + def tearDown(self): + + super(TestDcnmImageUploadModule, self).tearDown() + self.mock_dcnm_send.stop() + self.mock_dcnm_version_supported.stop() + self.mock_dcnm_get_protocol_and_address.stop() + self.mock_dcnm_get_auth_token.stop() + self.mock_dcnm_post_request.stop() + + # -------------------------- FIXTURES -------------------------- + + def load_image_upload_fixtures(self): + + if "test_dcnm_image_upload_merged_all_new" == self._testMethodName: + + create_resp = self.payloads_data.get("create_response") + image_list_no_images_resp = self.payloads_data.get( + "image_list_no_images_resp" + ) + + self.run_dcnm_send.side_effect = [ + image_list_no_images_resp, + create_resp, + create_resp, + create_resp, + ] + + self.run_dcnm_post_request.side_effect = [create_resp] + + if "test_dcnm_image_upload_merged_no_source" == self._testMethodName: + + create_resp = self.payloads_data.get("create_response") + image_list_no_images_resp = self.payloads_data.get( + "image_list_no_images_resp" + ) + + self.run_dcnm_send.side_effect = [ + image_list_no_images_resp, + create_resp, + ] + + self.run_dcnm_post_request.side_effect = [create_resp] + + if "test_dcnm_image_upload_merged_existing" == self._testMethodName: + + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_all_images_response] + + if ( + "test_dcnm_image_upload_merged_new_no_state" + == self._testMethodName + ): + + create_resp = self.payloads_data.get("create_response") + image_list_no_images_resp = self.payloads_data.get( + "image_list_no_images_resp" + ) + + self.run_dcnm_send.side_effect = [ + image_list_no_images_resp, + create_resp, + create_resp, + create_resp, + ] + + self.run_dcnm_post_request.side_effect = [create_resp] + + if ( + "test_dcnm_image_upload_merged_new_check_mode" + == self._testMethodName + ): + pass + + if ( + "test_dcnm_image_upload_merged_new_existing_and_non_existing" + == self._testMethodName + ): + + create_resp = self.payloads_data.get("create_response") + image_list_9_3_8_and_9_3_10_image_response = self.payloads_data.get( + "image_list_9_3_8_and_9_3_10_image_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_9_3_8_and_9_3_10_image_response, + create_resp, + ] + + self.run_dcnm_post_request.side_effect = [create_resp] + + if "test_dcnm_image_upload_delete_existing" == self._testMethodName: + + delete_resp = self.payloads_data.get("delete_response") + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_all_images_response, + delete_resp, + ] + + if ( + "test_dcnm_image_upload_delete_existing_and_non_existing" + == self._testMethodName + ): + + delete_resp = self.payloads_data.get("delete_response") + image_list_9_3_8_and_9_3_10_image_response = self.payloads_data.get( + "image_list_9_3_8_and_9_3_10_image_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_9_3_8_and_9_3_10_image_response, + delete_resp, + ] + + if ( + "test_dcnm_image_upload_delete_non_existing" + == self._testMethodName + ): + + image_list_no_images_response = self.payloads_data.get( + "image_list_no_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_no_images_response] + + if ( + "test_dcnm_image_upload_delete_without_config" + == self._testMethodName + ): + + delete_resp = self.payloads_data.get("delete_response") + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_all_images_response, + delete_resp, + ] + + if "test_dcnm_image_upload_delete_one" == self._testMethodName: + + delete_resp = self.payloads_data.get("delete_response") + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_all_images_response, + delete_resp, + ] + + if "test_dcnm_image_upload_query_no_config" == self._testMethodName: + + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_all_images_response] + + if "test_dcnm_image_upload_query_one" == self._testMethodName: + + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_all_images_response] + + if "test_dcnm_image_upload_query_all" == self._testMethodName: + + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_all_images_response] + + if "test_dcnm_image_upload_query_non_existing" == self._testMethodName: + + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_all_images_response] + + if "test_dcnm_image_upload_query_2" == self._testMethodName: + + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_all_images_response] + + if ( + "test_dcnm_image_upload_query_exist_and_non_exist" + == self._testMethodName + ): + + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_all_images_response] + + if ( + "test_dcnm_image_upload_override_existing_no_config" + == self._testMethodName + ): + + delete_resp = self.payloads_data.get("delete_response") + image_list_all_images_response = self.payloads_data.get( + "image_list_all_images_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_all_images_response, + delete_resp, + delete_resp, + delete_resp, + ] + + if ( + "test_dcnm_image_upload_override_non_existing_no_config" + == self._testMethodName + ): + + image_list_no_images_response = self.payloads_data.get( + "image_list_no_images_response" + ) + + self.run_dcnm_send.side_effect = [image_list_no_images_response] + + if ( + "test_dcnm_image_upload_override_non_existing_with_new_config" + == self._testMethodName + ): + + create_resp = self.payloads_data.get("create_response") + image_list_no_images_response = self.payloads_data.get( + "image_list_no_images_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_no_images_response, + image_list_no_images_response, + create_resp, + ] + + self.run_dcnm_post_request.side_effect = [create_resp] + + if ( + "test_dcnm_image_upload_override_existing_with_new_config" + == self._testMethodName + ): + + create_resp = self.payloads_data.get("create_response") + delete_resp = self.payloads_data.get("delete_response") + image_list_9_3_8_and_9_3_10_image_response = self.payloads_data.get( + "image_list_9_3_8_and_9_3_10_image_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_9_3_8_and_9_3_10_image_response, + image_list_9_3_8_and_9_3_10_image_response, + delete_resp, + delete_resp, + create_resp, + ] + + self.run_dcnm_post_request.side_effect = [create_resp] + + if ( + "test_dcnm_image_upload_override_with_new_and_existing_config" + == self._testMethodName + ): + + create_resp = self.payloads_data.get("create_response") + image_list_9_3_8_and_9_3_10_image_response = self.payloads_data.get( + "image_list_9_3_8_and_9_3_10_image_response" + ) + + self.run_dcnm_send.side_effect = [ + image_list_9_3_8_and_9_3_10_image_response, + image_list_9_3_8_and_9_3_10_image_response, + create_resp, + ] + + self.run_dcnm_post_request.side_effect = [create_resp] + + def load_fixtures(self, response=None, device=""): + + self.run_dcnm_version_supported.side_effect = [11] + self.run_dcnm_get_protocol_and_address.side_effect = [ + ["https", "//10.195.225.193"] + ] + self.run_dcnm_get_auth_token.side_effect = [ + {"BearerToken": "SampleTokenForUT1"}, + {"BearerToken": "SampleTokenForUT2"}, + {"BearerToken": "SampleTokenForUT3"}, + {"BearerToken": "SampleTokenForUT4"}, + {"BearerToken": "SampleTokenForUT5"}, + {"BearerToken": "SampleTokenForUT6"}, + {"BearerToken": "SampleTokenForUT7"}, + ] + # Load image upload related side-effects + self.load_image_upload_fixtures() + + # -------------------------- FIXTURES END -------------------------- + # -------------------------- TEST-CASES ---------------------------- + + def test_dcnm_image_upload_merged_all_new(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_merge_all_config" + ) + + set_module_args(dict(state="merged", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 3) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["diff"][0]["query"]), 0) + + # Validate create responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual( + resp["DATA"], "Successfully uploaded selected image file(s)." + ) + + def test_dcnm_image_upload_merged_no_source(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_merge_without_source_config" + ) + + set_module_args(dict(state="merged", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 1) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["diff"][0]["query"]), 0) + + # Validate create responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual( + resp["DATA"], "Successfully uploaded selected image file(s)." + ) + + def test_dcnm_image_upload_merged_existing(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_merge_all_config" + ) + + set_module_args(dict(state="merged", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["diff"][0]["query"]), 0) + self.assertEqual(len(result["response"]), 0) + + def test_dcnm_image_upload_merged_new_no_state(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_merge_all_config" + ) + + set_module_args(dict(files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 3) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["diff"][0]["query"]), 0) + + # Validate create responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual( + resp["DATA"], "Successfully uploaded selected image file(s)." + ) + + def test_dcnm_image_upload_merged_new_check_mode(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_merge_all_config" + ) + + set_module_args( + dict( + _ansible_check_mode=True, + state="merged", + files=self.playbook_config, + ) + ) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 3) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["diff"][0]["query"]), 0) + self.assertEqual(len(result["response"]), 0) + + def test_dcnm_image_upload_merged_new_existing_and_non_existing(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_merge_all_config" + ) + + set_module_args(dict(state="merged", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 1) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["diff"][0]["query"]), 0) + + # Validate create responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + + def test_dcnm_image_upload_delete_existing(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_delete_all_config" + ) + + set_module_args(dict(state="deleted", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 3) + self.assertEqual(len(result["diff"][0]["query"]), 0) + + # Validate delete responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual(resp["DATA"], "Image(s) Deleted Successfully") + + def test_dcnm_image_upload_delete_existing_and_non_existing(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_delete_all_config" + ) + + set_module_args(dict(state="deleted", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 2) + self.assertEqual(len(result["diff"][0]["query"]), 0) + + # Validate delete responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual(resp["DATA"], "Image(s) Deleted Successfully") + + def test_dcnm_image_upload_delete_non_existing(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_delete_all_config" + ) + + set_module_args(dict(state="deleted", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["diff"][0]["query"]), 0) + self.assertEqual(len(result["response"]), 0) + + def test_dcnm_image_upload_delete_without_config(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = [] + + set_module_args(dict(state="deleted", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 3) + self.assertEqual(len(result["diff"][0]["query"]), 0) + + # Validate delete responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual(resp["DATA"], "Image(s) Deleted Successfully") + + def test_dcnm_image_upload_delete_one(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_delete_rpm_config" + ) + + set_module_args(dict(state="deleted", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 1) + self.assertEqual(len(result["diff"][0]["query"]), 0) + + # Validate delete responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual(resp["DATA"], "Image(s) Deleted Successfully") + + def test_dcnm_image_upload_query_no_config(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = [] + + set_module_args(dict(state="query", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["response"]), 3) + + def test_dcnm_image_upload_query_one(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_query_rpm_config" + ) + + set_module_args(dict(state="query", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["response"]), 1) + + def test_dcnm_image_upload_query_all(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_query_all_config" + ) + + set_module_args(dict(state="query", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["response"]), 3) + + def test_dcnm_image_upload_query_non_existing(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_query_non_exist_config" + ) + + set_module_args(dict(state="query", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["response"]), 0) + + def test_dcnm_image_upload_query_2(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_query_9_3_8_and_9_3_10_config" + ) + + set_module_args(dict(state="query", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["response"]), 2) + + def test_dcnm_image_upload_query_exist_and_non_exist(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_query_exist_and_non_exist_config" + ) + + set_module_args(dict(state="query", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["response"]), 1) + + def test_dcnm_image_upload_override_existing_no_config(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = [] + + set_module_args(dict(state="overridden", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 3) + + # Validate delete responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual(resp["DATA"], "Image(s) Deleted Successfully") + + def test_dcnm_image_upload_override_non_existing_no_config(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = [] + + set_module_args(dict(state="overridden", files=self.playbook_config)) + result = self.execute_module(changed=False, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + + def test_dcnm_image_upload_override_non_existing_with_new_config(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_override_rpm_config" + ) + + set_module_args(dict(state="overridden", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 1) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + + # Validate delete responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertEqual( + resp["DATA"], "Successfully uploaded selected image file(s)." + ) + + def test_dcnm_image_upload_override_existing_with_new_config(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_override_rpm_config" + ) + + set_module_args(dict(state="overridden", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 1) + self.assertEqual(len(result["diff"][0]["deleted"]), 2) + + # Validate delete responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertTrue( + "Image(s) Deleted Successfully" in resp["DATA"] + or "Successfully uploaded selected image file(s)." + in resp["DATA"] + ) + + def test_dcnm_image_upload_override_with_new_and_existing_config(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_image_upload_configs") + self.payloads_data = loadPlaybookData("dcnm_image_upload_payloads") + + # load required config data + self.playbook_config = self.config_data.get( + "image_upload_override_all_new_config" + ) + + set_module_args(dict(state="overridden", files=self.playbook_config)) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 1) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + + # Validate delete responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + self.assertTrue( + "Successfully uploaded selected image file(s)." in resp["DATA"] + )