From 5503be0edee0fb32964e3cad4f2805ba11bc41fc Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 27 Oct 2023 10:46:26 -1000 Subject: [PATCH 001/300] initial commit for dcnm_image_upgrade module and unit-tests --- dcnm-ut | 0 plugins/modules/dcnm_image_upgrade.py | 5313 +++++++++++++++++ .../dcnm/dcnm_image_upgrade/__init__.py | 0 .../dcnm/dcnm_image_upgrade/fixture.py | 38 + .../fixtures/dcnm_image_upgrade_payloads.json | 24 + .../dcnm_image_upgrade_playbook_configs.json | 11 + .../dcnm_image_upgrade_responses.json | 2098 +++++++ ...sponses_NdfcAnsibleImageUpgradeCommon.json | 58 + ...ade_responses_NdfcImageInstallOptions.json | 38 + ...e_upgrade_responses_NdfcImagePolicies.json | 97 + ...grade_responses_NdfcSwitchIssuDetails.json | 2510 ++++++++ ...m_image_upgrade_responses_NdfcVersion.json | 363 ++ ...e_upgrade_NdfcAnsibleImageUpgradeCommon.py | 274 + .../test_dcnm_image_upgrade_NdfcEndpoints.py | 241 + ...m_image_upgrade_NdfcImageInstallOptions.py | 171 + ...st_dcnm_image_upgrade_NdfcImagePolicies.py | 211 + ...cnm_image_upgrade_NdfcImagePolicyAction.py | 274 + .../test_dcnm_image_upgrade_NdfcImageStage.py | 368 ++ ...st_dcnm_image_upgrade_NdfcImageValidate.py | 332 + ...grade_NdfcSwitchIssuDetailsByDeviceName.py | 215 + ...pgrade_NdfcSwitchIssuDetailsByIpAddress.py | 215 + ...ade_NdfcSwitchIssuDetailsBySerialNumber.py | 215 + .../test_dcnm_image_upgrade_NdfcVersion.py | 570 ++ 23 files changed, 13636 insertions(+) create mode 100644 dcnm-ut create mode 100644 plugins/modules/dcnm_image_upgrade.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/__init__.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_payloads.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_playbook_configs.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImageInstallOptions.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImagePolicies.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByDeviceName.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByIpAddress.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsBySerialNumber.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcVersion.py diff --git a/dcnm-ut b/dcnm-ut new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py new file mode 100644 index 000000000..8d4e610f6 --- /dev/null +++ b/plugins/modules/dcnm_image_upgrade.py @@ -0,0 +1,5313 @@ +#!/usr/bin/python +# +# Copyright (c) 2020-2024 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. +""" +Classes and methods for Ansible support of Nexus image upgrade. + +Ansible states "merged", "deleted", and "query" are implemented. + +merged: attach image policy to one or more devices +deleted: delete image policy from one or more devices +query: return image policy details for one or more devices +""" +from __future__ import absolute_import, division, print_function + +import copy +import json +from time import sleep + +from ansible.module_utils.basic import AnsibleModule +# from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm_image_upgrade_lib import ( +# NdfcAnsibleImageUpgradeCommon, +# NdfcImageValidate, +# NdfcImagePolicies, +# NdfcImagePolicyAction, +# NdfcImageInstallOptions, +# NdfcImageUpgrade, +# NdfcSwitchDetails, +# NdfcSwitchIssuDetailsByIpAddress +# ) +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, + validate_list_of_dicts, +) + +__metaclass__ = type +__author__ = "Cisco Systems, Inc." + +DOCUMENTATION = """ +--- +module: dcnm_image_upgrade +short_description: Attach, detach, and query device image policies. +version_added: "0.9.0" +description: + - Attach, detach, and query device image policies. +author: Cisco Systems, Inc. +options: + state: + description: + - The state of DCNM after module completion. + - I(merged) and I(query) are the only states supported. + type: str + choices: + - merged + - deleted + - query + default: merged + config: + description: + - A dictionary containing the image policy configuration. + type: dict + suboptions: + policy: + description: + - Image policy name + type: str + required: true + default: False + stage: + description: + - Stage (True) or unstage (False) an image policy + type: bool + required: false + default: True + validate: + description: + - Validate (True) or do not validate (False) the image + - after staging + type: bool + required: false + default: True + reboot: + description: + - Reboot the switch after upgrade + type: bool + required: false + default: False + upgrade: + description: + - A dictionary containing upgrade toggles for nxos and epld + type: dict + suboptions: + nxos: + description: + - Enable (True) or disable (False) image upgrade + type: bool + required: false + default: True + epld: + description: + - Enable (True) or disable (False) EPLD upgrade + - If upgrade.nxos is false, epld and packages cannot both be true + - If epld is true, nxos_option must be disruptive + type: bool + required: false + default: False + options: + description: + - A dictionary containing options for each of the upgrade types + type: dict + suboptions: + nxos: + description: + - A dictionary containing nxos upgrade options + type: dict + suboptions: + mode: + description: + - nxos upgrade mode + - Choose between distruptive, non_disruptive, force_non_disruptive + type: string + required: false + default: distruptive + bios_force: + description: + - Force BIOS upgrade + type: bool + required: false + default: False + epld: + description: + - A dictionary containing epld upgrade options + type: dict + suboptions: + module: + description: + - The switch module to upgrade + - Choose between ALL, or integer values + type: string + required: false + default: ALL + golden: + description: + - Enable (True) or disable (False) reverting to the golden EPLD image + type: bool + required: false + default: False + reboot: + description: + - A dictionary containing reboot options + type: dict + suboptions: + config_reload: + description: + - Reload the configuration + type: bool + required: false + default: False + write_erase: + description: + - Erase the startup configuration + type: bool + required: false + default: False + package: + description: + - A dictionary containing package upgrade options + type: dict + suboptions: + install: + description: + - Install the package + type: bool + required: false + default: False + uninstall: + description: + - Uninstall the package + type: bool + required: false + default: False + switches: + description: + - A list of devices to attach the image policy to. + type: list + elements: dict + required: true + suboptions: + ip_address: + description: + - The IP address of the device to which the policy will be attached. + type: str + required: true + policy: + description: + - Image policy name + type: str + required: true + default: False + stage: + description: + - Stage (True) or unstage (False) an image policy + type: bool + required: false + default: True + validate: + description: + - Validate (True) or do not validate (False) the image + - after staging + type: bool + required: false + default: True + reboot: + description: + - Reboot the switch after upgrade + type: bool + required: false + default: False + upgrade: + description: + - A dictionary containing upgrade toggles for nxos and epld + type: dict + suboptions: + nxos: + description: + - Enable (True) or disable (False) image upgrade + type: bool + required: false + default: True + epld: + description: + - Enable (True) or disable (False) EPLD upgrade + - If upgrade.nxos is false, epld and packages cannot both be true + - If epld is true, nxos_option must be disruptive + type: bool + required: false + default: False + options: + description: + - A dictionary containing options for each of the upgrade types + type: dict + suboptions: + nxos: + description: + - A dictionary containing nxos upgrade options + type: dict + suboptions: + mode: + description: + - nxos upgrade mode + - Choose between distruptive, non_disruptive, force_non_disruptive + type: string + required: false + default: distruptive + bios_force: + description: + - Force BIOS upgrade + type: bool + required: false + default: False + epld: + description: + - A dictionary containing epld upgrade options + type: dict + suboptions: + module: + description: + - The switch module to upgrade + - Choose between ALL, or integer values + type: string + required: false + default: ALL + golden: + description: + - Enable (True) or disable (False) reverting to the golden EPLD image + type: bool + required: false + default: False + reboot: + description: + - A dictionary containing reboot options + type: dict + suboptions: + config_reload: + description: + - Reload the configuration + type: bool + required: false + default: False + write_erase: + description: + - Erase the startup configuration + type: bool + required: false + default: False + package: + description: + - A dictionary containing package upgrade options + type: dict + suboptions: + install: + description: + - Install the package + type: bool + required: false + default: False + uninstall: + description: + - Uninstall the package + type: bool + required: false + default: False + +""" + +EXAMPLES = """ +# This module supports the following states: +# +# merged: +# Attach image policy to one or more devices. +# +# query: +# Return image policy details for one or more devices. +# +# deleted: +# Delete image policy from one or more devices +# + +# Attach image policy NR3F to two devices +# Stage and validate the image on two devices but do not upgrade + - name: stage/validate images + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + policy: NR3F + stage: true + validate: true + upgrade: + nxos: false + epld: false + switches: + - ip_address: 192.168.1.1 + - ip_address: 192.168.1.2 + +# Attach image policy NR1F to device 192.168.1.1 +# Attach image policy NR2F to device 192.168.1.2 +# Stage the image on device 192.168.1.1, but do not upgrade +# Stage the image and upgrade device 192.168.1.2 + - name: stage/upgrade devices + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + validate: false + stage: false + upgrade: + nxos: false + epld: false + options: + nxos: + type: disruptive + epld: + module: ALL + golden: false + switches: + - ip_address: 192.168.1.1 + policy: NR1F + stage: true + validate: true + upgrade: + nxos: true + epld: false + - ip_address: 192.168.1.2 + policy: NR2F + stage: true + validate: true + upgrade: + nxos: true + epld: true + options: + nxos: + type: disruptive + epld: + module: ALL + golden: false + +# Detach image policy NR3F from two devices + - name: stage/upgrade devices + cisco.dcnm.dcnm_image_upgrade: + state: deleted + config: + policy: NR3F + switches: + - ip_address: 192.168.1.1 + - ip_address: 192.168.1.2 + +""" +class NdfcEndpoints: + """ + Endpoints for NDFC API calls + """ + def __init__(self): + self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" + + self.endpoint_feature_manager = f"{self.endpoint_api_v1}/fm" + self.endpoint_lan_fabric = f"{self.endpoint_api_v1}/lan-fabric" + + self.endpoint_image_management = f"{self.endpoint_api_v1}" + self.endpoint_image_management += "/imagemanagement" + + self.endpoint_image_upgrade = f"{self.endpoint_image_management}" + self.endpoint_image_upgrade += "/rest/imageupgrade" + + self.endpoint_package_mgnt = f"{self.endpoint_image_management}" + self.endpoint_package_mgnt += "/rest/packagemgnt" + + self.endpoint_policy_mgnt = f"{self.endpoint_image_management}" + self.endpoint_policy_mgnt += "/rest/policymgnt" + + self.endpoint_staging_management = f"{self.endpoint_image_management}" + self.endpoint_staging_management += "/rest/stagingmanagement" + + @property + def bootflash_info(self): + path = f"{self.endpoint_image_management}/rest/imagemgnt/bootFlash" + path += f"/bootflash-info" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def install_options(self): + path = f"{self.endpoint_image_upgrade}/install-options" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_stage(self): + path = f"{self.endpoint_staging_management}/stage-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_upgrade(self): + path = f"{self.endpoint_image_upgrade}/upgrade-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_validate(self): + path = f"{self.endpoint_staging_management}/validate-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def issu_info(self): + path = f"{self.endpoint_package_mgnt}/issu" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def ndfc_version(self): + path = f"{self.endpoint_feature_manager}/about/version" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policies_attached_info(self): + path = f"{self.endpoint_policy_mgnt}/all-attached-policies" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policies_info(self): + path = f"{self.endpoint_policy_mgnt}/policies" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policy_attach(self): + path = f"{self.endpoint_policy_mgnt}/attach-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def policy_create(self): + path = f"{self.endpoint_policy_mgnt}/platform-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def policy_detach(self): + path = f"{self.endpoint_policy_mgnt}/detach-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "DELETE" + return endpoint + + @property + def policy_info(self): + # Replace __POLICY_NAME__ with the policy_name to query + # e.g. path.replace("__POLICY_NAME__", "NR1F") + path = f"{self.endpoint_policy_mgnt}/image-policy/__POLICY_NAME__" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def stage_info(self): + path = f"{self.endpoint_staging_management}/stage-info" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def switches_info(self): + path = f"{self.endpoint_lan_fabric}/rest/inventory/allswitches" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + +class NdfcAnsibleImageUpgradeCommon: + """ + Base class for the other classes in this file + """ + + def __init__(self, module): + self.module = module + self.params = module.params + self.debug = True + self.fd = None + self.logfile = "/tmp/dcnm_image_upgrade.log" + self.endpoints = NdfcEndpoints() + + + def _handle_response(self, response, verb): + if verb == "GET": + return self._handle_get_response(response) + if verb in {"POST", "PUT", "DELETE"}: + return self._handle_post_put_delete_response(response) + return self._handle_unknown_request_verbs(response, verb) + + def _handle_unknown_request_verbs(self, response, verb): + msg = f"Unknown request verb ({verb}) in _handle_response() for response {response}." + self.module.fail_json(msg) + + def _handle_get_response(self, response): + """ + Caller: + - self._handle_response() + Handle NDFC responses to GET requests + Returns: dict() with the following keys: + - found: + - False, if request error was "Not found" and RETURN_CODE == 404 + - True otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + success_return_codes = {200, 404} + if ( + response.get("RETURN_CODE") == 404 + and response.get("MESSAGE") == "Not Found" + ): + result["found"] = False + result["success"] = True + return result + if ( + response.get("RETURN_CODE") not in success_return_codes + or response.get("MESSAGE") != "OK" + ): + result["found"] = False + result["success"] = False + return result + result["found"] = True + result["success"] = True + return result + + def _handle_post_put_delete_response(self, response): + """ + Caller: + - self.self._handle_response() + + Handle POST, PUT responses from NDFC. + + Returns: dict() with the following keys: + - changed: + - True if changes were made to NDFC + - False otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + if response.get("MESSAGE") != "OK": + result["success"] = False + result["changed"] = False + return result + if response.get("ERROR"): + result["success"] = False + result["changed"] = False + return result + result["success"] = True + result["changed"] = True + return result + + def log_msg(self, msg): + """ + used for debugging. disable this when committing to main + by setting __init__().debug to False + """ + if self.debug is False: + return + if self.fd is None: + try: + self.fd = open(f"{self.logfile}", "a+", encoding="UTF-8") + except IOError as err: + msg = f"error opening logfile {self.logfile}. " + msg += f"detail: {err}" + self.module.fail_json(msg) + + self.fd.write(msg) + self.fd.write("\n") + self.fd.flush() + + def make_boolean(self, value): + """ + Return value converted to boolean, if possible. + Return value, if value cannot be converted. + """ + if isinstance(value, bool): + return value + if isinstance(value, str): + if value.lower() in ["true", "yes"]: + return True + if value.lower() in ["false", "no"]: + return False + return value + + def make_none(self, value): + """ + Return None if value is an empty string, or a string + representation of a None type + Return value otherwise + """ + if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: + return None + return value + + +class NdfcAnsibleImageUpgrade(NdfcAnsibleImageUpgradeCommon): + """ + Ansible support for image policy attach, detach, and query. + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.log_msg(f"{self.class_name}.__init__") + # populated in self._build_policy_attach_payload() + self.payloads = [] + + self.config = module.params.get("config") + self.log_msg(f"{self.class_name}.__init__() self.config {self.config}") + if not isinstance(self.config, dict): + msg = "expected dict type for self.config. " + msg = +f"got {type(self.config).__name__}" + self.module.fail_json(msg) + + self.check_mode = False + self.validated = [] + self.have_create = [] + self.want_create = [] + self.need = [] + self.diff_save = {} + self.query = [] + self.result = dict(changed=False, diff=[], response=[]) + + self.mandatory_global_keys = {"switches"} + self.mandatory_switch_keys = {"ip_address"} + + if not self.mandatory_global_keys.issubset(self.config): + msg = f"{self.class_name}.__init__: " + msg += "Missing mandatory key(s) in playbook global config. " + msg += f"expected {self.mandatory_global_keys}, " + msg += f"got {self.config.keys()}" + self.module.fail_json(msg) + + if self.config["switches"] is None: + msg = f"{self.class_name}.__init__: " + msg += "missing list of switches in playbook config." + self.module.fail_json(msg) + + for switch in self.config["switches"]: + if not self.mandatory_switch_keys.issubset(switch): + msg = f"{self.class_name}.__init__: " + msg += f"missing mandatory key(s) in playbook switch config. " + msg += f"expected {self.mandatory_switch_keys}, " + msg += f"got {switch.keys()}" + self.module.fail_json(msg) + + self.log_msg(f"{self.class_name}.__init__: instantiate NdfcSwitchDetails") + self.switch_details = NdfcSwitchDetails(self.module) + self.log_msg(f"{self.class_name}.__init__: instantiate NdfcImagePolicies") + self.image_policies = NdfcImagePolicies(self.module) + + def get_have(self): + """ + Caller: main() + + Determine current switch ISSU state on NDFC + """ + self.have = NdfcSwitchIssuDetailsByIpAddress(self.module) + self.have.refresh() + + def get_want(self): + """ + Caller: main() + + Update self.want_create for all switches defined in the playbook + """ + self._merge_global_and_switch_configs(self.config) + self._validate_switch_configs() + if not self.switch_configs: + return + self.log_msg(f"{self.class_name}.get_want() self.switch_configs: {self.switch_configs}") + self.want_create = self.switch_configs + + def _get_idempotent_want(self, want): + """ + Return an itempotent want item based on the have item contents. + + The have item is obtained from an instance of NdfcSwitchIssuDetails + created in self.get_have(). + + want structure passed to this method: + + { + 'policy': 'KR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'nxos': { + 'mode': 'non_disruptive' + 'bios_force': False + }, + 'epld': { + 'module': 'ALL', + 'golden': False + } + }, + 'validate': True, + 'ip_address': '172.22.150.102' + } + + The returned idempotent_want structure is identical to the + above structure, except that the policy_changed key is added, + and values are modified based on results from the have item, + and the information returned by NdfcImageInstallOptions. + + Caller: self.get_need_merged() + """ + self.log_msg(f"{self.class_name}._get_idempotent_want() want: {want}") + self.have.ip_address = want["ip_address"] + + want["policy_changed"] = True + # In NDFC, the switch does not have an image policy attached + # Return the want item as-is with policy_changed = True + if self.have.serial_number is None: + self.log_msg(f"{self.class_name}._get_idempotent_want() no serial_number. returning want: {want}") + return want + # The switch has an image policy attached which is + # different from the want policy. + # Return the want item as-is with policy_changed = True + if want["policy"] != self.have.policy: + self.log_msg(f"{self.class_name}._get_idempotent_want() different policy attached. returning want: {want}") + return want + + # start with a copy of the want item + idempotent_want = copy.deepcopy(want) + # Give an indication to the caller that the policy has not changed + # We can use this later to determine if we need to do anything in + # the case where the image is already staged and/or upgraded. + idempotent_want["policy_changed"] = False + + # if the image is already staged, don't stage it again + if self.have.image_staged == "Success": + idempotent_want["stage"] = False + # if the image is already validated, don't validate it again + if self.have.validated == "Success": + idempotent_want["validate"] = False + # if the image is already upgraded, don't upgrade it again + if self.have.status == "In-Sync" and self.have.reason == "Upgrade" and self.have.policy == want["policy"]: + idempotent_want["upgrade"]["nxos"] = False + + # Get relevant install options from NDFC based on the + # options in our want item + instance = NdfcImageInstallOptions(self.module) + instance.policy_name = want["policy"] + msg = f"REMOVE: {self.class_name}._get_idempotent_want() " + msg += f"calling NdfcImageInstallOptions.refresh() with " + msg += f"serial_number {self.have.serial_number} " + msg += f"ip_address {self.have.ip_address} " + msg += f"device_name {self.have.device_name}" + self.log_msg(msg) + instance.serial_number = self.have.serial_number + instance.epld = want["upgrade"]["epld"] + instance.issu = want["upgrade"]["nxos"] + instance.refresh() + + if instance.epld_modules is None: + idempotent_want["upgrade"]["epld"] = False + self.log_msg(f"REMOVE: {self.class_name}._get_idempotent_want() returned idempotent_want: {idempotent_want}") + return idempotent_want + + def get_need_merged(self): + """ + Caller: main() + + For merged state, populate self.need list() with items from + our want list that are not in our have list. These items will be sent + to NDFC. + """ + need = [] + + for want_create in self.want_create: + self.have.ip_address = want_create["ip_address"] + if self.have.serial_number is not None: + idempotent_want = self._get_idempotent_want(want_create) + if ( + idempotent_want["policy_changed"] is False + and idempotent_want["stage"] is False + and idempotent_want["upgrade"]["nxos"] is False + and idempotent_want["upgrade"]["epld"] is False + ): + continue + need.append(idempotent_want) + self.need = need + msg = f"REMOVE: {self.class_name}.get_need_merged: " + msg += f"need: {self.need}" + self.log_msg(msg) + + def get_need_deleted(self): + """ + Caller: main() + + For deleted state, populate self.need list() with items from our want + list that are not in our have list. These items will be sent to NDFC. + """ + need = [] + for want in self.want_create: + self.have.ip_address = want["ip_address"] + if self.have.serial_number is None: + continue + if self.have.policy is None: + continue + need.append(want) + self.need = need + + def get_need_query(self): + """ + Caller: main() + + For query state, populate self.need list() with all items from our want + list. These items will be sent to NDFC. + """ + need = [] + for want in self.want_create: + need.append(want) + self.need = need + + @staticmethod + def _build_params_spec_for_merged_state(): + """ + Build the specs for the parameters expected when state == merged. + + Caller: _validate_input_for_merged_state() + Return: params_spec, a dictionary containing the set of + parameter specifications. + """ + params_spec = {} + params_spec["policy"] = {} + params_spec["policy"]["required"] = False + params_spec["policy"]["type"] = "str" + + params_spec["stage"] = {} + params_spec["stage"]["required"] = False + params_spec["stage"]["type"] = "bool" + params_spec["stage"]["default"] = True + + params_spec["validate"] = {} + params_spec["validate"]["required"] = False + params_spec["validate"]["type"] = "bool" + params_spec["validate"]["default"] = True + + params_spec["upgrade"] = {} + params_spec["upgrade"]["required"] = False + params_spec["upgrade"]["type"] = "dict" + params_spec["upgrade"]["default"] = {} + + section = "options" + params_spec[section] = {} + params_spec[section]["required"] = False + params_spec[section]["type"] = "dict" + params_spec[section]["default"] = {} + + sub_section = "nxos" + params_spec[section][sub_section] = {} + params_spec[section][sub_section]["required"] = False + params_spec[section][sub_section]["type"] = "dict" + params_spec[section][sub_section]["default"] = {} + + params_spec[section][sub_section]["mode"] = {} + params_spec[section][sub_section]["mode"]["required"] = False + params_spec[section][sub_section]["mode"]["type"] = "str" + params_spec[section][sub_section]["mode"]["default"] = "disruptive" + + params_spec[section][sub_section]["bios_force"] = {} + params_spec[section][sub_section]["bios_force"]["required"] = False + params_spec[section][sub_section]["bios_force"]["type"] = "bool" + params_spec[section][sub_section]["bios_force"]["default"] = False + + sub_section = "epld" + params_spec[section][sub_section] = {} + params_spec[section][sub_section]["required"] = False + params_spec[section][sub_section]["type"] = "dict" + params_spec[section][sub_section]["default"] = {} + + params_spec[section][sub_section]["module"] = {} + params_spec[section][sub_section]["module"] ["required"] = False + params_spec[section][sub_section]["module"] ["type"] = "str" + params_spec[section][sub_section]["module"] ["default"] = "ALL" + + params_spec[section][sub_section]["golden"] = {} + params_spec[section][sub_section]["golden"] ["required"] = False + params_spec[section][sub_section]["golden"] ["type"] = "bool" + params_spec[section][sub_section]["golden"] ["default"] = False + + sub_section = "reboot" + params_spec[section][sub_section] = {} + params_spec[section][sub_section]["required"] = False + params_spec[section][sub_section]["type"] = "dict" + params_spec[section][sub_section]["default"] = {} + + params_spec[section][sub_section]["config_reload"] = {} + params_spec[section][sub_section]["config_reload"] ["required"] = False + params_spec[section][sub_section]["config_reload"] ["type"] = "bool" + params_spec[section][sub_section]["config_reload"] ["default"] = False + + params_spec[section][sub_section]["write_erase"] = {} + params_spec[section][sub_section]["write_erase"] ["required"] = False + params_spec[section][sub_section]["write_erase"] ["type"] = "bool" + params_spec[section][sub_section]["write_erase"] ["default"] = False + + sub_section = "package" + params_spec[section][sub_section] = {} + params_spec[section][sub_section]["required"] = False + params_spec[section][sub_section]["type"] = "dict" + params_spec[section][sub_section]["default"] = {} + + params_spec[section][sub_section]["install"] = {} + params_spec[section][sub_section]["install"] ["required"] = False + params_spec[section][sub_section]["install"] ["type"] = "bool" + params_spec[section][sub_section]["install"] ["default"] = False + + params_spec[section][sub_section]["uninstall"] = {} + params_spec[section][sub_section]["uninstall"] ["required"] = False + params_spec[section][sub_section]["uninstall"] ["type"] = "bool" + params_spec[section][sub_section]["uninstall"] ["default"] = False + + return copy.deepcopy(params_spec) + + def validate_input(self): + """ + Caller: main() + + Validate the playbook parameters + """ + state = self.params["state"] + self.log_msg(f"{self.class_name}.validate_input: state: {state}") + + if state not in ["merged", "deleted", "query"]: + msg = f"This module supports deleted, merged, and query states. Got state {state}" + self.module.fail_json(msg) + + if state == "merged": + self.log_msg(f"{self.class_name}.validate_input: call _validate_input_for_merged_state()") + self._validate_input_for_merged_state() + return + if state == "deleted": + self._validate_input_for_deleted_state() + return + if state == "query": + self._validate_input_for_query_state() + return + + def _validate_input_for_merged_state(self): + """ + Caller: self.validate_input() + + Validate that self.config contains appropriate values for merged state + """ + if not self.config: + msg = "config: element is mandatory for state merged" + self.module.fail_json(msg) + + params_spec = self._build_params_spec_for_merged_state() + + self.log_msg(f"{self.class_name}._validate_input_for_merged_state: params_spec: {params_spec}") + self.log_msg(f"{self.class_name}._validate_input_for_merged_state: self.config: {self.config}") + valid_params, invalid_params = validate_list_of_dicts( + self.config.get("switches"), params_spec, self.module + ) + # We're not using self.validated. Keeping this to avoid + # linter error due to non-use of valid_params + self.validated = copy.deepcopy(valid_params) + + if invalid_params: + msg = "Invalid parameters in playbook: " + msg += f"{','.join(invalid_params)}" + self.module.fail_json(msg) + + def _validate_input_for_deleted_state(self): + """ + Caller: self.validate_input() + + Validate that self.config contains appropriate values for deleted state + + NOTES: + 1. This is currently identical to _validate_input_for_merged_state() + 2. Adding in case there are differences in the future + """ + params_spec = self._build_params_spec_for_merged_state() + if not self.config: + msg = "config: element is mandatory for state deleted" + self.module.fail_json(msg) + + valid_params, invalid_params = validate_list_of_dicts( + self.config.get("switches"), params_spec, self.module + ) + # We're not using self.validated. Keeping this to avoid + # linter error due to non-use of valid_params + self.validated = copy.deepcopy(valid_params) + + if invalid_params: + msg = "Invalid parameters in playbook: " + msg += f"{','.join(invalid_params)}" + self.module.fail_json(msg) + + def _validate_input_for_query_state(self): + """ + Caller: self.validate_input() + + Validate that self.config contains appropriate values for query state + + NOTES: + 1. This is currently identical to _validate_input_for_merged_state() + 2. Adding in case there are differences in the future + """ + params_spec = self._build_params_spec_for_merged_state() + if not self.config: + msg = "config: element is mandatory for state query" + self.module.fail_json(msg) + + valid_params, invalid_params = validate_list_of_dicts( + self.config.get("switches"), params_spec, self.module + ) + # We're not using self.validated. Keeping this to avoid + # linter error due to non-use of valid_params + self.validated = copy.deepcopy(valid_params) + + if invalid_params: + msg = "Invalid parameters in playbook: " + msg += f"{','.join(invalid_params)}" + self.module.fail_json(msg) + + def _merge_global_and_switch_configs(self, config): + """ + Merge the global config with each switch config and return + a dict of switch configs keyed on switch ip_address. + + Merge rules: + 1. switch_config takes precedence over global_config. + 2. If switch_config is missing a parameter, use parameter + from global_config. + 3. If a switch_config has a parameter, use it. + 4. If global_config and switch_config are both missing an + optional parameter, use the parameter's default value. + 5. If global_config and switch_config are both missing a + mandatory parameter, fail. + """ + if not config.get("switches"): + msg = f"{self.class_name}._merge_global_and_switch_configs: " + msg += "playbook is missing list of switches" + self.module.fail_json(msg) + + msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + msg += f"config: {config}" + self.log_msg(msg) + global_config = {} + global_config["policy"] = config.get("policy") + global_config["stage"] = config.get("stage") + global_config["upgrade"] = config.get("upgrade") + global_config["options"] = config.get("options") + global_config["validate"] = config.get("validate") + msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + msg += f"global_config: {global_config}" + self.log_msg(msg) + self.switch_configs = [] + for switch in config["switches"]: + switch_config = global_config.copy() | switch.copy() + msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + msg += f"merged switch_config: {switch_config}" + self.log_msg(msg) + self.switch_configs.append(switch_config) + + def _validate_switch_configs(self): + """ + Ensure mandatory parameters are present for each switch + - fail_json if this isn't the case + Set defaults for missing optional parameters + + NOTES: + 1. Final application of missing default parameters is done in + NdfcImageUpgrade.commit() + + Callers: + - self.get_want + """ + for switch in self.switch_configs: + msg = f"REMOVE: {self.class_name}._validate_switch_configs: " + msg += f"switch: {switch}" + self.log_msg(msg) + if not switch.get("ip_address"): + msg = "playbook is missing ip_address for at least one switch" + self.module.fail_json(msg) + # for query state, the only mandatory parameter is ip_address + # so skip the remaining checks + if self.params.get("state") == "query": + continue + if switch.get("policy") is None: + msg = "playbook is missing image policy for switch " + msg += f"{switch.get('ip_address')} " + msg += "and global image policy is not defined." + self.module.fail_json(msg) + + def _build_policy_attach_payload(self): + """ + Build the payload for the policy attach request to NDFC + Verify that the image policy exists on NDFC + Verify that the image policy supports the switch platform + + Callers: + - self.handle_merged_state + """ + self.payloads = [] + # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests + self.switch_details.refresh() + self.image_policies.refresh() + for switch in self.need: + if switch.get("policy_changed") is False: + continue + self.switch_details.ip_address = switch.get("ip_address") + self.image_policies.policy_name = switch.get("policy") + + # Fail if the image policy does not exist. + # Image policy creation is handled by a different module. + if self.image_policies.name is None: + msg = f"policy {switch.get('policy')} does not exist on NDFC" + self.module.fail_json(msg) + + # Fail if the image policy does not support the switch platform + if self.switch_details.platform not in self.image_policies.platform: + msg = f"policy {switch.get('policy')} does not support platform " + msg += f"{self.switch_details.platform}. {switch.get('policy')} " + msg += "supports the following platform(s): " + msg += f"{self.image_policies.platform}" + self.module.fail_json(msg) + + payload = {} + payload["policyName"] = self.image_policies.name + # switch_details.host_name is always None in 12.1.2e + # so we're using logical_name instead + payload["hostName"] = self.switch_details.logical_name + payload["ipAddr"] = self.switch_details.ip_address + payload["platform"] = self.switch_details.platform + payload["serialNumber"] = self.switch_details.serial_number + # payload["bootstrapMode"] = switch.get('bootstrap_mode') + + for item in payload: + if payload[item] is None: + msg = f"Unable to determine {item} for switch {switch.get('ip_address')}. " + msg += "Please verify that the switch is managed by NDFC." + self.module.fail_json(msg) + self.payloads.append(payload) + + def _send_policy_attach_payload(self): + """ + Send the policy attach payload to NDFC and handle the response + + Callers: + - self.handle_merged_state + """ + if len(self.payloads) == 0: + return + path = self.endpoints.policy_attach.get("path") + verb = self.endpoints.policy_attach.get("verb") + payload = {} + payload["mappingList"] = self.payloads + response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) + result = self._handle_response(response, verb) + + if not result["success"]: + self._failure(response) + + def _stage_images(self, serial_numbers): + """ + Initiate image staging to the switch(es) associated with serial_numbers + + Callers: + - handle_merged_state + """ + instance = NdfcImageStage(self.module) + instance.serial_numbers = serial_numbers + instance.commit() + + def _validate_images(self, serial_numbers): + """ + Validate the image staged to the switch(es) + + Callers: + - handle_merged_state + """ + instance = NdfcImageValidate(self.module) + instance.serial_numbers = serial_numbers + # TODO:2 Discuss with Mike/Shangxin - NdfcImageValidate.non_disruptive + # Should we add this option to the playbook? + # It's supported in NdfcImageValidate with default of False + # instance.non_disruptive = False + instance.commit() + + def _verify_install_options(self, devices): + """ + Verify that the install options for the devices(es) are valid + + Example devices structure: + + [ + { + 'policy': 'KR3F', + 'stage': False, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'nxos': { + 'mode': 'non_disruptive' + }, + 'epld': { + 'module': 'ALL', + 'golden': False + } + }, + 'validate': False, + 'ip_address': '172.22.150.102', + 'policy_changed': False + }, + etc... + ] + + Callers: + - self.handle_merged_state + """ + if len(devices) == 0: + return + install_options = NdfcImageInstallOptions(self.module) + # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests + self.switch_details.refresh() + for device in devices: + self.log_msg(f"REMOVE: {self.class_name}._verify_install_options: device: {device}") + self.switch_details.ip_address = device.get("ip_address") + install_options.serial_number = self.switch_details.serial_number + install_options.policy_name = device["policy"] + install_options.epld = device["upgrade"]["epld"] + install_options.issu = device["upgrade"]["nxos"] + install_options.refresh() + if install_options.status not in ["Success", "Skipped"] and device["upgrade"]["nxos"] is True: + msg = f"NXOS upgrade is set to True for switch " + msg += f"{device['ip_address']}, but the image policy " + msg += f"{install_options.policy_name} does not contain an " + msg += f"NX-OS image" + self.module.fail_json(msg) + if install_options.epld_modules is None and device["upgrade"]["epld"] is True: + msg = f"EPLD upgrade is set to True for switch " + msg += f"{device['ip_address']}, but the image policy " + msg += f"{install_options.policy_name} does not contain an " + msg += f"EPLD image." + self.module.fail_json(msg) + + def _upgrade_images(self, devices): + """ + Upgrade the switch(es) to the specified image + + Callers: + - handle_merged_state + """ + self.log_msg(f"REMOVE: {self.class_name}._upgrade_images: devices: {devices}") + upgrade = NdfcImageUpgrade(self.module) + upgrade.devices = devices + # TODO:2 Discuss with Mike/Shangxin. Upgrade option handling mutex options. + # I'm leaning toward doing this in NdfcImageUpgrade().validate_options() + # which would cover the various scenarios and fail_json() on invalid + # combinations. + # For epld upgrade disrutive must be True and non_disruptive must be False + # upgrade.epld_upgrade = True + # upgrade.disruptive = True + # upgrade.non_disruptive = False + # upgrade.epld_module = "ALL" + upgrade.commit() + + def handle_merged_state(self): + """ + Update the switch policy if it has changed. + Stage the image if requested. + Validate the image if requested. + Upgrade the image if requested. + + Caller: main() + """ + # TODO:1 Replace these with NdfcImagePolicyAction + # See commented code below + self._build_policy_attach_payload() + self._send_policy_attach_payload() + + # Use (or not) below for policy attach/detach + # instance = NdfcImagePolicyAction(self.module) + # instance.policy_name = "NR3F" + # instance.action = "attach" # or detach + # instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] + # instance.commit() + # policy_attach_devices = [] + # policy_detach_devices = [] + + stage_devices = [] + validate_devices = [] + upgrade_devices = [] + + # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests + self.switch_details.refresh() + for switch in self.need: + msg = f"REMOVE: {self.class_name}.handle_merged_state: switch: {switch}" + self.log_msg(msg) + self.switch_details.ip_address = switch.get("ip_address") + device = {} + device["serial_number"] = self.switch_details.serial_number + self.have.ip_address = self.switch_details.ip_address + device["policy_name"] = switch.get("policy") + device["ip_address"] = self.switch_details.ip_address + if switch.get("stage") is not False: + stage_devices.append(device["serial_number"]) + if switch.get("validate") is not False: + validate_devices.append(device["serial_number"]) + if ( + switch.get("upgrade").get("nxos") is not False or + switch.get("upgrade").get("epld") is not False): + upgrade_devices.append(switch) + + msg = f"REMOVE: {self.class_name}.handle_merged_state: stage_devices: {stage_devices}" + self.log_msg(msg) + self._stage_images(stage_devices) + + self.log_msg( + f"REMOVE: {self.class_name}.handle_merged_state: validate_devices: {validate_devices}" + ) + self._validate_images(validate_devices) + + self.log_msg( + f"REMOVE: {self.class_name}.handle_merged_state: upgrade_devices: {upgrade_devices}" + ) + self._verify_install_options(upgrade_devices) + self._upgrade_images(upgrade_devices) + + def handle_deleted_state(self): + """ + Delete the image policy from the switch(es) + + Caller: main() + """ + msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + msg += f"Entered with self.need {self.need}" + self.log_msg(msg) + detach_policy_devices = {} + # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests + self.switch_details.refresh() + self.image_policies.refresh() + for switch in self.need: + self.switch_details.ip_address = switch.get("ip_address") + self.image_policies.policy_name = switch.get("policy") + # if self.image_policies.name is None: + # continue + if self.image_policies.name not in detach_policy_devices: + detach_policy_devices[self.image_policies.policy_name] = [] + detach_policy_devices[self.image_policies.policy_name].append( + self.switch_details.serial_number + ) + msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + msg += f"detach_policy_devices: {detach_policy_devices}" + self.log_msg(msg) + + if len(detach_policy_devices) == 0: + self.result = dict(changed=False, diff=[], response=[]) + return + instance = NdfcImagePolicyAction(self.module) + for policy_name in detach_policy_devices: + msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + msg += f"detach policy_name: {policy_name}" + msg += f" from devices: {detach_policy_devices[policy_name]}" + self.log_msg(msg) + instance.policy_name = policy_name + instance.action = "detach" + instance.serial_numbers = detach_policy_devices[policy_name] + instance.commit() + + def handle_query_state(self): + """ + Return the ISSU state of the switch(es) listed in the playbook + + Caller: main() + """ + instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + instance.refresh() + msg = f"REMOVE: {self.class_name}.handle_query_state: " + msg += f"Entered. self.need {self.need}" + self.log_msg(msg) + query_devices = [] + for switch in self.need: + instance.ip_address = switch.get("ip_address") + if instance.filtered_data is None: + continue + query_devices.append(instance.filtered_data) + msg = f"REMOVE: {self.class_name}.handle_query_state: " + msg += f"query_policies: {query_devices}" + self.log_msg(msg) + self.result["response"] = query_devices + self.result["diff"] = [] + self.result["changed"] = False + + + def _failure(self, resp): + """ + Caller: self.attach_policies() + + This came from dcnm_inventory.py, but doesn't seem to be correct + for the case where resp["DATA"] does not exist? + + If resp["DATA"] does not exist, the contents of the + if block don't seem to actually do anything: + - data will be None + - Hence, data.get("stackTrace") will also be None + - Hence, data.update() and res.update() are never executed + + So, the only two lines that will actually ever be executed are + the happy path: + + res = copy.deepcopy(resp) + self.module.fail_json(msg=res) + """ + res = copy.deepcopy(resp) + + if not resp.get("DATA"): + data = copy.deepcopy(resp.get("DATA")) + if data.get("stackTrace"): + data.update( + {"stackTrace": "Stack trace is hidden, use '-vvvvv' to print it"} + ) + res.update({"DATA": data}) + + self.module.fail_json(msg=res) + + +class NdfcSwitchDetails(NdfcAnsibleImageUpgradeCommon): + """ + Retrieve switch details from NDFC and provide property accessors + for the switch attributes. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchDetails(module) + instance.ip_address = 10.1.1.1 + fabric_name = instance.fabric_name + serial_number = instance.serial_number + etc... + + Switch details are retrieved on instantiation of this class. + Switch details can be refreshed by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + # self.refresh() + + def _init_properties(self): + self.properties = {} + self.properties["ip_address"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh switch_details with current switch details from NDFC + """ + path = self.endpoints.switches_info.get("path") + verb = self.endpoints.switches_info.get("verb") + self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") + self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response_1 {self.ndfc_response}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response_2 {self.ndfc_response}" + self.log_msg(msg) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + if self.ndfc_response["RETURN_CODE"] != 200: + msg = "Unable to retrieve switch information from NDFC. " + msg += f"Got response {self.ndfc_response}" + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA") + self.properties["ndfc_data"] = {} + for switch in data: + self.properties["ndfc_data"][switch["ipAddress"]] = switch + + def _get(self, item): + if self.ip_address is None: + msg = f"{self.class_name}: set instance.ip_address " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + return self.properties["ndfc_data"][self.ip_address].get(item) + + @property + def ip_address(self): + """ + Set the ip_address of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("ip_address") + + @ip_address.setter + def ip_address(self, value): + self.properties["ip_address"] = value + + @property + def fabric_name(self): + """ + Return the fabricName of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("fabricName") + + @property + def hostname(self): + """ + Return the hostName of the switch with ip_address, if it exists. + Return None otherwise + + NOTES: + 1. This is None for 12.1.2e + 2. Better to use logical_name which is populated in both 12.1.2e and 12.1.3b + """ + return self._get("hostName") + + @property + def logical_name(self): + """ + Return the logicalName of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("logicalName") + + @property + def model(self): + """ + Return the model of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("model") + + @property + def ndfc_data(self): + """ + Return the parsed data from the GET request. + Return None otherwise + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the GET request. + Return None otherwise + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result of the GET request. + Return None otherwise + """ + return self.properties["ndfc_result"] + + @property + def platform(self): + """ + Return the platform of the switch with ip_address, if it exists. + Return None otherwise + """ + model = self._get("model") + if model is None: + return None + return model.split("-")[0] + + @property + def role(self): + """ + Return the switchRole of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("switchRole") + + @property + def serial_number(self): + """ + Return the serialNumber of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("serialNumber") + + +class NdfcImageInstallOptions(NdfcAnsibleImageUpgradeCommon): + """ + Retrieve install-options details for ONE switch from NDFC and + provide property accessors for the policy attributes. + + Caveats: + - This retrieves for a SINGLE switch only. + - Set serial_number and policy_name and call refresh() for + each switch separately. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImageInstallOptions(module) + # Mandatory + instance.policy_name = "NR3F" + instance.serial_number = "FDO211218GC" + # Optional + instance.epld = True + instance.package_install = True + instance.issu = True + # Retrieve install-options details from NDFC + instance.refresh() + if instance.device_name is None: + print("Cannot retrieve policy/serial_number combination from NDFC") + exit(1) + status = instance.status + platform = instance.platform + etc... + + install-options are retrieved by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options + Request body: + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + }, + { + "serialNumber": "FDO211218GC", + "policyName": "NR3F" + } + ], + "issu": true, + "epld": false, + "packageInstall": false + } + Response body: + NOTES: + 1. epldModules will be null if epld is false in the request body. + This class converts this to None (python NoneType) in this case. + + { + "compatibilityStatusList": [ + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "platform": "N9K/N3K", + "version": "10.2.5", + "osType": "64bit", + "status": "Skipped", + "installOption": "NA", + "compDisp": "Compatibility status skipped.", + "versionCheck": "Compatibility status skipped.", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "timestamp": "NA" + } + ], + "epldModules": { + "moduleList": [ + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "IO FPGA", + "oldVersion": "0x15", + "newVersion": "0x15" + }, + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "MI FPGA", + "oldVersion": "0x4", + "newVersion": "0x04" + } + ], + "bException": false, + "exceptionReason": null + }, + "installPacakges": null, + "errMessage": "" + } + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["epld"] = False + self.properties["issu"] = True + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["package_install"] = False + self.properties["policy_name"] = None + self.properties["serial_number"] = None + self.properties["epld_modules"] = None + + def refresh(self): + """ + Refresh self.data with current install-options from NDFC + """ + if self.policy_name is None: + msg = f"{self.class_name}.refresh: " + msg += "instance.policy_name must be set before " + msg += "calling refresh()" + self.module.fail_json(msg) + if self.serial_number is None: + msg = f"{self.class_name}.refresh: " + msg += f"instance.serial_number must be set before " + msg += f"calling refresh()" + self.module.fail_json(msg) + + path = self.endpoints.install_options.get("path") + verb = self.endpoints.install_options.get("verb") + self._build_payload() + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"payload: {self.payload}" + self.log_msg(msg) + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"ndfc_response: {self.ndfc_response}" + self.log_msg(msg) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + # TODO:2 should error message contain full response or just DATA.error? + if self.ndfc_result["success"] is False: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retrieving install-options from NDFC. " + msg += f"NDFC response: {self.ndfc_response}" + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.data = self.properties["ndfc_data"] + if self.data.get("compatibilityStatusList") is None: + self.compatibility_status = {} + else: + self.compatibility_status = self.data.get("compatibilityStatusList")[0] + + def _build_payload(self): + """ + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + } + ], + "issu": true, + "epld": false, + "packageInstall": false + } + """ + self.payload = {} + self.payload["devices"] = [] + devices = {} + devices["serialNumber"] = self.serial_number + devices["policyName"] = self.policy_name + self.payload["devices"].append(devices) + self.payload["issu"] = self.issu + self.payload["epld"] = self.epld + self.payload["packageInstall"] = self.package_install + + def _get(self, item): + return self.data.get(item) + + # Mandatory properties + @property + def policy_name(self): + """ + Set the policy_name of the policy to query. + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def serial_number(self): + """ + Set the serial_number of the device to query. + """ + return self.properties.get("serial_number") + + @serial_number.setter + def serial_number(self, value): + self.properties["serial_number"] = value + + # Optional properties + @property + def issu(self): + """ + Enable (True) or disable (False) issu compatibility check. + Valid values: + True - Enable issu compatibility check + False - Disable issu compatibility check + Default: True + """ + return self.properties.get("issu") + + @issu.setter + def issu(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.issu.setter: " + msg += "issu must be a boolean value" + self.module.fail_json(msg) + self.properties["issu"] = value + + @property + def epld(self): + """ + Enable (True) or disable (False) epld compatibility check. + + Valid values: + True - Enable epld compatibility check + False - Disable epld compatibility check + Default: False + """ + return self.properties.get("epld") + + @epld.setter + def epld(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.epld.setter: " + msg += "epld must be a boolean value" + self.module.fail_json(msg) + self.properties["epld"] = value + + @property + def package_install(self): + """ + Enable (True) or disable (False) package_install compatibility check. + Valid values: + True - Enable package_install compatibility check + False - Disable package_install compatibility check + Default: False + """ + return self.properties.get("package_install") + + @package_install.setter + def package_install(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.package_install.setter: " + msg += "package_install must be a boolean value" + self.module.fail_json(msg) + self.properties["package_install"] = value + + # Retrievable properties + @property + def comp_disp(self): + """ + Return the compDisp (CLI output from show install all status) + of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("compDisp") + + @property + def device_name(self): + """ + Return the deviceName of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("deviceName") + + @property + def epld_modules(self): + """ + Return the epldModules of the install-options response, + if it exists. + Return None otherwise + + epldModules will be "null" if self.epld is False, so + make_none() will convert to NoneType in this case. + """ + return self.make_none(self._get("epldModules")) + + @property + def err_message(self): + """ + Return the errMessage of the install-options response, + if it exists. + Return None otherwise + + epldModules will be "null" if self.epld is False, so + make_none() will convert to NoneType in this case. + """ + return self._get("errMessage") + + @property + def install_option(self): + """ + Return the installOption of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("installOption") + + @property + def install_packages(self): + """ + Return the installPackages of the install-options response, + if it exists. + Return None otherwise + + NOTE: yes, installPacakges is misspelled in the response in the + following versions (at least): + 12.1.2e + 12.1.3b + """ + return self.make_none(self._get("installPacakges")) + + @property + def ip_address(self): + """ + Return the ipAddress of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("ipAddress") + + @property + def ndfc_data(self): + """ + Return the raw data from the NDFC response. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_response(self): + """ + Return the response from NDFC of the query. + """ + return self.properties.get("ndfc_response") + + @property + def ndfc_result(self): + """ + Return the result from NDFC of the query. + """ + return self.properties.get("ndfc_result") + + @property + def os_type(self): + """ + Return the osType of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("osType") + + @property + def platform(self): + """ + Return the platform of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("platform") + + @property + def pre_issu_link(self): + """ + Return the preIssuLink of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("preIssuLink") + + @property + def raw_data(self): + """ + Return the raw data of the install-options response, if it exists. + """ + return self.data + + @property + def raw_response(self): + """ + Return the raw response, if it exists. + """ + return self.response + + @property + def rep_status(self): + """ + Return the repStatus of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("repStatus") + + @property + def status(self): + """ + Return the status of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("status") + + @property + def timestamp(self): + """ + Return the timestamp of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("timestamp") + + @property + def version(self): + """ + Return the version of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("version") + + @property + def version_check(self): + """ + Return the versionCheck (version check CLI output) + of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("versionCheck") + + +# ============================================================================== +class NdfcImagePolicyAction(NdfcAnsibleImageUpgradeCommon): + """ + Perform image policy actions on NDFC on one or more switches. + + Support for the following actions: + - attach + - detach + - query + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImagePolicyAction(module) + instance.policy_name = "NR3F" + instance.action = "attach" # or detach, or query + instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] + instance.commit() + # for query only + query_result = instance.query_result + + Endpoints: + For action == attach: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy + For action == detach: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy + For action == query: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + self.image_policies = NdfcImagePolicies(self.module) + self.switch_issu_details = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.valid_actions = {"attach", "detach", "query"} + + def _init_properties(self): + self.properties = {} + self.properties["action"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["policy_name"] = None + self.properties["query_result"] = None + self.properties["serial_numbers"] = None + + def build_attach_payload(self): + """ + build the payload to send in the POST request + to attach policies to devices + + caller _attach_policy() + """ + self.payloads = [] + # TODO:2 this results in more calls than necessary to NDFC. Need a better way to handle this. + self.switch_issu_details.refresh() + for serial_number in self.serial_numbers: + self.switch_issu_details.serial_number = serial_number + payload = {} + payload["policyName"] = self.policy_name + payload["hostName"] = self.switch_issu_details.device_name + payload["ipAddr"] = self.switch_issu_details.ip_address + payload["platform"] = self.switch_issu_details.platform + payload["serialNumber"] = self.switch_issu_details.serial_number + for item in payload: + if payload[item] is None: + msg = f"Unable to determine {item} for switch " + msg += f"{self.switch_issu_details.ip_address}, " + msg += f"{self.switch_issu_details.serial_number}, " + msg += f"{self.switch_issu_details.device_name}. " + msg += "Please verify that the switch is managed by NDFC." + self.module.fail_json(msg) + self.payloads.append(payload) + + def validate_request(self): + """ + validations prior to commit() should be added here. + """ + self.log_msg(f"REMOVE: {self.class_name}.validate_request: Entered") + if self.action is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.action must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + if self.policy_name is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.policy_name must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + self.log_msg(f"REMOVE: {self.class_name}.validate_request: action {self.action}") + + if self.action == "query": + return + + self.log_msg(f"REMOVE: {self.class_name}.validate_request: serial_numbers {self.serial_numbers}") + + if self.serial_numbers is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.serial_numbers must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + + # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it + self.image_policies.refresh() + self.switch_issu_details.refresh() + # Fail if the image policy does not support the switch platform + self.image_policies.policy_name = self.policy_name + for serial_number in self.serial_numbers: + self.switch_issu_details.serial_number = serial_number + if self.switch_issu_details.platform not in self.image_policies.platform: + msg = f"policy {self.policy_name} does not support platform " + msg += f"{self.switch_issu_details.platform}. {self.policy_name} " + msg += "supports the following platform(s): " + msg += f"{self.image_policies.platform}" + self.module.fail_json(msg) + + def commit(self): + self.validate_request() + if self.action == "attach": + self._attach_policy() + elif self.action == "detach": + self._detach_policy() + elif self.action == "query": + self._query_policy() + else: + msg = f"{self.class_name}.commit: " + msg += f"Unknown action {self.action}." + self.module.fail_json(msg) + + def _attach_policy(self): + """ + Attach policy_name to the switch(es) associated with serial_numbers + + NOTES: + 1. This method creates a list of responses and results which + are accessible via properties ndfc_response and ndfc_result, + respectively. + """ + self.build_attach_payload() + path = self.endpoints.policy_attach.get("path") + verb = self.endpoints.policy_attach.get("verb") + responses = [] + results = [] + for payload in self.payloads: + response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) + result = self._handle_response(response, verb) + if not result["success"]: + msg = f"{self.class_name}._attach_policy: " + msg += f"Bad result when attaching policy {self.policy_name} " + msg += f"to switch {payload['ipAddr']}." + self.module.fail_json(msg) + responses.append(response) + results.append(result) + self.properties["ndfc_response"] = responses + self.properties["ndfc_result"] = results + + def _detach_policy(self): + """ + Detach policy_name from the switch(es) associated with serial_numbers + verb: DELETE + endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy + query_params: ?serialNumber=FDO211218GC,FDO21120U5D + """ + path = self.endpoints.policy_detach.get("path") + verb = self.endpoints.policy_detach.get("verb") + query_params = ",".join(self.serial_numbers) + path += f"?serialNumber={query_params}" + response = dcnm_send(self.module, verb, path) + result = self._handle_response(response, verb) + if not result["success"]: + self._failure(response) + self.properties["ndfc_response"] = response + self.properties["ndfc_result"] = result + + def _query_policy(self): + """ + Query the image policy + verb: GET + endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ + """ + path = self.endpoints.policy_info.get("path") + verb = self.endpoints.policy_info.get("verb") + path = path.replace("__POLICY_NAME__", self.policy_name) + response = dcnm_send(self.module, verb, path) + result = self._handle_response(response, verb) + if not result["success"]: + self._failure(response) + self.properties["query_result"] = response.get("DATA") + self.properties["ndfc_response"] = response + self.properties["ndfc_result"] = result + + @property + def query_result(self): + """ + Return the value of properties["query_result"]. + """ + return self.properties.get("query_result") + + @property + def action(self): + """ + Set the action to take. Either "attach" or "detach". + + Must be set prior to calling instance.commit() + """ + return self.properties.get("action") + + @action.setter + def action(self, value): + if value not in self.valid_actions: + msg = f"{self.class_name}: instance.action must be " + msg += f"one of {','.join(sorted(self.valid_actions))}" + self.module.fail_json(msg) + self.properties["action"] = value + + @property + def ndfc_response(self): + """ + Return the raw response from NDFC after calling commit(). + + In the case of attach, this is a list of responses. + """ + return self.properties.get("ndfc_response") + + @property + def ndfc_result(self): + """ + Return the raw result from NDFC after calling commit(). + + In the case of attach, this is a list of results. + """ + return self.properties.get("ndfc_result") + + @property + def policy_name(self): + """ + Set the name of the policy to attach, detach, query. + + Must be set prior to calling instance.commit() + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to/from which + policy_name will be attached or detached. + + Must be set prior to calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.class_name}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + +# ============================================================================== + + +class NdfcImagePolicies(NdfcAnsibleImageUpgradeCommon): + """ + Retrieve image policy details from NDFC and provide property accessors + for the policy attributes. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImagePolicies(module).refresh() + instance.policy_name = "NR3F" + if instance.name is None: + print("policy NR3F does not exist on NDFC") + exit(1) + policy_name = instance.name + platform = instance.platform + epd_image_name = instance.epld_image_name + etc... + + Policies are retrieved on instantiation of this class. + Policies can be refreshed by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.log_msg(f"{self.class_name}.__init__ entered") + self._init_properties() + # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it + #self.refresh() + + def _init_properties(self): + self.properties = {} + self.properties["policy_name"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + + def refresh(self): + """ + Refresh self.image_policies with current image policies from NDFC + """ + path = self.endpoints.policies_info.get("path") + verb = self.endpoints.policies_info.get("verb") + + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"result: {self.ndfc_result}" + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retriving image policy " + msg += "information from NDFC." + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA").get("lastOperDataObject") + if data is None: + msg = f"{self.class_name}.refresh: " + msg += "Bad response when retrieving image policy " + msg += "information from NDFC." + self.module.fail_json(msg) + if len(data) == 0: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no defined image policies." + self.module.fail_json(msg) + self.properties["ndfc_data"] = {} + for policy in data: + policy_name = policy.get("policyName") + if policy_name is None: + msg = f"{self.class_name}.refresh: " + msg += "Cannot parse NDFC policy information" + self.module.fail_json(msg) + self.properties["ndfc_data"][policy_name] = policy + + def _get(self, item): + if self.policy_name is None: + msg = f"{self.class_name}._get: " + msg += f"instance.policy_name must be set before " + msg += f"accessing property {item}." + self.module.fail_json(msg) + if self.properties['ndfc_data'].get(self.policy_name) is None: + msg = f"{self.class_name}._get: " + msg += f"policy_name {self.policy_name} is not defined in NDFC" + self.module.fail_json(msg) + return_item = self.make_boolean(self.properties["ndfc_data"][self.policy_name].get(item)) + return_item = self.make_none(return_item) + return return_item + + @property + def description(self): + """ + Return the policyDescr of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyDescr") + + @property + def epld_image_name(self): + """ + Return the epldImgName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("epldImgName") + + @property + def name(self): + """ + Return the name of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyName") + + @property + def ndfc_data(self): + """ + Return the parsed data from the NDFC response as a dictionary, + keyed on policy_name. + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the NDFC response. + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result from the NDFC response. + """ + return self.properties["ndfc_result"] + + @property + def policy_name(self): + """ + Set the name of the policy to query. + + This must be set prior to accessing any other properties + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def policy_type(self): + """ + Return the policyType of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyType") + + @property + def nxos_version(self): + """ + Return the nxosVersion of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("nxosVersion") + + @property + def package_name(self): + """ + Return the packageName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("packageName") + + @property + def platform(self): + """ + Return the platform of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("platform") + + @property + def platform_policies(self): + """ + Return the platformPolicies of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("platformPolicies") + + @property + def ref_count(self): + """ + Return the reference count of the policy matching self.policy_name, + if it exists. The reference count is the number of switches using + this policy. + Return None otherwise + """ + return self._get("ref_count") + + @property + def rpm_images(self): + """ + Return the rpmimages of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("rpmimages") + + @property + def image_name(self): + """ + Return the imageName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("imageName") + + @property + def agnostic(self): + """ + Return the value of agnostic for the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("agnostic") + + +class NdfcSwitchIssuDetails(NdfcAnsibleImageUpgradeCommon): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes. + + Usage: See subclasses. + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu + + Response body: + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO211218GC", + "deviceName": "cvd-1312-leaf", + "fabric": "fff", + "version": "10.3(2)", + "policy": "NR3F", + "status": "In-Sync", + "reason": "Compliance", + "imageStaged": "Success", + "validated": "None", + "upgrade": "None", + "upgGroups": "None", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": null, + "vpcPeer": null, + "role": "leaf", + "lastUpgAction": "Never", + "model": "N9K-C93180YC-EX", + "ipAddress": "172.22.150.103", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 8430, + "platform": "N9K", + "vpc_role": null, + "ip_address": "172.22.150.103", + "peer": null, + "vdc_id": -1, + "sys_name": "cvd-1312-leaf", + "id": 3, + "group": "fff", + "fcoEEnabled": false, + "mds": false + }, + {etc...} + ] + + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_data"] = None + # action_keys is used in subclasses to determine if any actions + # are in progress. + # Property actions_in_progress returns True if so, False otherwise + self.properties["action_keys"] = set() + self.properties["action_keys"].add("imageStaged") + self.properties["action_keys"].add("upgrade") + self.properties["action_keys"].add("validated") + + + def refresh(self) -> None: + """ + Refresh current issu details from NDFC + """ + path = self.endpoints.issu_info.get("path") + verb = self.endpoints.issu_info.get("verb") + + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"result: {self.ndfc_result}" + # ndfc_result for 404 response + # {'found': False, 'success': True} + if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retriving switch " + msg += "information from NDFC" + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA").get("lastOperDataObject") + if data is None: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no switch ISSU information." + self.module.fail_json(msg) + if len(data) == 0: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no switch ISSU information." + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA", {}).get( + "lastOperDataObject", [] + ) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + @property + def actions_in_progress(self): + """ + Return True if any actions are in progress + Return False otherwise + """ + for action_key in self.properties["action_keys"]: + if self._get(action_key) == "In-Progress": + return True + return False + + def _get(self, item): + """ + overridden in subclasses + """ + pass + + @property + def ndfc_data(self): + """ + Return the raw data retrieved from NDFC + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the GET request. + Return None otherwise + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result of the GET request. + Return None otherwise + """ + return self.properties["ndfc_result"] + + @property + def device_name(self): + """ + Return the deviceName of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + device name, e.g. "cvd-1312-leaf" + None + """ + return self._get("deviceName") + + @property + def eth_switch_id(self): + """ + Return the ethswitchid of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + integer + None + """ + return self._get("ethswitchid") + + @property + def fabric(self): + """ + Return the fabric of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + fabric name, e.g. "myfabric" + None + """ + return self._get("fabric") + + @property + def fcoe_enabled(self): + """ + Return whether FCOE is enabled on the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + boolean (true/false) + None + """ + return self.make_boolean(self._get("fcoEEnabled")) + + @property + def group(self): + """ + Return the group of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + group name, e.g. "mygroup" + None + """ + return self._get("group") + + @property + # id is a python keyword, so we can't use it as a property name + # so we use switch_id instead + def switch_id(self): + """ + Return the switch ID of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("id") + + @property + def image_staged(self): + """ + Return the imageStaged of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Success + Failed + None + """ + return self._get("imageStaged") + + @property + def image_staged_percent(self): + """ + Return the imageStagedPercent of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("imageStagedPercent") + + @property + def ip_address(self): + """ + Return the ipAddress of the switch, if it exists. + Return None otherwise + + Possible values: + switch IP address + None + """ + return self._get("ipAddress") + + @property + def issu_allowed(self): + """ + Return the issuAllowed value of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + "" + None + """ + return self._get("issuAllowed") + + @property + def last_upg_action(self): + """ + Return the last upgrade action performed on the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + Never + None + """ + return self._get("lastUpgAction") + + @property + def mds(self): + """ + Return whether the switch with ip_address is an MSD, if it exists. + Return None otherwise + + Possible values: + Boolean (True or False) + None + """ + return self.make_boolean(self._get("mds")) + + @property + def mode(self): + """ + Return the ISSU mode of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + "Normal" + None + """ + return self._get("mode") + + @property + def model(self): + """ + Return the model of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + model number e.g. "N9K-C93180YC-EX" + None + """ + return self._get("model") + + @property + def model_type(self): + """ + Return the model type of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("modelType") + + @property + def peer(self): + """ + Return the peer of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + None + """ + return self._get("peer") + + @property + def platform(self): + """ + Return the platform of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + platform, e.g. "N9K" + None + """ + return self._get("platform") + + @property + def policy(self): + """ + Return the policy attached to the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + policy name, e.g. "NR3F" + None + """ + return self._get("policy") + + @property + def reason(self): + """ + Return the reason (?) of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Compliance + Validate + Upgrade + None + """ + return self._get("reason") + + @property + def role(self): + """ + Return the role of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + switch role, e.g. "leaf" + None + """ + return self._get("role") + + @property + def serial_number(self): + """ + Return the serialNumber of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + switch serial number, e.g. "AB1234567CD" + None + """ + return self._get("serialNumber") + + @property + def status(self): + """ + Return the sync status of the switch with ip_address, if it exists. + Return None otherwise + + Details: The sync status is the status of the switch with respect + to the image policy. If the switch is in sync with the image policy, + the status is "In-Sync". If the switch is out of sync with the image + policy, the status is "Out-Of-Sync". + + Possible values: + "In-Sync" + "Out-Of-Sync" + None + """ + return self._get("status") + + @property + def status_percent(self): + """ + Return the upgrade (TODO:3 verify this) percentage completion + of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("statusPercent") + + @property + def sys_name(self): + """ + Return the system name of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + system name, e.g. "cvd-1312-leaf" + None + """ + return self._get("sys_name") + + @property + def system_mode(self): + """ + Return the system mode of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + "Maintenance" (TODO:3 verify this) + "Normal" + None + """ + return self._get("systemMode") + + @property + def upgrade(self): + """ + Return the upgrade status of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + Success + In-Progress + None + """ + return self._get("upgrade") + + @property + def upg_groups(self): + """ + Return the upgGroups (upgrade groups) of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + upgrade group to which the switch belongs e.g. "LEAFS" + None + """ + return self._get("upgGroups") + + @property + def upgrade_percent(self): + """ + Return the upgrade percent complete of the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("upgradePercent") + + @property + def validated(self): + """ + Return the validation status of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + Failed + Success + None + """ + return self._get("validated") + + @property + def validated_percent(self): + """ + Return the validation percent complete of the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("validatedPercent") + + @property + def vdc_id(self): + """ + Return the vdcId of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("vdcId") + + @property + def vdc_id2(self): + """ + Return the vdc_id of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer (negative values are valid) + None + """ + return self._get("vdc_id") + + @property + def version(self): + """ + Return the version of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + version, e.g. "10.3(2)" + None + """ + return self._get("version") + + @property + def vpc_peer(self): + """ + Return the vpcPeer of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + vpc peer e.g.: 10.1.1.1 + None + """ + return self._get("vpcPeer") + + @property + def vpc_role(self): + """ + Return the vpcRole of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + vpc role e.g.: + "primary" + "secondary" + "none" -> This will be translated to None + "none established" (TODO:3 verify this) + "primary, operational secondary" (TODO:3 verify this) + None + """ + return self._get("vpcRole") + + +class NdfcSwitchIssuDetailsByIpAddress(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by ip address. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsByIpAddress(module) + instance.refresh() + instance.ip_address = 10.1.1.1 + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + serial_number = instance.serial_number + etc... + + See NdfcSwitchIssuDetails for more details. + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["ip_address"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh ip_address current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + msg = f"{self.class_name}.refresh: " + msg += f"switch {switch}" + self.data_subclass[switch["ipAddress"]] = switch + + def _get(self, item): + if self.ip_address is None: + msg = f"{self.class_name}: set instance.ip_address " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.ip_address) is None: + msg = f"{self.class_name}: {self.ip_address} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.ip_address].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.ip_address. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.ip_address) + + @property + def ip_address(self): + """ + Set the ip_address of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("ip_address") + + @ip_address.setter + def ip_address(self, value): + self.properties["ip_address"] = value + + +class NdfcSwitchIssuDetailsBySerialNumber(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by serial_number. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsBySerialNumber(module) + instance.refresh() + instance.serial_number = "FDO211218GC" + instance.refresh() + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + ip_address = instance.ip_address + etc... + + See NdfcSwitchIssuDetails for more details. + + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["serial_number"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh serial_number current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + self.data_subclass[switch["serialNumber"]] = switch + + def _get(self, item): + if self.serial_number is None: + msg = f"{self.class_name}: set instance.serial_number " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.serial_number) is None: + msg = f"{self.class_name}: {self.serial_number} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.serial_number].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.serial_number. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.serial_number) + + @property + def serial_number(self): + """ + Set the serial_number of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("serial_number") + + @serial_number.setter + def serial_number(self, value): + self.properties["serial_number"] = value + + +class NdfcSwitchIssuDetailsByDeviceName(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by device_name. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsByDeviceName(module) + instance.refresh() + instance.device_name = "leaf_1" + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + ip_address = instance.ip_address + etc... + + See NdfcSwitchIssuDetails for more details. + + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["device_name"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh device_name current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + self.data_subclass[switch["deviceName"]] = switch + + def _get(self, item): + if self.device_name is None: + msg = f"{self.class_name}: set instance.device_name " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.device_name) is None: + msg = f"{self.class_name}: {self.device_name} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.device_name].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.device_name. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.device_name) + + @property + def device_name(self): + """ + Set the device_name of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("device_name") + + @device_name.setter + def device_name(self, value): + self.properties["device_name"] = value + + +class NdfcImageStage(NdfcAnsibleImageUpgradeCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image + + Verb: POST + + Usage (where module is an instance of AnsibleModule): + + stage = NdfcImageStage(module) + stage.serial_numbers = ["FDO211218HH", "FDO211218GC"] + stage.commit() + data = stage.data + + Request body (12.1.2e) (yes, serialNum is misspelled): + { + "sereialNum": [ + "FDO211218HH", + "FDO211218GC" + ] + } + Request body (12.1.3b): + { + "serialNumbers": [ + "FDO211218HH", + "FDO211218GC" + ] + } + + Response: + Unfortunately, the response does not contain consistent data. + Would be better if all responses contained serial numbers as keys so that + we could verify against a set() of serial numbers. Sigh. It is what it is. + { + 'RETURN_CODE': 200, + 'METHOD': 'POST', + 'REQUEST_PATH': 'https: //172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image', + 'MESSAGE': 'OK', + 'DATA': [ + { + 'key': 'success', + 'value': '' + }, + { + 'key': 'success', + 'value': '' + } + ] + } + + Response when there are no files to stage: + [ + { + "key": "FDO211218GC", + "value": "No files to stage" + }, + { + "key": "FDO211218HH", + "value": "No files to stage" + } + ] + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + self.serial_numbers_done = set() + self.ndfc_version = None + self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + + def _init_properties(self): + self.properties = {} + self.properties["serial_numbers"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + + def _populate_ndfc_version(self): + """ + Populate self.ndfc_version with the NDFC version. + + Notes: + 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + imports resulting in RecursionError + """ + instance = NdfcVersion(self.module) + instance.refresh() + self.ndfc_version = instance.version + + def prune_serial_numbers(self): + """ + If the image is already staged on a switch, remove that switch's + serial number from the list of serial numbers to stage. + """ + serial_numbers = copy.copy(self.serial_numbers) + for serial_number in serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.image_staged == "Success": + msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " + msg += "image already staged for " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}." + self.log_msg(msg) + self.serial_numbers.remove(serial_number) + + def validate_serial_numbers(self): + """ + Fail if the image_staged state for any serial_number + is Failed. + """ + for serial_number in self.serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.image_staged == "Failed": + msg = "Image staging is failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += f"Check the switch connectivity to NDFC " + msg += "and try again." + self.module.fail_json(msg) + else: + self.log_msg(f"Image staging is not failing for the following switch: {self.issu_detail.serial_number}") + + def commit(self): + """ + Commit the image staging request to NDFC and wait + for the images to be staged. + """ + if self.serial_numbers is None: + msg = f"{self.class_name}.commit() call instance.serial_numbers " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.serial_numbers) == 0: + msg = f"REMOVE: {self.class_name}.commit() no serial numbers to stage." + self.log_msg(msg) + return + self.prune_serial_numbers() + self.validate_serial_numbers() + self._wait_for_current_actions_to_complete() + + path = self.endpoints.image_stage.get("path") + verb = self.endpoints.image_stage.get("verb") + + payload = {} + self._populate_ndfc_version() + if self.ndfc_version == "12.1.2e": + # Yes, NDFC wants serialNum to be misspelled + payload["sereialNum"] = self.serial_numbers + else: + payload["serialNumbers"] = self.serial_numbers + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_stage_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not stage an image if there are any actions in progress. + Wait for all actions to complete before staging image. + Actions include image staging, image upgrade, and image validation. + """ + self.serial_numbers_done = set() + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + timeout = self.check_timeout + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} actions in progress: " + msg += f"{self.issu_detail.actions_in_progress}, " + msg += f"{timeout} seconds remaining." + self.log_msg(msg) + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"Timed out waiting for actions to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + def _wait_for_image_stage_to_complete(self): + """ + # Wait for image stage to complete + """ + # We're promiting serial_numbers_done to a class-level attribute + # so that it can be used in unit test asserts. + self.serial_numbers_done = set() + timeout = self.check_timeout + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" + self.log_msg(msg) + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + staged_percent = self.issu_detail.image_staged_percent + staged_status = self.issu_detail.image_staged + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Seconds remaining {timeout}: " + msg += f"{device_name}, {serial_number}, {ip_address} " + msg += f"image staged percent: {staged_percent}" + self.log_msg(msg) + + if staged_status == "Failed": + msg = f"Seconds remaining {timeout}: stage image failed " + msg += f"for {device_name}, {serial_number}, {ip_address}. " + msg += f"image staged percent: {staged_percent}" + self.module.fail_json(msg) + + if staged_status == "Success": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Seconds remaining {timeout}: stage image complete for " + msg += f"for {device_name}, {serial_number}, {ip_address}. " + msg += f"image staged percent: {staged_percent}" + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + + if staged_status == None: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Seconds remaining {timeout}: stage image " + msg += "not started for " + msg += f"{device_name}, {serial_number}, {ip_address}. " + msg += f"image staged percent: {staged_percent}" + self.log_msg(msg) + + if staged_status == "In Progress": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Seconds remaining {timeout}: stage image " + msg += f"{staged_status} for " + msg += f"{device_name}, {serial_number}, {ip_address}. " + msg += f"image staged percent: {staged_percent}" + self.log_msg(msg) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Timed out waiting for image stage to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to stage. + + This must be set before calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.__class__.__name__}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + + @property + def ndfc_data(self): + """ + Return the result of the image staging request + for serial_numbers. + + instance.serial_numbers must be set first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def check_interval(self): + """ + Return the stage check interval in seconds + """ + return self.properties.get("check_interval") + + @check_interval.setter + def check_interval(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_interval must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_interval"] = value + + @property + def check_timeout(self): + """ + Return the stage check timeout in seconds + """ + return self.properties.get("check_timeout") + + @check_timeout.setter + def check_timeout(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_timeout must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_timeout"] = value + +# ============================================================================== +class NdfcImageValidate(NdfcAnsibleImageUpgradeCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image + + Verb: POST + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImageValidate(module) + instance.serial_numbers = ["FDO211218HH", "FDO211218GC"] + # non_disruptive is optional + instance.non_disruptive = True + instance.commit() + data = instance.ndfc_data + + Request body: + { + "serialNum": ["FDO21120U5D"], + "nonDisruptive":"true" + } + + Response body when nonDisruptive is True: + [StageResponse [key=success, value=]] + + Response body when nonDisruptive is False: + [StageResponse [key=success, value=]] + + The response is not JSON, nor is it very useful. + Instead, we poll for validation status using + NdfcSwitchIssuDetailsBySerialNumber. + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + + def _init_properties(self): + self.properties = {} + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["non_disruptive"] = False + self.properties["serial_numbers"] = None + + def _populate_ndfc_version(self): + """ + Populate self.ndfc_version with the NDFC version. + + TODO:3 Remove if 12.1.3b works with no changes to request/response payloads. + + Notes: + 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + imports resulting in RecursionError + """ + instance = NdfcVersion(self.module) + instance.refresh() + self.ndfc_version = instance.version + + def prune_serial_numbers(self): + """ + If the image is already validated on a switch, remove that switch's + serial number from the list of serial numbers to validate. + """ + serial_numbers = copy.copy(self.serial_numbers) + for serial_number in serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.validated == "Success": + msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " + msg += "image already validated for " + msg += f"{self.issu_detail.serial_number}, " + msg += f"{self.issu_detail.ip_address}" + self.log_msg(msg) + self.serial_numbers.remove(self.issu_detail.serial_number) + + def validate_serial_numbers(self): + """ + Log a warning if the validated state for any serial_number + is Failed. + + TODO:1 Need a way to compare current image_policy with the image policy in the response + TODO:3 If validate == Failed, it may have been from the last operation. + TODO:3 We can't fail here based on this until we can verify the failure is happening for the current image_policy. + TODO:3 Change this to a log message and update the unit test if we can't verify the failure is happening for the current image_policy. + """ + for serial_number in self.serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.validated == "Failed": + msg = f"{self.class_name}.validate_serial_numbers: " + msg += "image validation is failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += "If this persists, check the switch connectivity to NDFC and " + msg += "try again." + #self.log_msg(msg) + self.module.fail_json(msg) + + def build_payload(self): + self.payload = {} + self.payload["serialNum"] = self.serial_numbers + self.payload["nonDisruptive"] = self.non_disruptive + + def commit(self): + """ + Commit the image validation request to NDFC and wait + for the images to be validated. + """ + if self.serial_numbers is None: + msg = f"{self.class_name}.commit() call instance.serial_numbers " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.serial_numbers) == 0: + msg = f"REMOVE: {self.class_name}.commit() no serial numbers " + msg += "to validate." + self.log_msg(msg) + return + self.prune_serial_numbers() + self.validate_serial_numbers() + self._wait_for_current_actions_to_complete() + path = self.endpoints.image_validate.get("path") + verb = self.endpoints.image_validate.get("verb") + self.build_payload() + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_validate_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not validate an image if there are any actions in progress. + Wait for all actions to complete before validating image. + Actions include image staging, image upgrade, and image validation. + """ + self.serial_numbers_done = set() + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + timeout = self.check_timeout + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} actions in progress: " + msg += f"{self.issu_detail.actions_in_progress}, " + msg += f"{timeout} seconds remaining." + self.log_msg(msg) + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"Timed out waiting for actions to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + def _wait_for_image_validate_to_complete(self): + """ + Wait for image validation to complete + """ + # We're promiting serial_numbers_done to a class-level attribute + # so that it can be used in unit test asserts. + self.serial_numbers_done = set() + timeout = self.check_timeout + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" + self.log_msg(msg) + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + validated_percent = self.issu_detail.validated_percent + validated_status = self.issu_detail.validated + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"validated_percent: {validated_percent} " + msg += f"validated_state: {validated_status}" + self.log_msg(msg) + + if validated_status == "Failed": + msg = f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}. " + msg += "Check the switch e.g. show install log detail, " + msg += "show incompatibility-all nxos . Or " + msg += "check NDFC Operations > Image Management > " + msg += "Devices > View Details > Validate for " + msg += "more details." + self.module.fail_json(msg) + + if validated_status == "Success": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + + if validated_status == None: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += "not started for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + + if validated_status == "In Progress": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Timed out waiting for image validation to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to stage. + + This must be set before calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.__class__.__name__}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + + @property + def non_disruptive(self): + """ + Set the non_disruptive flag to True or False. + """ + return self.properties.get("non_disruptive") + + @non_disruptive.setter + def non_disruptive(self, value): + value = self.make_boolean(value) + if not isinstance(value, bool): + msg = f"{self.class_name}.non_disruptive: " + msg += "instance.non_disruptive must " + msg += f"be a boolean. Got {value}." + self.module.fail_json(msg) + self.properties["non_disruptive"] = value + + @property + def ndfc_data(self): + """ + Return the result of the image staging request + for serial_numbers. + + instance.serial_numbers must be set first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def check_interval(self): + """ + Return the validate check interval in seconds + """ + return self.properties.get("check_interval") + + @check_interval.setter + def check_interval(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_interval must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_interval"] = value + + @property + def check_timeout(self): + """ + Return the validate check timeout in seconds + """ + return self.properties.get("check_timeout") + + @check_timeout.setter + def check_timeout(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_timeout must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_timeout"] = value + + +# ============================================================================== +class NdfcImageUpgrade(NdfcAnsibleImageUpgradeCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image + Verb: POST + + TODO:3 Discuss with Mike/Shangxin. NdfcImageUpgrade.epld_upgrade, etc + + Usage (where module is an instance of AnsibleModule): + + upgrade = NdfcImageUpgrade(module) + upgrade.devices = devices + upgrade.commit() + data = upgrade.data + + Where devices is a list of dict. Example structure: + + [ + { + 'policy': 'KR3F', + 'ip_address': '172.22.150.102', + 'policy_changed': False + 'stage': False, + 'validate': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'nxos': { + 'mode': 'non_disruptive' + 'bios_force': False + }, + 'epld': { + 'module': 'ALL', + 'golden': False + }, + 'reboot': { + 'config_reload': False, + 'write_erase': False + }, + 'package': { + 'install': False, + 'uninstall': False + } + }, + }, + etc... + ] + + Request body: + Yes, the keys below are misspelled in the request body: + pacakgeInstall + pacakgeUnInstall + + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + } + ], + "issuUpgrade": true, + "issuUpgradeOptions1": { + "nonDisruptive": true, + "forceNonDisruptive": false, + "disruptive": false + }, + "issuUpgradeOptions2": { + "biosForce": false + }, + "epldUpgrade": false, + "epldOptions": { + "moduleNumber": "ALL", + "golden": false + }, + "reboot": false, + "rebootOptions": { + "configReload": "false", + "writeErase": "false" + }, + "pacakgeInstall": false, + "pacakgeUnInstall": false + } + Response bodies: + Responses are text, not JSON, and are returned immediately. + They do not contain useful information. We need to poll NDFC + to determine when the upgrade is complete. Basically, we ignore + these responses in favor of the poll responses. + - If an action is in progress, text is returned: + "Action in progress for some of selected device(s). Please try again after completing current action." + - If an action is not in progress, text is returned: + "3" + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + # Maximum number of modules/linecards in a switch + self.max_module_number = 9 + + self._init_defaults() + self._init_properties() + self._populate_ndfc_version() + self.issu_detail = NdfcSwitchIssuDetailsByIpAddress(self.module) + self.issu_detail.refresh() + + def _init_defaults(self): + self.defaults = {} + self.defaults["reboot"] = False + self.defaults["stage"] = True + self.defaults["validate"] = True + self.defaults["upgrade"] = {} + self.defaults["upgrade"]["nxos"] = True + self.defaults["upgrade"]["epld"] = False + self.defaults["options"] = {} + self.defaults["options"]["nxos"] = {} + self.defaults["options"]["nxos"]["mode"] = "disruptive" + self.defaults["options"]["nxos"]["bios_force"] = False + self.defaults["options"]["epld"] = {} + self.defaults["options"]["epld"]["module"] = "ALL" + self.defaults["options"]["epld"]["golden"] = False + self.defaults["options"]["reboot"] = {} + self.defaults["options"]["reboot"]["config_reload"] = False + self.defaults["options"]["reboot"]["write_erase"] = False + self.defaults["options"]["package"] = {} + self.defaults["options"]["package"]["install"] = False + self.defaults["options"]["package"]["uninstall"] = False + + def _init_properties(self): + # self.ip_addresses is used in: + # self._wait_for_current_actions_to_complete() + # self._wait_for_image_upgrade_to_complete() + self.ip_addresses = set() + # TODO:1 Review these properties since we are no longer + # calling this class per-switch given the payload structure + # is not amenable to that. + self.properties = {} + self.properties["bios_force"] = False + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + self.properties["config_reload"] = False + self.properties["devices"] = None + self.properties["disruptive"] = True + self.properties["epld_golden"] = False + self.properties["epld_module"] = "ALL" + self.properties["epld_upgrade"] = False + self.properties["force_non_disruptive"] = False + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["non_disruptive"] = False + self.properties["package_install"] = False + self.properties["package_uninstall"] = False + self.properties["reboot"] = False + self.properties["write_erase"] = False + + self.valid_nxos_mode = set() + self.valid_nxos_mode.add("disruptive") + self.valid_nxos_mode.add("non_disruptive") + self.valid_nxos_mode.add("force_non_disruptive") + + self.valid_epld_module = set() + self.valid_epld_module.add("ALL") + for module in range(1, self.max_module_number): + self.valid_epld_module.add(str(module)) + + def _populate_ndfc_version(self): + """ + Populate self.ndfc_version with the NDFC version. + + Notes: + 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + imports resulting in RecursionError + """ + instance = NdfcVersion(self.module) + instance.refresh() + self.ndfc_version = instance.version + + # def prune_devices(self): + # """ + # If the image is already upgraded on a device, remove that device + # from self.devices. self.devices dict has already been validated, + # so no further error checking is needed here. + + # TODO:1 This prunes devices only based on the image upgrade state. + # TODO:1 It does not check other image states and EPLD states. + # """ + # # issu = NdfcSwitchIssuDetailsBySerialNumber(self.module) + # pruned_devices = set() + # instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + # instance.refresh() + # for device in self.devices: + # msg = f"REMOVE: {self.class_name}.prune_devices() device: {device}" + # self.log_msg(msg) + # instance.ip_address = device.get("ip_address") + # instance.refresh() + # if instance.upgrade == "Success": + # msg = f"REMOVE: {self.class_name}.prune_devices: " + # msg = "image already upgraded for " + # msg += f"{instance.device_name}, " + # msg += f"{instance.serial_number}, " + # msg += f"{instance.ip_address}" + # self.log_msg(msg) + # pruned_devices.add(instance.ip_address) + # self.devices = [ + # device + # for device in self.devices + # if device.get("ip_address") not in pruned_devices + # ] + + def validate_devices(self): + """ + Fail if the upgrade state for any device is Failed. + """ + for device in self.devices: + self.issu_detail.ip_address = device.get("ip_address") + self.issu_detail.refresh() + if self.issu_detail.upgrade == "Failed": + msg = f"{self.class_name}.validate_devices: Image upgrade is " + msg += "failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += "Please check the switch " + msg += "to determine the cause and try again." + self.module.fail_json(msg) + # used in self._wait_for_current_actions_to_complete() + self.ip_addresses.add(self.issu_detail.ip_address) + + def _merge_defaults_to_switch_config(self, config): + if config.get("stage") is None: + config["stage"] = self.defaults["stage"] + if config.get("reboot") is None: + config["reboot"] = self.defaults["reboot"] + if config.get("validate") is None: + config["validate"] = self.defaults["validate"] + if config.get("upgrade") is None: + config["upgrade"] = self.defaults["upgrade"] + if config.get("upgrade").get("nxos") is None: + config["upgrade"]["nxos"] = self.defaults["upgrade"]["nxos"] + if config.get("upgrade").get("epld") is None: + config["upgrade"]["epld"] = self.defaults["upgrade"]["epld"] + if config.get("options") is None: + config["options"] = self.defaults["options"] + if config["options"].get("nxos") is None: + config["options"]["nxos"] = self.defaults["options"]["nxos"] + if config["options"]["nxos"].get("mode") is None: + config["options"]["nxos"]["mode"] = self.defaults["options"]["nxos"]["mode"] + if config["options"]["nxos"].get("bios_force") is None: + config["options"]["nxos"]["bios_force"] = self.defaults["options"]["nxos"]["bios_force"] + if config["options"].get("epld") is None: + config["options"]["epld"] = self.defaults["options"]["epld"] + if config["options"]["epld"].get("module") is None: + config["options"]["epld"]["module"] = self.defaults["options"]["epld"]["module"] + if config["options"]["epld"].get("golden") is None: + config["options"]["epld"]["golden"] = self.defaults["options"]["epld"]["golden"] + if config["options"].get("reboot") is None: + config["options"]["reboot"] = self.defaults["options"]["reboot"] + if config["options"]["reboot"].get("config_reload") is None: + config["options"]["reboot"]["config_reload"] = self.defaults["options"]["reboot"]["config_reload"] + if config["options"]["reboot"].get("write_erase") is None: + config["options"]["reboot"]["write_erase"] = self.defaults["options"]["reboot"]["write_erase"] + if config["options"].get("package") is None: + config["options"]["package"] = self.defaults["options"]["package"] + if config["options"]["package"].get("install") is None: + config["options"]["package"]["install"] = self.defaults["options"]["package"]["install"] + if config["options"]["package"].get("uninstall") is None: + config["options"]["package"]["uninstall"] = self.defaults["options"]["package"]["uninstall"] + return config + + def build_payload(self, device): + """ + Build the request payload to upgrade the switches. + """ + msg = f"REMOVE: {self.class_name}.build_payload() PRE_DEFAULTS: " + msg += f"device: {device}" + self.log_msg(msg) + device = self._merge_defaults_to_switch_config(device) + msg = f"REMOVE: {self.class_name}.build_payload() POST_DEFAULTS: " + msg += f"device: {device}" + self.log_msg(msg) + + + # devices_to_upgrade must currently be a single device + devices_to_upgrade = [] + self.issu_detail.ip_address = device.get("ip_address") + self.issu_detail.refresh() + payload_device = {} + payload_device["serialNumber"] = self.issu_detail.serial_number + payload_device["policyName"] = device.get("policy") + devices_to_upgrade.append(payload_device) + + self.payload = {} + self.payload["devices"] = devices_to_upgrade + self.payload["issuUpgrade"] = device.get("upgrade").get("nxos") + + # nxos_mode: The choices for nxos_mode are mutually-exclusive. + # If one is set to True, the others must be False. + # nonDisruptive corresponds to NDFC Allow Non-Disruptive GUI option + self.payload["issuUpgradeOptions1"] = {} + self.payload["issuUpgradeOptions1"]["nonDisruptive"] = False + self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = False + self.payload["issuUpgradeOptions1"]["disruptive"] = False + + nxos_mode = device.get("options").get("nxos").get("mode") + if nxos_mode not in self.valid_nxos_mode: + msg = f"{self.class_name}.build_payload() options.nxos.mode must " + msg += f"be one of {self.valid_nxos_mode}. Got {nxos_mode}." + self.module.fail_json(msg) + if nxos_mode == "non_disruptive": + self.payload["issuUpgradeOptions1"]["nonDisruptive"] = True + if nxos_mode == "disruptive": + self.payload["issuUpgradeOptions1"]["disruptive"] = True + if nxos_mode == "force_non_disruptive": + self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True + + # biosForce corresponds to NDFC BIOS Force GUI option + bios_force = device.get("options").get("nxos").get("bios_force") + if not isinstance(bios_force, bool): + msg = f"{self.class_name}.build_payload() options.nxos.bios_force " + msg += f"must be a boolean. Got {bios_force}." + self.module.fail_json(msg) + self.payload["issuUpgradeOptions2"] = {} + self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force + + # EPLD + epld_module = device.get("options").get("epld").get("module") + epld_golden = device.get("options").get("epld").get("golden") + if epld_module not in self.valid_epld_module: + msg = f"{self.class_name}.build_payload() options.epld.module must " + msg += f"be one of {self.valid_epld_module}. Got {epld_module}." + self.module.fail_json(msg) + if not isinstance(epld_golden, bool): + msg = f"{self.class_name}.build_payload() options.epld.golden " + msg += f"must be a boolean. Got {epld_golden}." + self.module.fail_json(msg) + self.payload["epldUpgrade"] = device.get("upgrade").get("epld") + self.payload["epldOptions"] = {} + self.payload["epldOptions"]["moduleNumber"] = epld_module + self.payload["epldOptions"]["golden"] = epld_golden + + # Reboot + reboot = device.get("reboot") + if not isinstance(reboot, bool): + msg = f"{self.class_name}.build_payload() reboot must " + msg += f"be a boolean. Got {reboot}." + self.module.fail_json(msg) + self.payload["reboot"] = reboot + + # Reboot options + config_reload = device.get("options").get("reboot").get("config_reload") + write_erase = device.get("options").get("reboot").get("write_erase") + if not isinstance(config_reload, bool): + msg = f"{self.class_name}.build_payload() options.reboot.config_reload " + msg += f"must be a boolean. Got {config_reload}." + self.module.fail_json(msg) + if not isinstance(write_erase, bool): + msg = f"{self.class_name}.build_payload() options.reboot.write_erase " + msg += f"must be a boolean. Got {write_erase}." + self.module.fail_json(msg) + self.payload["rebootOptions"] = {} + self.payload["rebootOptions"]["configReload"] = config_reload + self.payload["rebootOptions"]["writeErase"] = write_erase + + # Packages + package_install = device.get("options").get("package").get("install") + package_uninstall = device.get("options").get("package").get("uninstall") + if not isinstance(package_install, bool): + msg = f"{self.class_name}.build_payload() options.package.install " + msg += f"must be a boolean. Got {package_install}." + self.module.fail_json(msg) + if not isinstance(package_uninstall, bool): + msg = f"{self.class_name}.build_payload() options.package.uninstall " + msg += f"must be a boolean. Got {package_uninstall}." + self.module.fail_json(msg) + self.payload["pacakgeInstall"] = package_install + self.payload["pacakgeUnInstall"] = package_uninstall + + def commit(self): + """ + Commit the image upgrade request to NDFC and wait + for the images to be upgraded. + """ + if self.devices is None: + msg = f"{self.class_name}.commit() call instance.devices " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.devices) == 0: + msg = f"REMOVE: {self.class_name}.commit() no devices to upgrade." + self.log_msg(msg) + return + #self.prune_devices() + self.validate_devices() + self._wait_for_current_actions_to_complete() + path = self.endpoints.image_upgrade.get("path") + verb = self.endpoints.image_upgrade.get("verb") + for device in self.devices: + self.build_payload(device) + self.log_msg(f"REMOVE: {self.class_name}.commit() upgrade payload: {self.payload}") + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_upgrade_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not upgrade an image if there are any actions in progress. + Wait for all actions to complete before upgrading image. + Actions include image staging, image upgrade, and image validation. + """ + ipv4_todo = copy.copy(self.ip_addresses) + timeout = self.check_timeout + while len(ipv4_todo) > 0 and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for ipv4 in self.ip_addresses: + if ipv4 not in ipv4_todo: + continue + self.issu_detail.ip_address = ipv4 + self.issu_detail.refresh() + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{ipv4} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + ipv4_todo.remove(ipv4) + continue + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{ipv4} actions in progress. " + msg += f"Waiting. {timeout} seconds remaining." + self.log_msg(msg) + + def _wait_for_image_upgrade_to_complete(self): + """ + Wait for image upgrade to complete + """ + ipv4_done = set() + timeout = self.check_timeout + ipv4_todo = set(copy.copy(self.ip_addresses)) + while ipv4_done != ipv4_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"seconds remaining {timeout}, " + msg += f"ipv4_todo: {sorted(list(ipv4_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"seconds remaining {timeout}, " + msg += f"ipv4_done: {sorted(list(ipv4_done))}" + self.log_msg(msg) + for ipv4 in self.ip_addresses: + if ipv4 in ipv4_done: + continue + self.issu_detail.ip_address = ipv4 + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + upgrade_percent = self.issu_detail.upgrade_percent + upgrade_status = self.issu_detail.upgrade + serial_number = self.issu_detail.serial_number + + if upgrade_status == "Failed": + msg = f"{self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"Seconds remaining {timeout}: upgrade image " + msg += f"{upgrade_status} for " + msg += f"{device_name}, {serial_number}, {ip_address}" + self.module.fail_json(msg) + + if upgrade_status == "Success": + ipv4_done.add(ipv4) + status = "succeeded" + if upgrade_status == None: + status = "not started" + if upgrade_status == "In-Progress": + status = "in progress" + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"Seconds remaining {timeout}, " + msg += f"Percent complete {upgrade_percent}, " + msg += f"Status {status}, " + msg += f"{device_name}, {serial_number}, {ip_address}" + self.log_msg(msg) + + if ipv4_done != ipv4_todo: + msg = f"{self.class_name}._wait_for_image_upgrade_to_complete(): " + msg += "The following device(s) did not complete upgrade: " + msg += f"{ipv4_todo.difference(ipv4_done)}. " + msg += "Try increasing issu timeout in the playbook, or check " + msg += "the device(s) to determine the cause " + msg += "(e.g. show install all status)." + self.module.fail_json(msg) + + # setter properties + @property + def bios_force(self): + """ + Set the bios_force flag to True or False. + + Default: False + """ + return self.properties.get("bios_force") + + @bios_force.setter + def bios_force(self, value): + name = "bios_force" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def config_reload(self): + """ + Set the config_reload flag to True or False. + + Default: False + """ + return self.properties.get("config_reload") + + @config_reload.setter + def config_reload(self, value): + name = "config_reload" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def devices(self): + """ + Set the devices to upgrade. + + list() of dict() with the following structure: + { + "serial_number": "FDO211218HH", + "policy_name": "NR1F" + } + + Must be set before calling instance.commit() + """ + return self.properties.get("devices") + + @devices.setter + def devices(self, value): + name = "devices" + if not isinstance(value, list): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a python list of dict." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def disruptive(self): + """ + Set the disruptive flag to True or False. + + Default: False + """ + return self.properties.get("disruptive") + + @disruptive.setter + def disruptive(self, value): + name = "disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_golden(self): + """ + Set the epld_golden flag to True or False. + + Default: False + """ + return self.properties.get("epld_golden") + + @epld_golden.setter + def epld_golden(self, value): + name = "epld_golden" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_upgrade(self): + """ + Set the epld_upgrade flag to True or False. + + Default: False + """ + return self.properties.get("epld_upgrade") + + @epld_upgrade.setter + def epld_upgrade(self, value): + name = "epld_upgrade" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_module(self): + """ + Set the epld_module to upgrade. + + Ignored if epld_upgrade is set to False + Valid values: integer or "ALL" + Default: "ALL" + """ + return self.properties.get("epld_module") + + @epld_module.setter + def epld_module(self, value): + name = "epld_module" + try: + value = value.upper() + except AttributeError: + pass + if not isinstance(value, int) and value != "ALL": + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be an integer or 'ALL'" + self.module.fail_json(msg) + self.properties[name] = value + + @property + def force_non_disruptive(self): + """ + Set the force_non_disruptive flag to True or False. + + Default: False + """ + return self.properties.get("force_non_disruptive") + + @force_non_disruptive.setter + def force_non_disruptive(self, value): + name = "force_non_disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def non_disruptive(self): + """ + Set the non_disruptive flag to True or False. + + Default: True + """ + return self.properties.get("non_disruptive") + + @non_disruptive.setter + def non_disruptive(self, value): + name = "non_disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def package_install(self): + """ + Set the package_install flag to True or False. + + Default: False + """ + return self.properties.get("package_install") + + @package_install.setter + def package_install(self, value): + name = "package_install" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def package_uninstall(self): + """ + Set the package_uninstall flag to True or False. + + Default: False + """ + return self.properties.get("package_uninstall") + + @package_uninstall.setter + def package_uninstall(self, value): + name = "package_uninstall" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def reboot(self): + """ + Set the reboot flag to True or False. + + Default: False + """ + return self.properties.get("reboot") + + @reboot.setter + def reboot(self, value): + name = "reboot" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def write_erase(self): + """ + Set the write_erase flag to True or False. + + Default: False + """ + return self.properties.get("write_erase") + + @write_erase.setter + def write_erase(self, value): + name = "write_erase" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + + # getter properties + @property + def check_interval(self): + """ + Return the image upgrade check interval in seconds + """ + return self.properties.get("check_interval") + + @property + def check_timeout(self): + """ + Return the image upgrade check timeout in seconds + """ + return self.properties.get("check_timeout") + + @property + def ndfc_data(self): + """ + Return the data retrieved from NDFC for the image upgrade request. + + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_response") + + @property + def serial_numbers(self): + """ + Return a list of serial numbers from self.devices + """ + return [device.get("serial_number") for device in self.devices] + + +class NdfcVersion(NdfcAnsibleImageUpgradeCommon): + """ + Return image version information from NDFC + + NOTES: + 1. considered using dcnm_version_supported() but it does not return + minor release info, which is needed due to key changes between + 12.1.2e and 12.1.3b. For example, see NdfcImageStage().commit() + + Endpoint: + /appcenter/cisco/ndfc/api/v1/fm/about/version + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcVersion(module) + instance.refresh() + if instance.version == "12.1.2e": + do 12.1.2e stuff + else: + do other stuff + + Response: + { + "version": "12.1.2e", + "mode": "LAN", + "isMediaController": false, + "dev": false, + "isHaEnabled": false, + "install": "EASYFABRIC", + "uuid": "f49e6088-ad4f-4406-bef6-2419de914ff1", + "is_upgrade_inprogress": false + } + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + + def refresh(self): + """ + Refresh self.ndfc_data with current version info from NDFC + """ + path = self.endpoints.ndfc_version.get("path") + verb = self.endpoints.ndfc_version.get("verb") + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_response: {self.ndfc_response}" + self.log_msg(msg) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_result: {self.ndfc_result}" + self.log_msg(msg) + + if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: + msg = f"{self.class_name}.refresh() failed: {self.ndfc_result}" + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + if self.ndfc_data is None: + msg = f"{self.class_name}.refresh() failed: NDFC response " + msg += "does not contain DATA key. NDFC response: " + msg += f"{self.ndfc_response}" + self.module.fail_json(msg) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_data: {self.ndfc_data}" + self.log_msg(msg) + + def _get(self, item): + return self.make_boolean(self.make_none(self.ndfc_data.get(item))) + + @property + def dev(self): + """ + Return True if NDFC is a development release. + Return False if NDFC is not a development release. + Return None otherwise + + Possible values: + True + False + None + """ + return self._get("dev") + + @property + def install(self): + """ + Return the value of install, if it exists. + Return None otherwise + + Possible values: + EASYFABRIC + (probably other values) + None + """ + return self._get("install") + + @property + def is_ha_enabled(self): + """ + Return True if NDFC is a media controller. + Return False if NDFC is not a media controller. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("isHaEnabled")) + + @property + def is_media_controller(self): + """ + Return True if NDFC is a media controller. + Return False if NDFC is not a media controller. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("isMediaController")) + + @property + def is_upgrade_inprogress(self): + """ + Return True if an NDFC upgrade is in progress. + Return False if an NDFC upgrade is not in progress. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("is_upgrade_inprogress")) + + @property + def ndfc_data(self): + """ + Return the data retrieved from the request + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the GET result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the GET response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def mode(self): + """ + Return the NDFC mode, if it exists. + Return None otherwise + + Possible values: + LAN + None + """ + return self._get("mode") + + @property + def uuid(self): + """ + Return the value of uuid, if it exists. + Return None otherwise + + Possible values: + uuid e.g. "f49e6088-ad4f-4406-bef6-2419de914df1" + None + """ + return self._get("uuid") + + @property + def version(self): + """ + Return the NDFC version, if it exists. + Return None otherwise + + Possible values: + version, e.g. "12.1.2e" + None + """ + return self._get("version") + + @property + def version_major(self): + """ + Return the NDFC major version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 12 + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[0] + + @property + def version_minor(self): + """ + Return the NDFC minor version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 1 + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[1] + + @property + def version_patch(self): + """ + Return the NDFC minor version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 2e + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[2] + + +def main(): + """main entry point for module execution""" + + element_spec = dict( + config=dict(required=True, type="dict"), + state=dict(default="merged", choices=["merged", "deleted", "query"]), + ) + + module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) + dcnm_module = NdfcAnsibleImageUpgrade(module) + dcnm_module.validate_input() + dcnm_module.get_have() + dcnm_module.get_want() + + if module.params["state"] == "merged": + dcnm_module.get_need_merged() + elif module.params["state"] == "deleted": + dcnm_module.get_need_deleted() + elif module.params["state"] == "query": + dcnm_module.get_need_query() + + if module.params["state"] == "query": + dcnm_module.result["changed"] = False + if module.params["state"] in ["merged", "deleted"]: + if dcnm_module.need: + dcnm_module.result["changed"] = True + else: + module.exit_json(**dcnm_module.result) + + if module.check_mode: + dcnm_module.result["changed"] = False + module.exit_json(**dcnm_module.result) + + if dcnm_module.need: + if module.params["state"] == "merged": + dcnm_module.handle_merged_state() + elif module.params["state"] == "deleted": + dcnm_module.handle_deleted_state() + elif module.params["state"] == "query": + dcnm_module.handle_query_state() + + module.exit_json(**dcnm_module.result) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/__init__.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py new file mode 100644 index 000000000..15dc580de --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py @@ -0,0 +1,38 @@ +# 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 + +import os +import json + +fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") + +def load_fixture(filename): + path = os.path.join(fixture_path, "{0}.json".format(filename)) + + with open(path) as f: + data = f.read() + + try: + fixture = json.loads(data) + except Exception as exception: + print(f"Exception loading fixture {filename}. Exception detail: {exception}") + + return fixture + + diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_payloads.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_payloads.json new file mode 100644 index 000000000..5e8601d43 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_payloads.json @@ -0,0 +1,24 @@ +{ + "ndfc_image_install_options": { + "devices": [ + { + "serialNumber": "FDO21120U5D", + "policyName": "KR5M" + } + ], + "issu": "true", + "epld": "false", + "packageInstall": "false" + }, + "ndfc_image_install_options_epld_true": { + "devices": [ + { + "serialNumber": "FDO21120U5D", + "policyName": "KR5M" + } + ], + "issu": "true", + "epld": "true", + "packageInstall": "false" + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_playbook_configs.json new file mode 100644 index 000000000..3447438c9 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_playbook_configs.json @@ -0,0 +1,11 @@ +{ + "query_config":{ + "policy": "KMR5", + "switches": [ + { + "ip_address": "172.22.150.102", + "policy": "OR1F" + } + ] + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses.json new file mode 100644 index 000000000..8ea217326 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses.json @@ -0,0 +1,2098 @@ +{ + "policymgmt_policies_200": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + }, + { + "policyName": "OR1F", + "policyType": "PLATFORM", + "nxosVersion": "10.4.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "OR1F EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.4.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.4.1.F.bin", + "agnostic": "false", + "ref_count": 0 + } + ], + "message": "" + } + }, + "image_stage_200": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image", + "MESSAGE": "OK", + "DATA": [ + { + "key": "FDO211218FV", + "value": "No files to stage" + } + ] + }, + "validate_image_200": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", + "MESSAGE": "OK", + "DATA": "Invalid JSON response: [StageResponse [key=success, value=]]" + }, + "imageupgrade_install_options_200": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "MESSAGE": "OK", + "DATA": { + "compatibilityStatusList": [ + { + "deviceName": "cvd-1314-leaf", + "ipAddress": "172.22.150.105", + "policyName": "KR5M", + "platform": "N9K/N3K", + "version": "10.2.5", + "osType": "64bit", + "status": "Success", + "installOption": "disruptive", + "compDisp": "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "timestamp": "NA" + } + ], + "epldModules": "null", + "installPacakges": "null", + "errMessage": "" + } + }, + "about_version_200": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "issu_detail_200": { + "status": "SUCCESS", + "RETURN_CODE": 200, + "DATA": { + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "None", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": null, + "vpcPeer": null, + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": null, + "ip_address": "172.22.150.102", + "peer": null, + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": false, + "mds": false + } + ] + }, + "message": "" + }, + "inventory_all_switches_200": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [ + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.110", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1111-bgw", + "switchDbID": 158560, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:28:36", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PGCT", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-1111-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1400, + "swUUID": "DCNM-UUID-1400", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.111", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1112-bgw", + "switchDbID": 160170, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:28:20", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PGD1", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-1112-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1200, + "swUUID": "DCNM-UUID-1200", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Healthy", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Spine", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.112", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1211-spine", + "switchDbID": 154230, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:29:22", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PGCS", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "spine", + "mode": "Normal", + "hostName": "cvd-1211-spine", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1100, + "swUUID": "DCNM-UUID-1100", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Major", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Spine", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.113", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1212-spine", + "switchDbID": 155930, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:28:58", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PGD0", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "spine", + "mode": "Normal", + "hostName": "cvd-1212-spine", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1300, + "swUUID": "DCNM-UUID-1300", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Healthy", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C93180YC-EX", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.103", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1312-leaf", + "switchDbID": 150610, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:30:38", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO211218GC", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "leaf", + "mode": "Normal", + "hostName": "cvd-1312-leaf", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1350, + "swUUID": "DCNM-UUID-1350", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C93180YC-EX", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.104", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1313-leaf", + "switchDbID": 151490, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:30:23", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO211218HH", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "leaf", + "mode": "Normal", + "hostName": "cvd-1313-leaf", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1050, + "swUUID": "DCNM-UUID-1050", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C93180YC-EX", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.105", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1314-leaf", + "switchDbID": 153350, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:30:09", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO211218FV", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "leaf", + "mode": "Normal", + "hostName": "cvd-1314-leaf", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1150, + "swUUID": "DCNM-UUID-1150", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 3, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9336C-FX2", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.100", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "hard", + "modelType": 0, + "logicalName": "cvd-2111-bgw", + "switchDbID": 39990, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "13 days, 21:32:36", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO22180ASJ", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-2111-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 7780, + "swUUID": "DCNM-UUID-7780", + "swType": "null", + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 3, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9336C-FX2", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.101", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "hard", + "modelType": 0, + "logicalName": "cvd-2112-bgw", + "switchDbID": 39650, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "13 days, 21:32:54", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO23021QYJ", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-2112-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 7420, + "swUUID": "DCNM-UUID-7420", + "swType": "null", + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Spine", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 3, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.114", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "hard", + "modelType": 0, + "logicalName": "cvd-2211-spine", + "switchDbID": 39690, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "13 days, 21:32:38", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PHDD", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "spine", + "mode": "Normal", + "hostName": "cvd-2211-spine", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 39720, + "swUUID": "DCNM-UUID-39720", + "swType": "null", + "ccStatus": "In-Sync", + "operStatus": "Healthy", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Spine", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 3, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.115", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "hard", + "modelType": 0, + "logicalName": "cvd-2212-spine", + "switchDbID": 39940, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "13 days, 21:32:35", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PHDQ", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "spine", + "mode": "Normal", + "hostName": "cvd-2212-spine", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 39970, + "swUUID": "DCNM-UUID-39970", + "swType": "null", + "ccStatus": "In-Sync", + "operStatus": "Healthy", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 3, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C93180YC-EX", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.106", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "hard", + "modelType": 0, + "logicalName": "cvd-2311-leaf", + "switchDbID": 39790, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "13 days, 21:33:04", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO211218HB", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "leaf", + "mode": "Normal", + "hostName": "cvd-2311-leaf", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 39820, + "swUUID": "DCNM-UUID-39820", + "swType": "null", + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 3, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C93180YC-EX", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.107", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "hard", + "modelType": 0, + "logicalName": "cvd-2312-leaf", + "switchDbID": 39740, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "13 days, 21:33:03", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO211218AX", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "leaf", + "mode": "Normal", + "hostName": "cvd-2312-leaf", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 39770, + "swUUID": "DCNM-UUID-39770", + "swType": "null", + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 3, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C93180YC-EX", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.108", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "hard", + "modelType": 0, + "logicalName": "cvd-2313-leaf", + "switchDbID": 39890, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "13 days, 21:33:08", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO2112189M", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "leaf", + "mode": "Normal", + "hostName": "cvd-2313-leaf", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 39920, + "swUUID": "DCNM-UUID-39920", + "swType": "null", + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 3, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C93180YC-EX", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.109", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "hard", + "modelType": 0, + "logicalName": "cvd-2314-leaf", + "switchDbID": 39840, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "13 days, 21:33:02", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO211218B5", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "leaf", + "mode": "Normal", + "hostName": "cvd-2314-leaf", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 39870, + "swUUID": "DCNM-UUID-39870", + "swType": "null", + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Aggregation", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9336C-FX2", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.99", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-rs-111", + "switchDbID": 161530, + "uid": 0, + "release": "10.3(2)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:18:51", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO2443096H", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "aggregation", + "mode": "Normal", + "hostName": "cvd-rs-111", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 7740, + "swUUID": "DCNM-UUID-7740", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C93180YC-EX", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.102", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "leaf1", + "switchDbID": 145740, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "7 days, 03:30:48", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO21120U5D", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "leaf", + "mode": "Normal", + "hostName": "leaf1", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1250, + "swUUID": "DCNM-UUID-1250", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + } + ] + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon.json new file mode 100644 index 000000000..44b8c53c4 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon.json @@ -0,0 +1,58 @@ +{ + "mock_get_return_code_200_MESSAGE_OK": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "OK", + "DATA": {} + }, + "mock_get_return_code_200_MESSAGE_not_OK": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "not OK", + "DATA": {} + }, + "mock_get_return_code_404_MESSAGE_not_found": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "Not Found", + "DATA": {} + }, + "mock_get_return_code_500_MESSAGE_OK": { + "RETURN_CODE": 500, + "METHOD": "GET", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "OK", + "DATA": {} + }, + "mock_post_return_code_200_MESSAGE_OK": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "OK", + "DATA": {} + }, + "mock_post_return_code_400_MESSAGE_NOT_OK": { + "RETURN_CODE": 400, + "METHOD": "POST", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "error: image not found", + "DATA": {} + }, + "mock_post_return_code_200_ERROR_key_present": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://blah_noop", + "ERROR": "image not found", + "DATA": {} + }, + "mock_unknown_response_verb": { + "RETURN_CODE": 200, + "METHOD": "FOO", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "not OK", + "DATA": {} + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImageInstallOptions.json new file mode 100644 index 000000000..e41654f2d --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImageInstallOptions.json @@ -0,0 +1,38 @@ +{ + "imageupgrade_install_options_post_return_code_200": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "MESSAGE": "OK", + "DATA": { + "compatibilityStatusList": [ + { + "deviceName": "cvd-1314-leaf", + "ipAddress": "172.22.150.105", + "policyName": "KR5M", + "platform": "N9K/N3K", + "version": "10.2.5", + "osType": "64bit", + "status": "Success", + "installOption": "disruptive", + "compDisp": "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "timestamp": "NA" + } + ], + "epldModules": "null", + "installPacakges": "null", + "errMessage": "" + } + }, + "imageupgrade_install_options_post_return_code_500": { + "RETURN_CODE": 500, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "MESSAGE": "Internal Server Error", + "DATA": { + "error": "null" + } + } +} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImagePolicies.json new file mode 100644 index 000000000..4478a8300 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImagePolicies.json @@ -0,0 +1,97 @@ +{ + "policymgnt_policies_get_return_code_200": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + }, + { + "policyName": "OR1F", + "policyType": "PLATFORM", + "nxosVersion": "10.4.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "OR1F EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.4.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.4.1.F.bin", + "agnostic": "false", + "ref_count": 0 + } + ], + "message": "" + } + }, + "policymgnt_policies_get_return_code_200_empty_DATA": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": {} + }, + "policymgnt_policies_get_return_code_200_ndfc_has_no_defined_image_policies": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "policymgnt_policies_get_return_code_404": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policiess", + "MESSAGE": "Not Found", + "DATA": { + "timestamp": "2023-10-22T21:01:17.375+00:00", + "status": 404, + "error": "Not Found", + "path": "/rest/policymgnt/policiess" + } + }, + "policymgnt_policies_get_return_code_200_policyName_missing_in_response": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json new file mode 100644 index 000000000..07b72260c --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json @@ -0,0 +1,2510 @@ +{ + "packagemgnt_issu_get_return_code_200_one_switch": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "packagemgnt_issu_get_return_code_200_empty_DATA": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": {} + }, + "packagemgnt_issu_get_return_code_200_ndfc_switch_issu_info_length_0": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "packagemgnt_issu_get_return_code_404": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issuu", + "MESSAGE": "Not Found", + "DATA": { + "timestamp": "2023-10-22T21:01:17.375+00:00", + "status": 404, + "error": "Not Found", + "path": "/rest/packagemgnt/issuu" + } + }, + "packagemgnt_issu_get_return_code_200_many_switch": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218AX", + "deviceName": "cvd-2312-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.107", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.107", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2312-leaf", + "id": 3, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218B5", + "deviceName": "cvd-2314-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.109", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39840, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.109", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2314-leaf", + "id": 4, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218FV", + "deviceName": "cvd-1314-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:40", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.105", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 153350, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.105", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1314-leaf", + "id": 5, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218GC", + "deviceName": "cvd-1312-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-23 01:43", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.103", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 150610, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.103", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1312-leaf", + "id": 6, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218HB", + "deviceName": "cvd-2311-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.106", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39790, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.106", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2311-leaf", + "id": 7, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218HH", + "deviceName": "cvd-1313-leaf", + "version": "10.2(5)", + "policy": "null", + "status": "null", + "reason": "null", + "imageStaged": "null", + "validated": "null", + "upgrade": "null", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "Never", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.104", + "issuAllowed": "", + "statusPercent": 0, + "imageStagedPercent": 0, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 151490, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.104", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1313-leaf", + "id": 8, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO22180ASJ", + "deviceName": "cvd-2111-bgw", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "border gateway", + "lastUpgAction": "2023-Oct-06 03:42", + "model": "N9K-C9336C-FX2", + "fabric": "hard", + "ipAddress": "172.22.150.100", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39990, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.100", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2111-bgw", + "id": 9, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO23021QYJ", + "deviceName": "cvd-2112-bgw", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "border gateway", + "lastUpgAction": "2023-Oct-06 17:25", + "model": "N9K-C9336C-FX2", + "fabric": "hard", + "ipAddress": "172.22.150.101", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39650, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.101", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2112-bgw", + "id": 10, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2443096H", + "deviceName": "cvd-rs-111", + "version": "10.3(2)", + "policy": "null", + "status": "null", + "reason": "null", + "imageStaged": "null", + "validated": "null", + "upgrade": "null", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "aggregation", + "lastUpgAction": "Never", + "model": "N9K-C9336C-FX2", + "fabric": "easy", + "ipAddress": "172.22.150.99", + "issuAllowed": "", + "statusPercent": 0, + "imageStagedPercent": 0, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 161530, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.99", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-rs-111", + "id": 11, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FOX2109PGCS", + "deviceName": "cvd-1211-spine", + "version": "10.2(5)", + "policy": "null", + "status": "null", + "reason": "null", + "imageStaged": "null", + "validated": "null", + "upgrade": "null", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "spine", + "lastUpgAction": "Never", + "model": "N9K-C9504", + "fabric": "easy", + "ipAddress": "172.22.150.112", + "issuAllowed": "", + "statusPercent": 0, + "imageStagedPercent": 0, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 154230, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.112", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1211-spine", + "id": 12, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FOX2109PGCT", + "deviceName": "cvd-1111-bgw", + "version": "10.2(5)", + "policy": "null", + "status": "null", + "reason": "null", + "imageStaged": "null", + "validated": "null", + "upgrade": "null", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "border gateway", + "lastUpgAction": "Never", + "model": "N9K-C9504", + "fabric": "easy", + "ipAddress": "172.22.150.110", + "issuAllowed": "", + "statusPercent": 0, + "imageStagedPercent": 0, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 158560, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.110", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1111-bgw", + "id": 13, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FOX2109PGD0", + "deviceName": "cvd-1212-spine", + "version": "10.2(5)", + "policy": "null", + "status": "null", + "reason": "null", + "imageStaged": "null", + "validated": "null", + "upgrade": "null", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "spine", + "lastUpgAction": "Never", + "model": "N9K-C9504", + "fabric": "easy", + "ipAddress": "172.22.150.113", + "issuAllowed": "", + "statusPercent": 0, + "imageStagedPercent": 0, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 155930, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.113", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1212-spine", + "id": 14, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FOX2109PGD1", + "deviceName": "cvd-1112-bgw", + "version": "10.2(5)", + "policy": "null", + "status": "null", + "reason": "null", + "imageStaged": "null", + "validated": "null", + "upgrade": "null", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "border gateway", + "lastUpgAction": "Never", + "model": "N9K-C9504", + "fabric": "easy", + "ipAddress": "172.22.150.111", + "issuAllowed": "", + "statusPercent": 0, + "imageStagedPercent": 0, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 160170, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.111", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1112-bgw", + "id": 15, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FOX2109PHDD", + "deviceName": "cvd-2211-spine", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "spine", + "lastUpgAction": "2023-Oct-06 04:00", + "model": "N9K-C9504", + "fabric": "hard", + "ipAddress": "172.22.150.114", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39690, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.114", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2211-spine", + "id": 16, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FOX2109PHDQ", + "deviceName": "cvd-2212-spine", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "spine", + "lastUpgAction": "2023-Oct-06 04:00", + "model": "N9K-C9504", + "fabric": "hard", + "ipAddress": "172.22.150.115", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39940, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.115", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2212-spine", + "id": 17, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageStage_test_prune_serial_numbers": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218AX", + "deviceName": "cvd-2312-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.107", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.107", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2312-leaf", + "id": 3, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218B5", + "deviceName": "cvd-2314-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.109", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39840, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.109", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2314-leaf", + "id": 4, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218FV", + "deviceName": "cvd-1314-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:40", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.105", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 153350, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.105", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1314-leaf", + "id": 5, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218GC", + "deviceName": "cvd-1312-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-23 01:43", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.103", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 150610, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.103", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1312-leaf", + "id": 6, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageStage_test_validate_serial_numbers": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Failed", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageStage_test_wait_for_image_stage_to_complete": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageStage_test_wait_for_image_stage_to_complete_fail_json": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Failed", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 90, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageStage_test_wait_for_image_stage_to_complete_timeout": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "In-Progress", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 50, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageStage_test_wait_for_current_actions_to_complete": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageStage_test_wait_for_current_actions_to_complete_timeout": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "In-Progress", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 50, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageValidate_test_prune_serial_numbers": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "none", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218AX", + "deviceName": "cvd-2312-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "none", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.107", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.107", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2312-leaf", + "id": 3, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218B5", + "deviceName": "cvd-2314-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "none", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.109", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39840, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.109", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2314-leaf", + "id": 4, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218FV", + "deviceName": "cvd-1314-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:40", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.105", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 153350, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.105", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1314-leaf", + "id": 5, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218GC", + "deviceName": "cvd-1312-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-23 01:43", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.103", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 150610, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.103", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1312-leaf", + "id": 6, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageValidate_test_validate_serial_numbers": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Failed", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 50, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageValidate_test_wait_for_image_validate_to_complete": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageValidate_test_wait_for_image_validate_to_complete_fail_json": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Failed", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 90, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageValidate_test_wait_for_image_validate_to_complete_timeout": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "In-Progress", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 50, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageValidate_test_wait_for_current_actions_to_complete": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImageValidate_test_wait_for_current_actions_to_complete_timeout": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "In-Progress", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 50, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImagePolicyAction_test_build_attach_payload": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218AX", + "deviceName": "cvd-2312-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.107", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.107", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2312-leaf", + "id": 3, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218B5", + "deviceName": "cvd-2314-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.109", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39840, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.109", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2314-leaf", + "id": 4, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218FV", + "deviceName": "cvd-1314-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:40", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.105", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 153350, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.105", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1314-leaf", + "id": 5, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO211218GC", + "deviceName": "cvd-1312-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-23 01:43", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.103", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 150610, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.103", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-1312-leaf", + "id": 6, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "NdfcImagePolicyAction_test_build_attach_payload_fail_json": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "none", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json new file mode 100644 index 000000000..b75eb4eb0 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json @@ -0,0 +1,363 @@ +{ + "NdfcVersion_get_return_code_200": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_get_return_code_404": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://foo/noop", + "MESSAGE": "Not Found", + "DATA": {} + }, + "NdfcVersion_get_return_code_500": { + "RETURN_CODE": 500, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "Internal Server Error", + "DATA": {} + }, + "NdfcVersion_dev_false": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_dev_true": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "true", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_dev_none": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_install_EASYFABRIC": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_install_none": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_is_ha_enabled_false": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_is_ha_enabled_true": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "true", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_is_ha_enabled_none": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_is_upgrade_inprogress_false": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_is_upgrade_inprogress_true": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "true" + } + }, + "NdfcVersion_is_upgrade_inprogress_none": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "" + } + }, + "NdfcVersion_is_media_controller_false": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_is_media_controller_true": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "true", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_is_media_controller_none": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_DATA_present": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_DATA_not_present": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK" + }, + "NdfcVersion_mode_LAN": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_mode_none": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_uuid_UUID": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "foo-uuid", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_uuid_none": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_version_12.1.3b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcVersion_version_none": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py new file mode 100644 index 000000000..13329b421 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py @@ -0,0 +1,274 @@ +from typing import Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcAnsibleImageUpgradeCommon, NdfcEndpoints) +from .fixture import load_fixture + +""" +ndfc_version: 12 +description: Verify functionality of class NdfcAnsibleImageUpgradeCommon +""" +class_name = "NdfcAnsibleImageUpgradeCommon" +response_file = f"dcnm_image_upgrade_responses_{class_name}" + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> dict: + raise AnsibleFailJson(msg) + + +@pytest.fixture +def module(): + return NdfcAnsibleImageUpgradeCommon(MockAnsibleModule) + + +def response_data(key: str) -> Dict[str, str]: + response = load_fixture(response_file).get(key) + verb = response.get("METHOD") + print(f"{key} : {verb} : {response}") + return {"response": response, "verb": verb} + + +def test_init_(module) -> None: + """ + __init__ sets expected values + """ + module.__init__(MockAnsibleModule) + assert module.params == {} + assert module.debug == True + assert module.fd == None + assert module.logfile == "/tmp/dcnm_image_upgrade.log" + assert isinstance(module.endpoints, NdfcEndpoints) + + +@pytest.mark.parametrize( + "key, expected", + [ + ("mock_post_return_code_200_MESSAGE_OK", {"success": True, "changed": True}), + ( + "mock_post_return_code_400_MESSAGE_NOT_OK", + {"success": False, "changed": False}, + ), + ( + "mock_post_return_code_200_ERROR_key_present", + {"success": False, "changed": False}, + ), + ], +) +def test_handle_response_post(module, key, expected) -> None: + """ + verify _handle_reponse() return values for 200/OK response + to POST request + """ + data = response_data(key) + result = module._handle_response(data.get("response"), data.get("verb")) + assert result.get("success") == expected.get("success") + assert result.get("changed") == expected.get("changed") + + +@pytest.mark.parametrize( + "key, expected", + [ + ("mock_get_return_code_200_MESSAGE_OK", {"success": True, "found": True}), + ("mock_get_return_code_200_MESSAGE_not_OK", {"success": False, "found": False}), + ( + "mock_get_return_code_404_MESSAGE_not_found", + {"success": True, "found": False}, + ), + ("mock_get_return_code_500_MESSAGE_OK", {"success": False, "found": False}), + ], +) +def test_handle_response_get(module, key, expected) -> None: + """ + verify _handle_reponse() return values for GET requests + """ + data = response_data(key) + result = module._handle_response(data.get("response"), data.get("verb")) + # TODO: We could assert on the dictionary, with a less granular error message + # assert result == expected + assert result.get("success") == expected.get("success") + assert result.get("changed") == expected.get("changed") + + +def test_handle_response_unknown_response_verb(module) -> None: + """ + verify that fail_json() is called if a unknown request verb is provided + """ + data = response_data("mock_unknown_response_verb") + with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): + module._handle_response(data.get("response"), data.get("verb")) + + +@pytest.mark.parametrize( + "key, expected", + [ + ("mock_get_return_code_200_MESSAGE_OK", {"success": True, "found": True}), + ("mock_get_return_code_200_MESSAGE_not_OK", {"success": False, "found": False}), + ( + "mock_get_return_code_404_MESSAGE_not_found", + {"success": True, "found": False}, + ), + ("mock_get_return_code_500_MESSAGE_OK", {"success": False, "found": False}), + ], +) +def test_handle_get_response(module, key, expected) -> None: + """ + verify _handle_get_reponse() return values for GET requests + + NOTE: Adding this test increases coverage by 2% according to pytest-cov + """ + data = response_data(key) + result = module._handle_get_response(data.get("response")) + + assert result.get("success") == expected.get("success") + assert result.get("changed") == expected.get("changed") + + +@pytest.mark.parametrize( + "key, expected", + [ + ("mock_post_return_code_200_MESSAGE_OK", {"success": True, "changed": True}), + ( + "mock_post_return_code_400_MESSAGE_NOT_OK", + {"success": False, "changed": False}, + ), + ( + "mock_post_return_code_200_ERROR_key_present", + {"success": False, "changed": False}, + ), + ], +) +def test_handle_post_put_delete_response(module, key, expected) -> None: + """ + _handle_post_put_delete_response() return expected values for POST requests + NOTE: This method is covered in test_handle_response_post() above, but... + NOTE: Adding this test increases coverage by 2% according to pytest-cov + """ + data = response_data(key) + result = module._handle_post_put_delete_response(data.get("response")) + assert result.get("success") == expected.get("success") + assert result.get("changed") == expected.get("changed") + + +@pytest.mark.parametrize( + "key, expected", + [ + ("True", True), + ("true", True), + ("TRUE", True), + ("True", True), + ("False", False), + ("false", False), + ("FALSE", False), + ("False", False), + ("foo", "foo"), + (0, 0), + (1, 1), + (None, None), + (None, None), + ({"foo": 10}, {"foo": 10}), + ([1, 2, "3"], [1, 2, "3"]), + ], +) +def test_make_boolean(module, key, expected) -> None: + """ + verify that make_boolean() returns expected values for all cases + """ + assert module.make_boolean(key) == expected + + +# def test_dcnm_image_upgrade_common_make_boolean(module) -> None: +# """ +# NOTE: The above parameterized testcase results in 7% greater coverage according to pytest-cov versus for loops (below) +# verify that make_boolean() returns expected values for all cases +# """ +# for value in ["True", "true", "TRUE", True]: +# assert module.make_boolean(value) == True +# for value in ["False", "false", "FALSE", False]: +# assert module.make_boolean(value) == False +# for value in ["foo", 1, 0, None, {"foo": 10}, [1, 2, "3"]]: +# assert module.make_boolean(value) == value + + +@pytest.mark.parametrize( + "key, expected", + [ + ("", None), + ("none", None), + ("None", None), + ("NONE", None), + ("null", None), + ("Null", None), + ("NULL", None), + ("None", None), + ("foo", "foo"), + (0, 0), + (1, 1), + (True, True), + (False, False), + ({"foo": 10}, {"foo": 10}), + ([1, 2, "3"], [1, 2, "3"]), + ], +) +def test_make_none(module, key, expected) -> None: + """ + verify that make_none() returns expected values for all cases + """ + assert module.make_none(key) == expected + + +# def test_dcnm_image_upgrade_common_make_none(module) -> None: +# """ +# NOTE: The above parameterized testcase results in 7% greater coverage according to pytest-cov versus for loops (below) +# verify that make_none() returns expected values for all cases +# """ +# for value in ["", "none", "None", "NONE", "null", "Null", "NULL", None]: +# assert module.make_none(value) == None +# for value in ["foo", 1, 0, True, False, {"foo": 10}, [1, 2, "3"]]: +# assert module.make_none(value) == value + + +def test_log_msg_disabled(module) -> None: + """ + verify that make_none() returns expected values for all cases + """ + ERROR_MESSAGE = "This is an error message" + module.debug = False + assert module.log_msg(ERROR_MESSAGE) == None + + +def test_log_msg_enabled(tmp_path, module) -> None: + """ + verify that make_none() returns expected values for all cases + """ + directory = tmp_path / "test_log_msg" + directory.mkdir() + filename = directory / f"test_log_msg.txt" + + ERROR_MESSAGE = "This is an error message" + module.debug = True + module.logfile = filename + module.log_msg(ERROR_MESSAGE) + + assert filename.read_text(encoding="UTF-8") == ERROR_MESSAGE + "\n" + assert len(list(tmp_path.iterdir())) == 1 + + +def test_log_msg_enabled_fail_json(tmp_path, module) -> None: + """ + log_msg() calls fail_json() if the logfile cannot be opened + """ + directory = tmp_path / "test_log_msg" + directory.mkdir() + filename = directory / f"test_{'a' * 2000}_log_msg.txt" + + ERROR_MESSAGE = "This is an error message" + module.debug = True + module.logfile = filename + with pytest.raises(AnsibleFailJson, match=r"error opening logfile"): + module.log_msg(ERROR_MESSAGE) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py new file mode 100644 index 000000000..d5823f7a3 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py @@ -0,0 +1,241 @@ +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcEndpoints +) + +""" +ndfc_version: 12 +description: Verify that class NdfcEndpoints returns the correct endpoints +""" + + +def test_dcnm_image_upgrade_endpoints_init() -> None: + """ + Endpoints.__init__ + """ + endpoints = NdfcEndpoints() + endpoints.__init__() + assert endpoints.endpoint_api_v1 == "/appcenter/cisco/ndfc/api/v1" + assert endpoints.endpoint_feature_manager == "/appcenter/cisco/ndfc/api/v1/fm" + assert ( + endpoints.endpoint_image_management + == "/appcenter/cisco/ndfc/api/v1/imagemanagement" + ) + assert ( + endpoints.endpoint_image_upgrade + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade" + ) + assert endpoints.endpoint_lan_fabric == "/appcenter/cisco/ndfc/api/v1/lan-fabric" + assert ( + endpoints.endpoint_package_mgnt + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt" + ) + assert ( + endpoints.endpoint_policy_mgnt + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt" + ) + assert ( + endpoints.endpoint_staging_management + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement" + ) + + +def test_dcnm_image_upgrade_endpoints_bootflash_info() -> None: + """ + Endpoints.bootflash_info + """ + endpoints = NdfcEndpoints() + assert endpoints.bootflash_info.get("verb") == "GET" + assert ( + endpoints.bootflash_info.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imagemgnt/bootFlash/bootflash-info" + ) + + +def test_dcnm_image_upgrade_endpoints_install_options() -> None: + """ + Endpoints.install_options + """ + endpoints = NdfcEndpoints() + assert endpoints.install_options.get("verb") == "POST" + assert ( + endpoints.install_options.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options" + ) + + +def test_dcnm_image_upgrade_endpoints_image_stage() -> None: + """ + Endpoints.image_stage + """ + endpoints = NdfcEndpoints() + assert endpoints.image_stage.get("verb") == "POST" + assert ( + endpoints.image_stage.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image" + ) + + +def test_dcnm_image_upgrade_endpoints_image_upgrade() -> None: + """ + Endpoints.image_upgrade + """ + endpoints = NdfcEndpoints() + assert endpoints.image_upgrade.get("verb") == "POST" + assert ( + endpoints.image_upgrade.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image" + ) + + +def test_dcnm_image_upgrade_endpoints_image_validate() -> None: + """ + Endpoints.image_validate + """ + endpoints = NdfcEndpoints() + assert endpoints.image_validate.get("verb") == "POST" + assert ( + endpoints.image_validate.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image" + ) + + +def test_dcnm_image_upgrade_endpoints_issu_info() -> None: + """ + Endpoints.issu_info + """ + endpoints = NdfcEndpoints() + assert endpoints.issu_info.get("verb") == "GET" + assert ( + endpoints.issu_info.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu" + ) + + +def test_dcnm_image_upgrade_endpoints_ndfc_version() -> None: + """ + Endpoints.ndfc_version + """ + endpoints = NdfcEndpoints() + assert endpoints.ndfc_version.get("verb") == "GET" + assert ( + endpoints.ndfc_version.get("path") + == "/appcenter/cisco/ndfc/api/v1/fm/about/version" + ) + + +def test_dcnm_image_upgrade_endpoints_policies_attached_info() -> None: + """ + Endpoints.policies_attached_info + """ + endpoints = NdfcEndpoints() + assert endpoints.policies_attached_info.get("verb") == "GET" + assert ( + endpoints.policies_attached_info.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/all-attached-policies" + ) + + +def test_dcnm_image_upgrade_endpoints_policies_info() -> None: + """ + Endpoints.policies_info + """ + endpoints = NdfcEndpoints() + assert endpoints.policies_info.get("verb") == "GET" + assert ( + endpoints.policies_info.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies" + ) + + +def test_dcnm_image_upgrade_endpoints_policy_attach() -> None: + """ + Endpoints.policy_attach + """ + endpoints = NdfcEndpoints() + assert endpoints.policy_attach.get("verb") == "POST" + assert ( + endpoints.policy_attach.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy" + ) + + +def test_dcnm_image_upgrade_endpoints_policy_create() -> None: + """ + Endpoints.policy_create + """ + endpoints = NdfcEndpoints() + assert endpoints.policy_create.get("verb") == "POST" + assert ( + endpoints.policy_create.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy" + ) + + +def test_dcnm_image_upgrade_endpoints_policy_detach() -> None: + """ + Endpoints.policy_detach + """ + endpoints = NdfcEndpoints() + assert endpoints.policy_detach.get("verb") == "DELETE" + assert ( + endpoints.policy_detach.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy" + ) + + +def test_dcnm_image_upgrade_endpoints_policy_info() -> None: + """ + Endpoints.policy_info + """ + endpoints = NdfcEndpoints() + assert endpoints.policy_info.get("verb") == "GET" + assert ( + endpoints.policy_info.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__" + ) + + +def test_dcnm_image_upgrade_endpoints_stage_info() -> None: + """ + Endpoints.stage_info + """ + endpoints = NdfcEndpoints() + assert endpoints.stage_info.get("verb") == "GET" + assert ( + endpoints.stage_info.get("path") + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-info" + ) + + +def test_dcnm_image_upgrade_endpoints_switches_info() -> None: + """ + Endpoints.switches_info + """ + endpoints = NdfcEndpoints() + assert endpoints.switches_info.get("verb") == "GET" + assert ( + endpoints.switches_info.get("path") + == "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches" + ) + + +# Example of using monkeypatch if we need to patch a property +# Not needed in this case, but keep this around for reference +# def test_dcnm_image_upgrade_endpoints_image_stage_monkeypatch(monkeypatch) -> None: +# """ +# :param monkeypatch: +# :return: None +# """ +# @property +# def mock_image_stage(self) -> dict: +# path = f"/stage-image" +# endpoint = {} +# endpoint["path"] = path +# endpoint["verb"] = "POST" +# return endpoint + +# monkeypatch.setattr("dcnm_image_upgrade.dcnm_image_upgrade.NdfcEndpoints.image_stage", mock_image_stage) + +# endpoints = NdfcEndpoints() +# assert endpoints.image_stage.get("verb") == "POST" +# assert endpoints.image_stage.get("path") == "/stage-image" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py new file mode 100644 index 000000000..39c83365b --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py @@ -0,0 +1,171 @@ +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcImageInstallOptions +) +from .fixture import load_fixture + +""" +ndfc_version: 12 +description: Verify functionality of class NdfcImageInstallOptions +""" +class_name = "NdfcImageInstallOptions" +response_file = f"dcnm_image_upgrade_responses_{class_name}" + +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +def response_data(key: str) -> Dict[str, str]: + response = load_fixture(response_file).get(key) + print(f"{key} : : {response}") + return response + + +@pytest.fixture +def module(): + return NdfcImageInstallOptions(MockAnsibleModule) + + +def test_policy_name_not_defined(module) -> None: + """ + fail_json() is called if policy_name is not set when refresh() is called. + """ + module.serial_number = "FOO" + error_message = "NdfcImageInstallOptions.refresh: " + error_message += "instance.policy_name must be set before " + error_message += r"calling refresh\(\)" + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_serial_number_not_defined(module) -> None: + """ + fail_json() is called if serial_number is not set when refresh() is called. + """ + module.policy_name = "FOO" + error_message = "NdfcImageInstallOptions.refresh: " + error_message += "instance.serial_number must be set before " + error_message += r"calling refresh\(\)" + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_refresh_return_code_200(monkeypatch, module) -> None: + """ + Properties are updated based on 200 response from endpoint. + endpoint: install-options + """ + key = "imageupgrade_install_options_post_return_code_200" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.policy_name = "KRM5" + module.serial_number = "BAR" + module.refresh() + assert isinstance(module.ndfc_response, dict) + assert module.device_name == "cvd-1314-leaf" + assert module.err_message == "" + assert module.epld_modules is None + assert module.install_option == "disruptive" + assert module.install_packages is None + assert module.os_type == "64bit" + assert module.platform == "N9K/N3K" + assert module.serial_number == "BAR" + assert module.version == "10.2.5" + comp_disp = "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin" + assert module.comp_disp == comp_disp + assert module.ndfc_result.get("success") == True + + +def test_refresh_return_code_500(monkeypatch, module) -> None: + """ + fail_json() should be called if the response RETURN_CODE != 200 + """ + key = "imageupgrade_install_options_post_return_code_500" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.policy_name = "KRM5" + module.serial_number = "BAR" + error_message = "NdfcImageInstallOptions.refresh: " + error_message += "Bad result when retrieving install-options from NDFC" + with pytest.raises(AnsibleFailJson, match=rf"{error_message}"): + module.refresh() + + +def test_build_payload_defaults(module) -> None: + """ + Payload contains defaults if not specified by the user. + Defaults for issu, epld, and package_install are applied. + """ + module.policy_name = "KRM5" + module.serial_number = "BAR" + module._build_payload() + assert module.payload.get("devices")[0].get("policyName") == "KRM5" + assert module.payload.get("devices")[0].get("serialNumber") == "BAR" + assert module.payload.get("issu") == True + assert module.payload.get("epld") == False + assert module.payload.get("packageInstall") == False + + +def test_build_payload_user_changed_defaults(module) -> None: + """ + Payload contains user-specified values if the user sets them. + Defaults for issu, epld, and package_install overridden by user values. + """ + module.policy_name = "KRM5" + module.serial_number = "BAR" + module.issu = False + module.epld = True + module.package_install = True + module._build_payload() + assert module.payload.get("devices")[0].get("policyName") == "KRM5" + assert module.payload.get("devices")[0].get("serialNumber") == "BAR" + assert module.payload.get("issu") == False + assert module.payload.get("epld") == True + assert module.payload.get("packageInstall") == True + + +def test_invalid_value_issu(module) -> None: + """ + fail_json() is called if issu is not a boolean. + """ + error_message = "NdfcImageInstallOptions.issu.setter: issu must be a " + error_message += "boolean value" + with pytest.raises(AnsibleFailJson, match=error_message): + module.issu = "FOO" + + +def test_invalid_value_epld(module) -> None: + """ + fail_json() is called if epld is not a boolean. + """ + error_message = "NdfcImageInstallOptions.epld.setter: epld must be a " + error_message += "boolean value" + with pytest.raises(AnsibleFailJson, match=error_message): + module.epld = "FOO" + + +def test_invalid_value_package_install(module) -> None: + """ + fail_json() is called if package_install is not a boolean. + """ + error_message = "NdfcImageInstallOptions.package_install.setter: " + error_message += "package_install must be a boolean value" + with pytest.raises(AnsibleFailJson, match=error_message): + module.package_install = "FOO" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py new file mode 100644 index 000000000..5fdefc5aa --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py @@ -0,0 +1,211 @@ +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcImagePolicies +) +from .fixture import load_fixture + +""" +ndfc_version: 12 +description: Verify functionality of class NdfcImagePolicies +""" + +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +def response_data_ndfc_image_stage(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcImagePolicies" + response = load_fixture(response_file).get(key) + print(f"response_data_ndfc_image_stage: {key} : {response}") + return response + + +@pytest.fixture +def module(): + return NdfcImagePolicies(MockAnsibleModule) + + +def test_init_properties(module) -> None: + """ + Properties are initialized to None + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("policy_name") == None + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + + +def test_refresh_return_code_200(monkeypatch, module) -> None: + """ + Properties are initialized based on 200 response from endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policies + """ + key = "policymgnt_policies_get_return_code_200" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") + return response_data_ndfc_image_stage(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + module.policy_name = "KR5M" + assert isinstance(module.ndfc_response, dict) + assert module.agnostic == False + assert module.description == "10.2.(5) with EPLD" + assert module.epld_image_name == "n9000-epld.10.2.5.M.img" + assert module.image_name == "nxos64-cs.10.2.5.M.bin" + assert module.nxos_version == "10.2.5_nxos64-cs_64bit" + assert module.package_name == None + assert module.platform == "N9K/N3K" + assert module.platform_policies == None + assert module.policy_name == "KR5M" + assert module.policy_type == "PLATFORM" + assert module.ref_count == 10 + assert module.rpm_images == None + + +def test_ndfc_result_return_code_200(monkeypatch, module) -> None: + """ + ndfc_result contains expected key/values on 200 response from endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policies + """ + key = "policymgnt_policies_get_return_code_200" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") + return response_data_ndfc_image_stage(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_result, dict) + assert module.ndfc_result.get("found") == True + assert module.ndfc_result.get("success") == True + + +def test_ndfc_result_return_code_404(monkeypatch, module) -> None: + """ + fail_json is called on 404 response from malformed endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "policymgnt_policies_get_return_code_404" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") + return response_data_ndfc_image_stage(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcImagePolicies.refresh: Bad response when retrieving " + error_message += "image policy information from NDFC." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_ndfc_result_return_code_200_empty_data(monkeypatch, module) -> None: + """ + fail_json is called on 200 response with empty DATA key. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "policymgnt_policies_get_return_code_200_empty_DATA" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") + return response_data_ndfc_image_stage(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcImagePolicies.refresh: Bad response when retrieving " + error_message += "image policy information from NDFC." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_ndfc_result_return_code_200_ndfc_has_no_defined_image_policies( + monkeypatch, module +) -> None: + """ + fail_json is called on 200 response with DATA.lastOperDataObject length 0. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "policymgnt_policies_get_return_code_200" + key += "_ndfc_has_no_defined_image_policies" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") + return response_data_ndfc_image_stage(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcImagePolicies.refresh: " + error_message += "NDFC has no defined image policies." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_policy_name_not_found(monkeypatch, module) -> None: + """ + fail_json() is called if response does not contain policy_name. + i.e. image policy with name FOO has not yet been created on NDFC. + """ + key = "policymgnt_policies_get_return_code_200" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") + return response_data_ndfc_image_stage(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + module.policy_name = "FOO" + error_message = "NdfcImagePolicies._get: " + error_message += "policy_name FOO is not defined in NDFC" + with pytest.raises(AnsibleFailJson, match=error_message): + module.policy_type == "PLATFORM" + + +def test_get_with_policy_name_None(module) -> None: + """ + fail_json is called when _get() is called prior to setting policy_name. + """ + error_message = "NdfcImagePolicies._get: instance.policy_name must be " + error_message += "set before accessing property imageName." + with pytest.raises(AnsibleFailJson, match=error_message): + module._get("imageName") + + +def test_ndfc_result_return_code_200_policy_name_missing_in_response( + monkeypatch, module +) -> None: + """ + fail_json is called on 200 response with missing policyName key. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + + NOTE: This is to cover a check in NdfcImagePolicies.refresh() for a scenario that should never happen. + TODO: Consider removing this check, and this testcase. + """ + key = "policymgnt_policies_get_return_code_200" + key += "_policyName_missing_in_response" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") + return response_data_ndfc_image_stage(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcImagePolicies.refresh: " + error_message += "Cannot parse NDFC policy information" + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py new file mode 100644 index 000000000..a41249a6c --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py @@ -0,0 +1,274 @@ +""" +ndfc_version: 12 +description: Verify functionality of NdfcImageStage +""" + +from contextlib import contextmanager +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcImagePolicies, NdfcImagePolicyAction, + NdfcSwitchIssuDetailsBySerialNumber) +from .fixture import load_fixture + + +@contextmanager +def does_not_raise(): + yield + +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +def response_data_issu_details(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" + response = load_fixture(response_file).get(key) + print(f"response_data_issu_details: {key} : {response}") + return response + + +def response_data_ndfc_image_policies(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcImagePolicies" + response = load_fixture(response_file).get(key) + print(f"response_data_ndfc_image_policies: {key} : {response}") + return response + + +def response_data_ndfc_image_policy_actioin(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcImagePolicyAction" + response = load_fixture(response_file).get(key) + print(f"response_data_ndfc_image_policy_action: {key} : {response}") + return response + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +@pytest.fixture +def module(): + return NdfcImagePolicyAction(MockAnsibleModule) + + +@pytest.fixture +def mock_issu_details() -> NdfcSwitchIssuDetailsBySerialNumber: + return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) + + +@pytest.fixture +def mock_ndfc_image_policies() -> NdfcImagePolicies: + return NdfcImagePolicies(MockAnsibleModule) + + +# test_init + + +def test_init(module) -> None: + module.__init__(MockAnsibleModule) + assert isinstance(module, NdfcImagePolicyAction) + assert isinstance(module.switch_issu_details, NdfcSwitchIssuDetailsBySerialNumber) + assert module.valid_actions == {"attach", "detach", "query"} + + +# test_init_properties + + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("action") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + assert module.properties.get("policy_name") == None + assert module.properties.get("query_result") == None + assert module.properties.get("serial_numbers") == None + + +# test_build_attach_payload + + +def test_build_attach_payload(monkeypatch, module, mock_issu_details) -> None: + """ + build_attach_payload builds the payload to send in the POST request + to attach policies to devices + + Expectations: + 1. module.payload should be a list() + + Expected results: + 1. module.fail_json should not be called + 2. module.payloads should be a list() + 3. module.payloads should have length 5 + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImagePolicyAction_test_build_attach_payload" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.switch_issu_details = mock_issu_details + module.policy_name = "KR5M" + module.serial_numbers = [ + "FDO2112189M", + "FDO211218AX", + "FDO211218B5", + "FDO211218FV", + "FDO211218GC", + ] + with does_not_raise(): + module.build_attach_payload() + assert isinstance(module.payloads, list) + assert len(module.payloads) == 5 + + +# test_build_attach_payload_fail_json + + +def test_build_attach_payload_fail_json(monkeypatch, module, mock_issu_details) -> None: + """ + build_attach_payload builds the payload to send in the POST request + to attach policies to devices. If any key in the payload has a value + of None, the function calls fail_json. + + Expected results: + 1. module.fail_json should be called because deviceName is None in the issu_details response + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImagePolicyAction_test_build_attach_payload_fail_json" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.switch_issu_details = mock_issu_details + module.policy_name = "KR5M" + module.serial_numbers = [ + "FDO2112189M", + ] + error_message = "Unable to determine hostName for switch " + error_message += "172.22.150.108, FDO2112189M, None. " + error_message += "Please verify that the switch is managed by NDFC." + with pytest.raises(AnsibleFailJson, match=error_message): + module.build_attach_payload() + + +# test_validate_request_policy_name_none + + +def test_validate_request_action_none(monkeypatch, module, mock_issu_details) -> None: + """ + validate_request performs a number of validations prior to calling commit + If any of these validations fail, the function calls fail_json with a + validation-specific error message. + + Expected results: + 1. module.fail_json should be called because module.action is None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImagePolicyAction_test_build_attach_payload_fail_json" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.switch_issu_details = mock_issu_details + module.policy_name = "KR5M" + module.serial_numbers = [ + "FDO2112189M", + ] + match = "NdfcImagePolicyAction.validate_request: " + match += "instance.action must be set before calling commit()" + with pytest.raises(AnsibleFailJson, match=match): + module.validate_request() + + +# test_validate_request_policy_name_none + +match = "NdfcImagePolicyAction.validate_request: " +match += "instance.policy_name must be set before calling commit()" + + +@pytest.mark.parametrize( + "action,expected", + [ + ("attach", pytest.raises(AnsibleFailJson, match=match)), + ("detach", pytest.raises(AnsibleFailJson, match=match)), + ("query", pytest.raises(AnsibleFailJson, match=match)), + ], +) +def test_validate_request_policy_name_none( + monkeypatch, action, expected, module, mock_issu_details +) -> None: + """ + validate_request performs a number of validations prior to calling commit + If any of these validations fail, the function calls fail_json with a + validation-specific error message. + + Expected results: + 1. module.fail_json should be called because module.policy_name is None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImagePolicyAction_test_build_attach_payload_fail_json" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.switch_issu_details = mock_issu_details + module.action = action + module.serial_numbers = [ + "FDO2112189M", + ] + + with expected: + module.validate_request() + + +# test_validate_request_serial_numbers_none + +match = "NdfcImagePolicyAction.validate_request: " +match += "instance.serial_numbers must be set before calling commit()" + + +@pytest.mark.parametrize( + "action,expected", + [ + ("attach", pytest.raises(AnsibleFailJson, match=match)), + ("detach", pytest.raises(AnsibleFailJson, match=match)), + ("query", does_not_raise()), + ], +) +def test_validate_request_serial_numbers_none( + monkeypatch, action, expected, module, mock_issu_details +) -> None: + """ + validate_request performs a number of validations prior to calling commit + If any of these validations fail, the function calls fail_json with a + validation-specific error message. + + Expected results: + 1. action == attach module.fail_json should be called + 2. action == detach module.fail_json should be called + 3. action == query module.fail_json should NOT be called + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImagePolicyAction_test_build_attach_payload_fail_json" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.switch_issu_details = mock_issu_details + module.action = action + module.policy_name = "KR5M" + + with expected: + module.validate_request() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py new file mode 100644 index 000000000..f754b40ab --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py @@ -0,0 +1,368 @@ +""" +ndfc_version: 12 +description: Verify functionality of NdfcImageStage +TODO:2 NdfcImageStage._populate_ndfc_version unit test +TODO:2 NdfcImageStage.commit unit test +""" + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcEndpoints, NdfcImageStage, NdfcSwitchIssuDetailsBySerialNumber, + NdfcVersion) +from .fixture import load_fixture + +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +def response_data_issu_details(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" + response = load_fixture(response_file).get(key) + print(f"response_data_issu_details: {key} : {response}") + return response + + +def response_data_ndfc_version(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcVersion" + response = load_fixture(response_file).get(key) + print(f"response_data_ndfc_version: {key} : {response}") + return response + + +def response_data_ndfc_image_stage(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcImageStage" + response = load_fixture(response_file).get(key) + print(f"response_data_ndfc_image_stage: {key} : {response}") + return response + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +@pytest.fixture +def module(): + return NdfcImageStage(MockAnsibleModule) + + +@pytest.fixture +def mock_issu_details() -> NdfcSwitchIssuDetailsBySerialNumber: + return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) + + +# test_init + + +def test_init(module) -> None: + """ + class attributes are initialized to expected values + """ + module.__init__(MockAnsibleModule) + assert module.module == MockAnsibleModule + assert module.class_name == "NdfcImageStage" + assert isinstance(module.properties, dict) + assert isinstance(module.serial_numbers_done, set) + assert module.ndfc_version == None + assert isinstance(module.issu_detail, NdfcSwitchIssuDetailsBySerialNumber) + assert isinstance(module.endpoints, NdfcEndpoints) + + +# test_init_properties + + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + assert module.properties.get("serial_numbers") == None + assert module.properties.get("check_interval") == 10 + assert module.properties.get("check_timeout") == 1800 + + +# test_prune_serial_numbers + + +def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: + """ + prune_serial_numbers removes serial numbers from the list for which + imageStaged == "Success" (TODO: AND policy == ) + + Expectations: + 1. module.serial_numbers should contain only serial numbers for which + imageStaged == "none" + 2. module.serial_numbers should not contain serial numbers for which + imageStaged == "Success" + + Expected results: + 1. module.serial_numbers == ["FDO2112189M", "FDO211218AX", "FDO211218B5"] + 2. module.serial_numbers != ["FDO211218FV", "FDO211218GC"] + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_prune_serial_numbers" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO2112189M", + "FDO211218AX", + "FDO211218B5", + "FDO211218FV", + "FDO211218GC", + ] + module.prune_serial_numbers() + assert isinstance(module.serial_numbers, list) + assert len(module.serial_numbers) == 3 + assert "FDO2112189M" in module.serial_numbers + assert "FDO211218AX" in module.serial_numbers + assert "FDO211218B5" in module.serial_numbers + assert "FDO211218FV" not in module.serial_numbers + assert "FDO211218GC" not in module.serial_numbers + + +# test_validate_serial_numbers_failed + + +def test_validate_serial_numbers_failed(monkeypatch, module, mock_issu_details) -> None: + """ + fail_json is called when imageStaged == "Failed". + + Expectations: + + FDO21120U5D should pass since imageStaged == "Success" + FDO2112189M should fail since imageStaged == "Failed" + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_validate_serial_numbers" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] + + error_message = "Image staging is failing for the following switch: " + error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M." + with pytest.raises(AnsibleFailJson, match=error_message): + module.validate_serial_numbers() + + +# test_wait_for_image_stage_to_complete + + +def test_wait_for_image_stage_to_complete( + monkeypatch, module, mock_issu_details +) -> None: + """ + _wait_for_image_stage_to_complete looks at the imageStaged status for each + serial number and waits for it to be "Success" or "Failed". + In the case where all serial numbers are "Success", the module returns. + In the case where any serial number is "Failed", the module calls fail_json. + + Expectations: + 1. module.serial_numbers_done should be a set() + 2. module.serial_numbers_done should be length 2 + 3. module.serial_numbers_done should contain all serial numbers module.serial_numbers + 4. The module should return without calling fail_json. + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_wait_for_image_stage_to_complete" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 0 + module._wait_for_image_stage_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 2 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" in module.serial_numbers_done + + +# test_wait_for_image_stage_to_complete_stage_failed + + +def test_wait_for_image_stage_to_complete_stage_failed( + monkeypatch, module, mock_issu_details +) -> None: + """ + _wait_for_image_stage_to_complete looks at the imageStaged status for each + serial number and waits for it to be "Success" or "Failed". + In the case where all serial numbers are "Success", the module returns. + In the case where any serial number is "Failed", the module calls fail_json. + + Expectations: + 1. module.serial_numbers_done is a set() + 2. module.serial_numbers_done has length 1 + 3. module.serial_numbers_done contains FDO21120U5D, imageStaged is "Success" + 4. Call fail_json on serial number FDO2112189M, imageStaged is "Failed" + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_wait_for_image_stage_to_complete_fail_json" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 0 + error_message = "Seconds remaining 1800: stage image failed for " + error_message += "cvd-2313-leaf, FDO2112189M, 172.22.150.108. image " + error_message += "staged percent: 90" + with pytest.raises(AnsibleFailJson, match=error_message): + module._wait_for_image_stage_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 1 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" not in module.serial_numbers_done + + +# test_wait_for_image_stage_to_complete_timout + + +def test_wait_for_image_stage_to_complete_timout( + monkeypatch, module, mock_issu_details +) -> None: + """ + See test_wait_for_image_stage_to_complete for functional details. + + Expectations: + 1. module.serial_numbers_done should be a set() + 2. module.serial_numbers_done should be length 1 + 3. module.serial_numbers_done should contain FDO21120U5D + 3. module.serial_numbers_done should not contain FDO2112189M + 4. The function should call fail_json due to timeout + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_wait_for_image_stage_to_complete_timeout" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 1 + module.check_timeout = 1 + + error_message = "NdfcImageStage._wait_for_image_stage_to_complete: " + error_message += "Timed out waiting for image stage to complete. " + error_message += "serial_numbers_done: FDO21120U5D, " + error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + with pytest.raises(AnsibleFailJson, match=error_message): + module._wait_for_image_stage_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 1 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" not in module.serial_numbers_done + + +# test_wait_for_current_actions_to_complete + + +def test_wait_for_current_actions_to_complete( + monkeypatch, module, mock_issu_details +) -> None: + """ + _wait_for_current_actions_to_complete waits until staging, validation, + and upgrade actions are complete for all serial numbers. It calls + NdfcSwitchIssuDetailsBySerialNumber.actions_in_progress() and expects + this to return False. actions_in_progress() returns True until none of + the following keys has a value of "In-Progress": + + ["imageStaged", "upgrade", "validated"] + + Expectations: + 1. module.serial_numbers_done should be a set() + 2. module.serial_numbers_done should be length 2 + 3. module.serial_numbers_done should contain all serial numbers in + module.serial_numbers + 4. The function should return without calling fail_json. + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_wait_for_current_actions_to_complete" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 0 + module._wait_for_current_actions_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 2 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" in module.serial_numbers_done + + +# test_wait_for_current_actions_to_complete_timout + + +def test_wait_for_current_actions_to_complete_timout( + monkeypatch, module, mock_issu_details +) -> None: + """ + See test_wait_for_current_actions_to_complete for functional details. + + Expectations: + 1. module.serial_numbers_done should be a set() + 2. module.serial_numbers_done should be length 1 + 3. module.serial_numbers_done should contain FDO21120U5D + 3. module.serial_numbers_done should not contain FDO2112189M + 4. The function should call fail_json due to timeout + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_wait_for_current_actions_to_complete_timeout" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 1 + module.check_timeout = 1 + + error_message = "NdfcImageStage._wait_for_current_actions_to_complete: " + error_message += "Timed out waiting for actions to complete. " + error_message += "serial_numbers_done: FDO21120U5D, " + error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + with pytest.raises(AnsibleFailJson, match=error_message): + module._wait_for_current_actions_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 1 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" not in module.serial_numbers_done diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py new file mode 100644 index 000000000..d0e39974d --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py @@ -0,0 +1,332 @@ +""" +ndfc_version: 12 +description: Verify functionality of NdfcImageValidate +""" + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcImageValidate, NdfcSwitchIssuDetailsBySerialNumber, NdfcVersion) +from .fixture import load_fixture + +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +def response_data_issu_details(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" + response = load_fixture(response_file).get(key) + print(f"response_data_issu_details: {key} : {response}") + return response + + +def response_data_ndfc_version(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcVersion" + response = load_fixture(response_file).get(key) + print(f"response_data_ndfc_version: {key} : {response}") + return response + + +def response_data_ndfc_image_validate(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcImageValidate" + response = load_fixture(response_file).get(key) + print(f"response_data_ndfc_image_validate: {key} : {response}") + return response + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +@pytest.fixture +def module(): + return NdfcImageValidate(MockAnsibleModule) + + +@pytest.fixture +def mock_issu_details() -> NdfcSwitchIssuDetailsBySerialNumber: + return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) + + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("check_interval") == 10 + assert module.properties.get("check_timeout") == 1800 + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + assert module.properties.get("non_disruptive") == False + assert module.properties.get("serial_numbers") == None + + +def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: + """ + prune_serial_numbers removes serial numbers from the list for which + "validated" == "Success" (TODO: AND policy == ) + + Expectations: + 1. module.serial_numbers should contain only serial numbers for which + "validated" == "none" + 2. module.serial_numbers should not contain serial numbers for which + "validated" == "Success" + + Expected results: + 1. module.serial_numbers == ["FDO2112189M", "FDO211218AX", "FDO211218B5"] + 2. module.serial_numbers != ["FDO211218FV", "FDO211218GC"] + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageValidate_test_prune_serial_numbers" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO2112189M", + "FDO211218AX", + "FDO211218B5", + "FDO211218FV", + "FDO211218GC", + ] + module.prune_serial_numbers() + assert isinstance(module.serial_numbers, list) + assert len(module.serial_numbers) == 3 + assert "FDO2112189M" in module.serial_numbers + assert "FDO211218AX" in module.serial_numbers + assert "FDO211218B5" in module.serial_numbers + assert "FDO211218FV" not in module.serial_numbers + assert "FDO211218GC" not in module.serial_numbers + + +def test_validate_serial_numbers_failed(monkeypatch, module, mock_issu_details) -> None: + """ + fail_json is called when imageStaged == "Failed". + + Expectations: + + FDO21120U5D should pass since validated == "Success" + FDO2112189M should fail since validated == "Failed" + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageValidate_test_validate_serial_numbers" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] + + error_message = "NdfcImageValidate.validate_serial_numbers: " + error_message += "image validation is failing for the following switch: " + error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. If this " + error_message += "persists, check the switch connectivity to NDFC and " + error_message += "try again." + with pytest.raises(AnsibleFailJson, match=error_message): + module.validate_serial_numbers() + + +def test_wait_for_image_validate_to_complete( + monkeypatch, module, mock_issu_details +) -> None: + """ + _wait_for_image_validate_to_complete looks at the "validated" status for each + serial number and waits for it to be "Success" or "Failed". + In the case where all serial numbers are "Success", the module returns. + In the case where any serial number is "Failed", the module calls fail_json. + + Expectations: + 1. module.serial_numbers_done should be a set() + 2. module.serial_numbers_done should be length 2 + 3. module.serial_numbers_done should contain all serial numbers in + module.serial_numbers + 4. The module should return without calling fail_json. + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageValidate_test_wait_for_image_validate_to_complete" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 0 + module._wait_for_image_validate_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 2 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" in module.serial_numbers_done + + +def test_wait_for_image_validate_to_complete_validate_failed( + monkeypatch, module, mock_issu_details +) -> None: + """ + _wait_for_image_validate_to_complete looks at the "validate" status for each + serial number and waits for it to be "Success" or "Failed". + In the case where all serial numbers are "Success", the module returns. + In the case where any serial number is "Failed", the module calls fail_json. + + Expectations: + 1. module.serial_numbers_done is a set() + 2. module.serial_numbers_done has length 1 + 3. module.serial_numbers_done contains FDO21120U5D, "validated" is "Success" + 4. Call fail_json on serial number FDO2112189M, "validated" is "Failed" + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageValidate_test_wait_for_image_validate_to_complete_fail_json" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 0 + error_message = "Seconds remaining 1800: validate image Failed for " + error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M, " + error_message += "image validated percent: 100. Check the switch e.g. " + error_message += "show install log detail, show incompatibility-all nxos " + error_message += ". Or check NDFC Operations > Image Management > " + error_message += "Devices > View Details > Validate for more details." + with pytest.raises(AnsibleFailJson, match=error_message): + module._wait_for_image_validate_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 1 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" not in module.serial_numbers_done + + +def test_wait_for_image_validate_to_complete_timeout( + monkeypatch, module, mock_issu_details +) -> None: + """ + See test_wait_for_image_stage_to_complete for functional details. + + Expectations: + 1. module.serial_numbers_done should be a set() + 2. module.serial_numbers_done should be length 1 + 3. module.serial_numbers_done should contain FDO21120U5D + 3. module.serial_numbers_done should not contain FDO2112189M + 4. The function should call fail_json due to timeout + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageValidate_test_wait_for_image_validate_to_complete_timeout" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 1 + module.check_timeout = 1 + + error_message = "NdfcImageValidate._wait_for_image_validate_to_complete: " + error_message += "Timed out waiting for image validation to complete. " + error_message += "serial_numbers_done: FDO21120U5D, " + error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + with pytest.raises(AnsibleFailJson, match=error_message): + module._wait_for_image_validate_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 1 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" not in module.serial_numbers_done + + +def test_wait_for_current_actions_to_complete( + monkeypatch, module, mock_issu_details +) -> None: + """ + _wait_for_current_actions_to_complete waits until staging, validation, + and upgrade actions are complete for all serial numbers. It calls + NdfcSwitchIssuDetailsBySerialNumber.actions_in_progress() and expects + this to return False. actions_in_progress() returns True until none of + the following keys has a value of "In-Progress": + + ["imageStaged", "upgrade", "validated"] + + Expectations: + 1. module.serial_numbers_done should be a set() + 2. module.serial_numbers_done should be length 2 + 3. module.serial_numbers_done should contain all serial numbers in + module.serial_numbers + 4. The function should return without calling fail_json. + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageValidate_test_wait_for_current_actions_to_complete" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 0 + module._wait_for_current_actions_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 2 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" in module.serial_numbers_done + + +def test_wait_for_current_actions_to_complete_timeout( + monkeypatch, module, mock_issu_details +) -> None: + """ + See test_wait_for_current_actions_to_complete for functional details. + + Expectations: + 1. module.serial_numbers_done should be a set() + 2. module.serial_numbers_done should be length 1 + 3. module.serial_numbers_done should contain FDO21120U5D + 3. module.serial_numbers_done should not contain FDO2112189M + 4. The function should call fail_json due to timeout + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageValidate_test_wait_for_current_actions_to_complete_timeout" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.issu_detail = mock_issu_details + module.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + module.check_interval = 1 + module.check_timeout = 1 + + error_message = "NdfcImageValidate._wait_for_current_actions_to_complete: " + error_message += "Timed out waiting for actions to complete. " + error_message += "serial_numbers_done: FDO21120U5D, " + error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + with pytest.raises(AnsibleFailJson, match=error_message): + module._wait_for_current_actions_to_complete() + assert isinstance(module.serial_numbers_done, set) + assert len(module.serial_numbers_done) == 1 + assert "FDO21120U5D" in module.serial_numbers_done + assert "FDO2112189M" not in module.serial_numbers_done diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByDeviceName.py new file mode 100644 index 000000000..3040f5ee5 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByDeviceName.py @@ -0,0 +1,215 @@ +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ + NdfcSwitchIssuDetailsByDeviceName +from .fixture import load_fixture + +""" +ndfc_version: 12 +description: Verify functionality of subclass NdfcSwitchIssuDetailsByDeviceName +""" +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +# Here, we are using the superclass name, since we are sharing the +# same response file across all subclasses. +class_name = "NdfcSwitchIssuDetails" +response_file = f"dcnm_image_upgrade_responses_{class_name}" + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +def response_data(key: str) -> Dict[str, str]: + response = load_fixture(response_file).get(key) + print(f"response_data: {key} : {response}") + return response + + +@pytest.fixture +def module(): + return NdfcSwitchIssuDetailsByDeviceName(MockAnsibleModule) + + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + action_keys = {"imageStaged", "upgrade", "validated"} + + module._init_properties() + assert isinstance(module.properties, dict) + assert isinstance(module.properties.get("action_keys"), set) + assert module.properties.get("action_keys") == action_keys + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + assert module.properties.get("device_name") == None + + +def test_refresh_return_code_200(monkeypatch, module) -> None: + """ + NDFC response data for 200 response has expected types. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + + """ + key = "packagemgnt_issu_get_return_code_200_one_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_response, dict) + assert isinstance(module.ndfc_data, list) + + +def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: + """ + Properties are set based on device_name setter value. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + """ + key = "packagemgnt_issu_get_return_code_200_many_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + module.device_name = "leaf1" + assert module.device_name == "leaf1" + assert module.serial_number == "FDO21120U5D" + # change device_name to a different switch, expect different information + module.device_name = "cvd-2313-leaf" + assert module.device_name == "cvd-2313-leaf" + assert module.serial_number == "FDO2112189M" + # verify remaining properties using current device_name + assert module.eth_switch_id == 39890 + assert module.fabric == "hard" + assert module.fcoe_enabled == False + assert module.group == "hard" + # NOTE: For "id" see switch_id below + assert module.image_staged == "Success" + assert module.image_staged_percent == 100 + assert module.ip_address == "172.22.150.108" + assert module.issu_allowed == None + assert module.last_upg_action == "2023-Oct-06 03:43" + assert module.mds == False + assert module.mode == "Normal" + assert module.model == "N9K-C93180YC-EX" + assert module.model_type == 0 + assert module.peer == None + assert module.platform == "N9K" + assert module.policy == "KR5M" + assert module.reason == "Upgrade" + assert module.role == "leaf" + assert module.status == "In-Sync" + assert module.status_percent == 100 + # NOTE: switch_id appears in the response data as "id" + # NOTE: "id" is a python reserved keyword, so we changed the property name + assert module.switch_id == 2 + assert module.sys_name == "cvd-2313-leaf" + assert module.system_mode == "Normal" + assert module.upg_groups == None + assert module.upgrade == "Success" + assert module.upgrade_percent == 100 + assert module.validated == "Success" + assert module.validated_percent == 100 + assert module.version == "10.2(5)" + # NOTE: Two vdc_id values exist in the response data for each switch. + # NOTE: Namely, "vdcId" and "vdc_id" + # NOTE: Properties are provided for both, as follows. + # NOTE: vdc_id == vdcId + # NOTE: vdc_id2 == vdc_id + assert module.vdc_id == 0 + assert module.vdc_id2 == -1 + assert module.vpc_peer == None + assert module.vpc_role == None + + +def test_ndfc_result_return_code_200(monkeypatch, module) -> None: + """ + ndfc_result contains expected key/values on 200 response from endpoint. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + """ + key = "packagemgnt_issu_get_return_code_200_one_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_result, dict) + assert module.ndfc_result.get("found") == True + assert module.ndfc_result.get("success") == True + + +def test_ndfc_result_return_code_404(monkeypatch, module) -> None: + """ + fail_json is called on 404 response from malformed endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_404" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "Bad result when retriving switch information from NDFC" + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_ndfc_result_return_code_200_empty_data(monkeypatch, module) -> None: + """ + fail_json is called on 200 response with empty DATA key. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_200_empty_DATA" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcSwitchIssuDetailsByDeviceName.refresh: " + error_message += "NDFC has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( + monkeypatch, module +) -> None: + """ + fail_json is called on 200 response with DATA.lastOperDataObject length 0. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_200" + key += "_ndfc_switch_issu_info_length_0" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcSwitchIssuDetailsByDeviceName.refresh: " + error_message += "NDFC has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByIpAddress.py new file mode 100644 index 000000000..b6bff76a8 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByIpAddress.py @@ -0,0 +1,215 @@ +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ + NdfcSwitchIssuDetailsByIpAddress +from .fixture import load_fixture + +""" +ndfc_version: 12 +description: Verify functionality of subclass NdfcSwitchIssuDetailsByIpAddress +""" +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +# Here, we are using the superclass name, since we are sharing the +# same response file across all subclasses. +class_name = "NdfcSwitchIssuDetails" +response_file = f"dcnm_image_upgrade_responses_{class_name}" + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +def response_data(key: str) -> Dict[str, str]: + response = load_fixture(response_file).get(key) + print(f"response_data: {key} : {response}") + return response + + +@pytest.fixture +def module(): + return NdfcSwitchIssuDetailsByIpAddress(MockAnsibleModule) + + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + action_keys = {"imageStaged", "upgrade", "validated"} + + module._init_properties() + assert isinstance(module.properties, dict) + assert isinstance(module.properties.get("action_keys"), set) + assert module.properties.get("action_keys") == action_keys + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + assert module.properties.get("ip_address") == None + + +def test_refresh_return_code_200(monkeypatch, module) -> None: + """ + NDFC response data for 200 response has expected types. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + + """ + key = "packagemgnt_issu_get_return_code_200_one_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_response, dict) + assert isinstance(module.ndfc_data, list) + + +def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: + """ + Properties are set based on ip_address setter value. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + """ + key = "packagemgnt_issu_get_return_code_200_many_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + module.ip_address = "172.22.150.102" + assert module.device_name == "leaf1" + assert module.serial_number == "FDO21120U5D" + # change ip_address to a different switch, expect different information + module.ip_address = "172.22.150.108" + assert module.device_name == "cvd-2313-leaf" + assert module.serial_number == "FDO2112189M" + # verify remaining properties using current ip_address + assert module.eth_switch_id == 39890 + assert module.fabric == "hard" + assert module.fcoe_enabled == False + assert module.group == "hard" + # NOTE: For "id" see switch_id below + assert module.image_staged == "Success" + assert module.image_staged_percent == 100 + assert module.ip_address == "172.22.150.108" + assert module.issu_allowed == None + assert module.last_upg_action == "2023-Oct-06 03:43" + assert module.mds == False + assert module.mode == "Normal" + assert module.model == "N9K-C93180YC-EX" + assert module.model_type == 0 + assert module.peer == None + assert module.platform == "N9K" + assert module.policy == "KR5M" + assert module.reason == "Upgrade" + assert module.role == "leaf" + assert module.status == "In-Sync" + assert module.status_percent == 100 + # NOTE: switch_id appears in the response data as "id" + # NOTE: "id" is a python reserved keyword, so we changed the property name + assert module.switch_id == 2 + assert module.sys_name == "cvd-2313-leaf" + assert module.system_mode == "Normal" + assert module.upg_groups == None + assert module.upgrade == "Success" + assert module.upgrade_percent == 100 + assert module.validated == "Success" + assert module.validated_percent == 100 + assert module.version == "10.2(5)" + # NOTE: Two vdc_id values exist in the response data for each switch. + # NOTE: Namely, "vdcId" and "vdc_id" + # NOTE: Properties are provided for both, as follows. + # NOTE: vdc_id == vdcId + # NOTE: vdc_id2 == vdc_id + assert module.vdc_id == 0 + assert module.vdc_id2 == -1 + assert module.vpc_peer == None + assert module.vpc_role == None + + +def test_ndfc_result_return_code_200(monkeypatch, module) -> None: + """ + ndfc_result contains expected key/values on 200 response from endpoint. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + """ + key = "packagemgnt_issu_get_return_code_200_one_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_result, dict) + assert module.ndfc_result.get("found") == True + assert module.ndfc_result.get("success") == True + + +def test_ndfc_result_return_code_404(monkeypatch, module) -> None: + """ + fail_json is called on 404 response from malformed endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_404" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "Bad result when retriving switch information from NDFC" + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_ndfc_result_return_code_200_empty_data(monkeypatch, module) -> None: + """ + fail_json is called on 200 response with empty DATA key. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_200_empty_DATA" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcSwitchIssuDetailsByIpAddress.refresh: " + error_message += "NDFC has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( + monkeypatch, module +) -> None: + """ + fail_json is called on 200 response with DATA.lastOperDataObject length 0. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_200" + key += "_ndfc_switch_issu_info_length_0" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcSwitchIssuDetailsByIpAddress.refresh: " + error_message += "NDFC has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsBySerialNumber.py new file mode 100644 index 000000000..7d2449016 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsBySerialNumber.py @@ -0,0 +1,215 @@ +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ + NdfcSwitchIssuDetailsBySerialNumber +from .fixture import load_fixture + +""" +ndfc_version: 12 +description: Verify functionality of subclass NdfcSwitchIssuDetailsBySerialNumber +""" +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +# Here, we are using the superclass name, since we are sharing the +# same response file across all subclasses. +class_name = "NdfcSwitchIssuDetails" +response_file = f"dcnm_image_upgrade_responses_{class_name}" + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +def response_data(key: str) -> Dict[str, str]: + response = load_fixture(response_file).get(key) + print(f"response_data: {key} : {response}") + return response + + +@pytest.fixture +def module(): + return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) + + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + action_keys = {"imageStaged", "upgrade", "validated"} + + module._init_properties() + assert isinstance(module.properties, dict) + assert isinstance(module.properties.get("action_keys"), set) + assert module.properties.get("action_keys") == action_keys + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + assert module.properties.get("serial_number") == None + + +def test_refresh_return_code_200(monkeypatch, module) -> None: + """ + NDFC response data for 200 response has expected types. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + + """ + key = "packagemgnt_issu_get_return_code_200_one_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_response, dict) + assert isinstance(module.ndfc_data, list) + + +def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: + """ + Properties are set based on serial_number setter value. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + """ + key = "packagemgnt_issu_get_return_code_200_many_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + module.serial_number = "FDO21120U5D" + assert module.device_name == "leaf1" + assert module.serial_number == "FDO21120U5D" + # change serial_number to a different switch, expect different information + module.serial_number = "FDO2112189M" + assert module.device_name == "cvd-2313-leaf" + assert module.serial_number == "FDO2112189M" + # verify remaining properties using current serial_number + assert module.eth_switch_id == 39890 + assert module.fabric == "hard" + assert module.fcoe_enabled == False + assert module.group == "hard" + # NOTE: For "id" see switch_id below + assert module.image_staged == "Success" + assert module.image_staged_percent == 100 + assert module.ip_address == "172.22.150.108" + assert module.issu_allowed == None + assert module.last_upg_action == "2023-Oct-06 03:43" + assert module.mds == False + assert module.mode == "Normal" + assert module.model == "N9K-C93180YC-EX" + assert module.model_type == 0 + assert module.peer == None + assert module.platform == "N9K" + assert module.policy == "KR5M" + assert module.reason == "Upgrade" + assert module.role == "leaf" + assert module.status == "In-Sync" + assert module.status_percent == 100 + # NOTE: switch_id appears in the response data as "id" + # NOTE: "id" is a python reserved keyword, so we changed the property name + assert module.switch_id == 2 + assert module.sys_name == "cvd-2313-leaf" + assert module.system_mode == "Normal" + assert module.upg_groups == None + assert module.upgrade == "Success" + assert module.upgrade_percent == 100 + assert module.validated == "Success" + assert module.validated_percent == 100 + assert module.version == "10.2(5)" + # NOTE: Two vdc_id values exist in the response data for each switch. + # NOTE: Namely, "vdcId" and "vdc_id" + # NOTE: Properties are provided for both, as follows. + # NOTE: vdc_id == vdcId + # NOTE: vdc_id2 == vdc_id + assert module.vdc_id == 0 + assert module.vdc_id2 == -1 + assert module.vpc_peer == None + assert module.vpc_role == None + + +def test_ndfc_result_return_code_200(monkeypatch, module) -> None: + """ + ndfc_result contains expected key/values on 200 response from endpoint. + endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + """ + key = "packagemgnt_issu_get_return_code_200_one_switch" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_result, dict) + assert module.ndfc_result.get("found") == True + assert module.ndfc_result.get("success") == True + + +def test_ndfc_result_return_code_404(monkeypatch, module) -> None: + """ + fail_json is called on 404 response from malformed endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_404" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "Bad result when retriving switch information from NDFC" + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_ndfc_result_return_code_200_empty_data(monkeypatch, module) -> None: + """ + fail_json is called on 200 response with empty DATA key. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_200_empty_DATA" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcSwitchIssuDetailsBySerialNumber.refresh: " + error_message += "NDFC has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( + monkeypatch, module +) -> None: + """ + fail_json is called on 200 response with DATA.lastOperDataObject length 0. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "packagemgnt_issu_get_return_code_200" + key += "_ndfc_switch_issu_info_length_0" + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send: {response_data(key)}") + return response_data(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + error_message = "NdfcSwitchIssuDetailsBySerialNumber.refresh: " + error_message += "NDFC has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcVersion.py new file mode 100644 index 000000000..ae15399df --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcVersion.py @@ -0,0 +1,570 @@ +""" +ndfc_version: 12 +description: Verify functionality of NdfcVersion +""" + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import NdfcVersion +from .fixture import load_fixture + +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +def response_data_ndfc_version(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcVersion" + response = load_fixture(response_file).get(key) + print(f"response_data_ndfc_version: {key} : {response}") + return response + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +@pytest.fixture +def module(): + return NdfcVersion(MockAnsibleModule) + + +@pytest.fixture +def mock_ndfc_version() -> NdfcVersion: + return NdfcVersion(MockAnsibleModule) + + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + + +@pytest.mark.parametrize( + "key, expected", + [ + ("NdfcVersion_dev_false", False), + ("NdfcVersion_dev_true", True), + ("NdfcVersion_dev_none", None), + ], +) +def test_dev(monkeypatch, module, key, expected) -> None: + """ + Function description: + + NdfcVersion.dev returns: + - True if NDFC is a development version + - False if NDFC is not a development version + - None otherwise + + Expectations: + + 1. NdfcVersion.dev returns above values given corresponding responses + + Expected results: + + 1. NdfcVersion_dev_false == False + 2. NdfcVersion_dev_true == True + 3. NdfcVersion_dev_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.dev == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("NdfcVersion_install_EASYFABRIC", "EASYFABRIC"), + ("NdfcVersion_install_none", None), + ], +) +def test_install(monkeypatch, module, key, expected) -> None: + """ + Description: + + NdfcVersion.install returns: + - Value of NDFC response "install" key, if present + - None, if NDFC response "install" key is missing + + Expectations: + + 1. NdfcVersion.install returns above values given + corresponding responses + + Expected results: + + 1. NdfcVersion_install_EASYFABRIC == "EASYFABRIC" + 2. NdfcVersion_install_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.install == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("NdfcVersion_is_ha_enabled_true", True), + ("NdfcVersion_is_ha_enabled_false", False), + ("NdfcVersion_is_ha_enabled_none", None), + ], +) +def test_is_ha_enabled(monkeypatch, module, key, expected) -> None: + """ + Function description: + + NdfcVersion.is_ha_enabled returns: + - True, if NDFC response "isHaEnabled" key == "true" + - False, if NDFC response "isHaEnabled" key == "false" + - None, if NDFC response "isHaEnabled" key is missing + + Expectations: + + 1. install returns above values given corresponding responses + + Expected results: + + 1. NdfcVersion_is_ha_enabled_true == True + 2. NdfcVersion_is_ha_enabled_false == False + 3. NdfcVersion_is_ha_enabled_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.is_ha_enabled == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("NdfcVersion_is_media_controller_true", True), + ("NdfcVersion_is_media_controller_false", False), + ("NdfcVersion_is_media_controller_none", None), + ], +) +def test_is_media_controller(monkeypatch, module, key, expected) -> None: + """ + Function description: + + NdfcVersion.is_media_controller returns: + - True, if NDFC response "isMediaController" key == "true" + - False, if NDFC response "isMediaController" key == "false" + - None, if NDFC response "isMediaController" key is missing + + Expectations: + + 1. NdfcVersion.is_media_controller returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_is_media_controller_true == True + 2. NdfcVersion_is_media_controller_false == False + 3. NdfcVersion_is_media_controller_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.is_media_controller == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("NdfcVersion_is_upgrade_inprogress_true", True), + ("NdfcVersion_is_upgrade_inprogress_false", False), + ("NdfcVersion_is_upgrade_inprogress_none", None), + ], +) +def test_is_upgrade_inprogress(monkeypatch, module, key, expected) -> None: + """ + Function description: + + NdfcVersion.is_ha_enabled returns: + - True, if NDFC response "is_upgrade_inprogress" key == "true" + - False, if NDFC response "is_upgrade_inprogress" key == "false" + - None, if NDFC response "is_upgrade_inprogress" key is missing + + Expectations: + + 1. NdfcVersion.is_upgrade_inprogress returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_is_upgrade_inprogress_true == True + 2. NdfcVersion_is_upgrade_inprogress_false == False + 3. NdfcVersion_is_upgrade_inprogress_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.is_upgrade_inprogress == expected + + +def test_ndfc_data_present(monkeypatch, module) -> None: + """ + Function description: + + NdfcVersion.ndfc_data returns the "DATA" key in the + NDFC response, which is a dictionary of key-value pairs. + If the "DATA" key is absent, fail_json is called. + + Expectations: + + 1. NdfcVersion.ndfc_data will return a dictionary of key-value + pairs + + Expected results: + + 1. NdfcVersion_DATA_present, NdfcVersion.ndfc_data == type(dict) + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcVersion_DATA_present" + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_data, dict) + + +def test_ndfc_data_not_present(monkeypatch, module) -> None: + """ + Function description: + + See: test_ndfc_data_present + + Expectations: + + 1. fail_json is called if the "DATA" key is absent + + Expected results: + + 1. fail_json is called + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcVersion_DATA_not_present" + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + with pytest.raises(AnsibleFailJson): + module.refresh() + + +def test_ndfc_result_200(monkeypatch, module) -> None: + """ + Function description: + + NdfcVersion.ndfc_result returns the result of its superclass + method NdfcAnsibleImageUpgradeCommon._handle_response() + + Expectations: + + 1. For a 200 response with "message" key == "OK", + NdfcVersion.ndfc_result == {'found': True, 'success': True} + + Expected results: + + 1. NdfcVersion_result_200 == {'found': True, 'success': True} + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcVersion_get_return_code_200" + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.ndfc_result == {"found": True, "success": True} + + +def test_ndfc_result_404(monkeypatch, module) -> None: + """ + Function description: + + See: test_ndfc_result_200 + + Expectations: + + 1. For a 404 response with "message" key == "Not Found", + NdfcVersion.ndfc_result == {'found': False, 'success': True} + and NdfcVersion.refresh() calls fail_json + + Expected results: + + 1. fail_json is called + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcVersion_get_return_code_404" + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + with pytest.raises(AnsibleFailJson): + module.refresh() + + +def test_ndfc_result_500(monkeypatch, module) -> None: + """ + Function description: + + See: test_ndfc_result_200 + + Expectations: + + 1. For a 500 response with any "message" key value, + NdfcVersion.ndfc_result == {'found': False, 'success': False} + and NdfcVersion.refresh() calls fail_json + + Expected results: + + 1. fail_json is called + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcVersion_get_return_code_500" + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + with pytest.raises(AnsibleFailJson): + module.refresh() + + +@pytest.mark.parametrize( + "key, expected", [("NdfcVersion_mode_LAN", "LAN"), ("NdfcVersion_mode_none", None)] +) +def test_mode(monkeypatch, module, key, expected) -> None: + """ + Function description: + + NdfcVersion.mode returns: + - If NDFC response "mode" key is present, its value + - If NDFC response "mode" key is not present, None + + Expectations: + + 1. NdfcVersion.mode returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_mode_LAN == "LAN" + 2. NdfcVersion_mode_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.mode == expected + + +@pytest.mark.parametrize( + "key, expected", + [("NdfcVersion_uuid_UUID", "foo-uuid"), ("NdfcVersion_uuid_none", None)], +) +def test_uuid(monkeypatch, module, key, expected) -> None: + """ + Function description: + + NdfcVersion.uuid returns: + - If NDFC response "uuid" key is present, its value + - If NDFC response "uuid" key is not present, None + + Expectations: + + 1. NdfcVersion.uuid returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_uuid_UUID == "foo-uuid" + 2. NdfcVersion_uuid_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.uuid == expected + + +@pytest.mark.parametrize( + "key, expected", + [("NdfcVersion_version_12.1.3b", "12.1.3b"), ("NdfcVersion_version_none", None)], +) +def test_version(monkeypatch, module, key, expected) -> None: + """ + Function description: + + NdfcVersion.version returns: + - If NDFC response "version" key is present, its value + - If NDFC response "version" key is not present, None + + Expectations: + + 1. NdfcVersion.version returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_version_12.1.3b == "12.1.3b" + 2. NdfcVersion_version_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.version == expected + + +@pytest.mark.parametrize( + "key, expected", + [("NdfcVersion_version_12.1.3b", "12"), ("NdfcVersion_version_none", None)], +) +def test_version_major(monkeypatch, module, key, expected) -> None: + """ + Function description: + + version_major returns the major version of NDFC + It derives this from the "version" key in the NDFC response + by splitting the string on "." and returning the first element + + NdfcVersion.version_major returns: + - If NDFC response "version" key is present, the major version + - If NDFC response "version" key is not present, None + + Expectations: + + 1. NdfcVersion.version_major returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_version_12.1.3b == "12" + 2. NdfcVersion_version_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.version_major == expected + + +@pytest.mark.parametrize( + "key, expected", + [("NdfcVersion_version_12.1.3b", "1"), ("NdfcVersion_version_none", None)], +) +def test_version_minor(monkeypatch, module, key, expected) -> None: + """ + Function description: + + version_minor returns the minor version of NDFC + It derives this from the "version" key in the NDFC response + by splitting the string on "." and returning the second element + + NdfcVersion.version_minor returns: + - If NDFC response "version" key is present, the minor version + - If NDFC response "version" key is not present, None + + Expectations: + + 1. NdfcVersion.version_minor returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_version_12.1.3b == "1" + 2. NdfcVersion_version_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.version_minor == expected + + +@pytest.mark.parametrize( + "key, expected", + [("NdfcVersion_version_12.1.3b", "3b"), ("NdfcVersion_version_none", None)], +) +def test_version_patch(monkeypatch, module, key, expected) -> None: + """ + Function description: + + version_patch returns the patch version of NDFC + It derives this from the "version" key in the NDFC response + by splitting the string on "." and returning the third element + + NdfcVersion.version_patch returns: + - If NDFC response "version" key is present, the patch version + - If NDFC response "version" key is not present, None + + Expectations: + + 1. NdfcVersion.version_patch returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_version_12.1.3b == "3b" + 2. NdfcVersion_version_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert module.version_patch == expected From 5886f2d8aec66acaa3cf73e56ccae1613f57b373 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 27 Oct 2023 13:22:59 -1000 Subject: [PATCH 002/300] remove commented import --- plugins/modules/dcnm_image_upgrade.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 8d4e610f6..5c473bdc6 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -29,16 +29,6 @@ from time import sleep from ansible.module_utils.basic import AnsibleModule -# from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm_image_upgrade_lib import ( -# NdfcAnsibleImageUpgradeCommon, -# NdfcImageValidate, -# NdfcImagePolicies, -# NdfcImagePolicyAction, -# NdfcImageInstallOptions, -# NdfcImageUpgrade, -# NdfcSwitchDetails, -# NdfcSwitchIssuDetailsByIpAddress -# ) from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, validate_list_of_dicts, From b21d48ab1a957ca9273b5bcc453902467d0fc276 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 27 Oct 2023 13:28:19 -1000 Subject: [PATCH 003/300] rename response_data, more... rename response_data to responses_ndfc_image_upgrade_common Remove class_name and response_file and add response_file var to responses_ndfc_image_upgrade_common Run thru black and isort --- ...mage_upgrade_NdfcAnsibleImageUpgradeCommon.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py index 13329b421..c83d2f467 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py @@ -5,14 +5,13 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( NdfcAnsibleImageUpgradeCommon, NdfcEndpoints) + from .fixture import load_fixture """ ndfc_version: 12 description: Verify functionality of class NdfcAnsibleImageUpgradeCommon """ -class_name = "NdfcAnsibleImageUpgradeCommon" -response_file = f"dcnm_image_upgrade_responses_{class_name}" class MockAnsibleModule: @@ -27,7 +26,8 @@ def module(): return NdfcAnsibleImageUpgradeCommon(MockAnsibleModule) -def response_data(key: str) -> Dict[str, str]: +def responses_ndfc_ansible_image_upgrade_common(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon" response = load_fixture(response_file).get(key) verb = response.get("METHOD") print(f"{key} : {verb} : {response}") @@ -65,7 +65,7 @@ def test_handle_response_post(module, key, expected) -> None: verify _handle_reponse() return values for 200/OK response to POST request """ - data = response_data(key) + data = responses_ndfc_ansible_image_upgrade_common(key) result = module._handle_response(data.get("response"), data.get("verb")) assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") @@ -87,7 +87,7 @@ def test_handle_response_get(module, key, expected) -> None: """ verify _handle_reponse() return values for GET requests """ - data = response_data(key) + data = responses_ndfc_ansible_image_upgrade_common(key) result = module._handle_response(data.get("response"), data.get("verb")) # TODO: We could assert on the dictionary, with a less granular error message # assert result == expected @@ -99,7 +99,7 @@ def test_handle_response_unknown_response_verb(module) -> None: """ verify that fail_json() is called if a unknown request verb is provided """ - data = response_data("mock_unknown_response_verb") + data = responses_ndfc_ansible_image_upgrade_common("mock_unknown_response_verb") with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): module._handle_response(data.get("response"), data.get("verb")) @@ -122,7 +122,7 @@ def test_handle_get_response(module, key, expected) -> None: NOTE: Adding this test increases coverage by 2% according to pytest-cov """ - data = response_data(key) + data = responses_ndfc_ansible_image_upgrade_common(key) result = module._handle_get_response(data.get("response")) assert result.get("success") == expected.get("success") @@ -149,7 +149,7 @@ def test_handle_post_put_delete_response(module, key, expected) -> None: NOTE: This method is covered in test_handle_response_post() above, but... NOTE: Adding this test increases coverage by 2% according to pytest-cov """ - data = response_data(key) + data = responses_ndfc_ansible_image_upgrade_common(key) result = module._handle_post_put_delete_response(data.get("response")) assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") From a1f61b9663f93cac05ef59c4892d39fb871891df Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 27 Oct 2023 13:45:28 -1000 Subject: [PATCH 004/300] fix code path not hit when ERROR key present NdfcAnsibleImageUpgradeCommon._handle_post_put_delete_response: Modified logic to ensure correct result is returned if an ERROR key is present in the response. --- plugins/modules/dcnm_image_upgrade.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 5c473bdc6..13b2b96cd 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -618,11 +618,11 @@ def _handle_post_put_delete_response(self, response): - True otherwise """ result = {} - if response.get("MESSAGE") != "OK": + if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: result["success"] = False result["changed"] = False return result - if response.get("ERROR"): + if response.get("ERROR") is not None: result["success"] = False result["changed"] = False return result From 898af12fcae2adb7714c24412d9697440e08b691 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 27 Oct 2023 16:46:05 -1000 Subject: [PATCH 005/300] NdfcSwitchDetails._get() enhancement, more... NdfcSwitchDetails._get() - Convert values with make_boolean() and make_none() before returning them. NdfcAnsibleImageUpgradeCommon._handle_post_put_delete_response: Another tweak to the logic to ensure ERROR case is reached. Update comments and docstrings. --- plugins/modules/dcnm_image_upgrade.py | 31 ++++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 13b2b96cd..d55bd1bc0 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -618,11 +618,11 @@ def _handle_post_put_delete_response(self, response): - True otherwise """ result = {} - if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: + if response.get("ERROR") is not None: result["success"] = False result["changed"] = False return result - if response.get("ERROR") is not None: + if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: result["success"] = False result["changed"] = False return result @@ -1516,13 +1516,13 @@ class NdfcSwitchDetails(NdfcAnsibleImageUpgradeCommon): Usage (where module is an instance of AnsibleModule): instance = NdfcSwitchDetails(module) + instance.refresh() instance.ip_address = 10.1.1.1 fabric_name = instance.fabric_name serial_number = instance.serial_number etc... - Switch details are retrieved on instantiation of this class. - Switch details can be refreshed by calling instance.refresh(). + Switch details are retrieved by calling instance.refresh(). Endpoint: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches @@ -1532,7 +1532,6 @@ def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ self._init_properties() - # self.refresh() def _init_properties(self): self.properties = {} @@ -1552,11 +1551,12 @@ def refresh(self): self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response_1 {self.ndfc_response}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response_2 {self.ndfc_response}" + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response {self.ndfc_response}" self.log_msg(msg) self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_result {self.ndfc_result}" + self.log_msg(msg) + if self.ndfc_response["RETURN_CODE"] != 200: msg = "Unable to retrieve switch information from NDFC. " msg += f"Got response {self.ndfc_response}" @@ -1567,12 +1567,19 @@ def refresh(self): for switch in data: self.properties["ndfc_data"][switch["ipAddress"]] = switch + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_data {self.ndfc_data}" + self.log_msg(msg) + def _get(self, item): if self.ip_address is None: msg = f"{self.class_name}: set instance.ip_address " msg += f"before accessing property {item}." self.module.fail_json(msg) - return self.properties["ndfc_data"][self.ip_address].get(item) + return self.make_boolean( + self.make_none( + self.properties["ndfc_data"][self.ip_address].get(item) + ) + ) @property def ip_address(self): @@ -1626,8 +1633,10 @@ def model(self): @property def ndfc_data(self): """ - Return the parsed data from the GET request. + Return parsed data from the GET request. Return None otherwise + + NOTE: Keyed on ip_address """ return self.properties["ndfc_data"] @@ -1652,6 +1661,8 @@ def platform(self): """ Return the platform of the switch with ip_address, if it exists. Return None otherwise + + NOTE: This is derived from "model" and is not in the NDFC response """ model = self._get("model") if model is None: From a5ed401e687e171678a445041466e7d1db6d10e8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 27 Oct 2023 16:46:32 -1000 Subject: [PATCH 006/300] NdfcSwitchDetails - initial unit tests --- ...e_upgrade_responses_NdfcSwitchDetails.json | 252 ++++++++++++++++++ ...st_dcnm_image_upgrade_NdfcSwitchDetails.py | 243 +++++++++++++++++ 2 files changed, 495 insertions(+) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchDetails.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchDetails.json new file mode 100644 index 000000000..84cd03d90 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchDetails.json @@ -0,0 +1,252 @@ +{ + "NdfcSwitchDetails_get_return_code_200": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [ + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.110", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1111-bgw", + "switchDbID": 158560, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "15 days, 03:37:10", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PGCT", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-1111-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1400, + "swUUID": "DCNM-UUID-1400", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.111", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1112-bgw", + "switchDbID": 160170, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "15 days, 03:36:55", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PGD1", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-1112-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1200, + "swUUID": "DCNM-UUID-1200", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Healthy", + "intentedpeerName": "" + } + ] + }, + "NdfcSwitchDetails_get_return_code_404": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitchess", + "MESSAGE": "Not Found", + "DATA": {} + }, + "NdfcSwitchDetails_get_return_code_500": { + "RETURN_CODE": 500, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "Internal Server Error", + "DATA": {} + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py new file mode 100644 index 000000000..f32ca3661 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py @@ -0,0 +1,243 @@ +""" +ndfc_version: 12 +description: Verify functionality of NdfcSwitchDetails +""" +from contextlib import contextmanager +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import NdfcSwitchDetails, NdfcVersion, NdfcAnsibleImageUpgradeCommon +from .fixture import load_fixture + +@contextmanager +def does_not_raise(): + yield + +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +def responses_ndfc_switch_details(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcSwitchDetails" + response = load_fixture(response_file).get(key) + print(f"responses_ndfc_switch_details: {key} : {response}") + return response + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +@pytest.fixture +def module(): + return NdfcSwitchDetails(MockAnsibleModule) + # return NdfcSwitchDetails(NdfcAnsibleImageUpgradeCommon(MockAnsibleModule)) + + +@pytest.fixture +def mock_ndfc_version() -> NdfcVersion: + return NdfcVersion(MockAnsibleModule) + + +def test_init(module) -> None: + module.__init__(MockAnsibleModule) + assert isinstance(module, NdfcSwitchDetails) + assert module.class_name == "NdfcSwitchDetails" + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("ip_address") == None + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + +def test_ip_address_not_set(module) -> None: + """ + Function description: + + NdfcSwitchDetails.ip_address returns: + - IP Address, if the user has set ip_address + - None, if the user has not already set ip_address + + Expected results: + + 1. instance.ip_address will return None + """ + assert module.ip_address == None + +def test_ip_address_is_set(module) -> None: + """ + Function description: + + NdfcSwitchDetails.ip_address returns: + - IP Address, if the user has set ip_address + - None, if the user has not already set ip_address + + Expected results: + + 1. instance.ip_address will return the value set by the user + """ + module.ip_address = "1.2.3.4" + assert module.ip_address == "1.2.3.4" + +def test_refresh(monkeypatch, module) -> None: + """ + Function description: + + NdfcSwitchDetails.refresh sets the following properties: + - ndfc_data + - ndfc_response + - ndfc_result + + Expected results: + + 1. instance.ndfc_data is a dictionary + 2. instance.ndfc_response is a dictionary + 3. instance.ndfc_data is a list + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcSwitchDetails_get_return_code_200" + return responses_ndfc_switch_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_data, dict) + assert isinstance(module.ndfc_result, dict) + assert isinstance(module.ndfc_response, dict) + + +def test_refresh_ndfc_data(monkeypatch, module) -> None: + """ + Function description: + + See test_refresh + + Expected results: + + 1. instance.ndfc_data is a dictionary + 2. When instance.ip_address is set, getter properties will return values specific to ip_address + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcSwitchDetails_get_return_code_200" + return responses_ndfc_switch_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + assert isinstance(module.ndfc_data, dict) + module.ip_address = "172.22.150.110" + assert module.hostname == "cvd-1111-bgw" + module.ip_address = "172.22.150.111" + # We use the above IP address to test the remaining properties + assert module.fabric_name == "easy" + assert module.hostname == "cvd-1112-bgw" + assert module.logical_name == "cvd-1112-bgw" + assert module.model == "N9K-C9504" + # This is derived from "model" and is not in the NDFC response + assert module.platform == "N9K" + assert module.role == "border gateway" + assert module.serial_number == "FOX2109PGD1" + + +match = "Unable to retrieve switch information from NDFC. " +@pytest.mark.parametrize( + "key,expected", + [ + ("NdfcSwitchDetails_get_return_code_200", does_not_raise()), + ("NdfcSwitchDetails_get_return_code_404", pytest.raises(AnsibleFailJson, match=match)), + ("NdfcSwitchDetails_get_return_code_500", pytest.raises(AnsibleFailJson, match=match)), + ] +) +def test_ndfc_result(monkeypatch, module, key, expected) -> None: + """ + Function description: + + NdfcSwitchDetails.ndfc_result returns the result of its superclass + method NdfcAnsibleImageUpgradeCommon._handle_response() + + Expectations: + + 1. 200 RETURN_CODE, MESSAGE == "OK", + NdfcSwitchDetails.ndfc_result == {'found': True, 'success': True} + + 2. 404 RETURN_CODE, MESSAGE == "Not Found", + NdfcSwitchDetails.ndfc_result == {'found': False, 'success': True} + + 3. 500 RETURN_CODE, MESSAGE ~= "Internal Server Error", + NdfcSwitchDetails.ndfc_result == {'found': False, 'success': False} + + Expected results: + + 1. NdfcSwitchDetails_result_200 == {'found': True, 'success': True} + 2. NdfcSwitchDetails_result_404 == {'found': False, 'success': True} + 3. NdfcSwitchDetails_result_500 == {'found': False, 'success': False} + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return responses_ndfc_switch_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + with expected: + module.refresh() + + +@pytest.mark.parametrize( + "item, expected", [ + ("fabricName", "easy"), + ("hostName", "cvd-1111-bgw"), + ("licenseViolation", False), + ("location", None), + ("logicalName", "cvd-1111-bgw"), + ("managable", True), + ("model", "N9K-C9504"), + ("present", True), + ("serialNumber", "FOX2109PGCT"), + ("switchRole", "border gateway"), + ], +) +def test_get_with_ip_address_set(monkeypatch, module, item, expected) -> None: + """ + Function description: + + NdfcSwitchDetails._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set ip_address. + It returns the value of the requested property if the user has set ip_address. + The property value is passed to both make_boolean() and make_none(), which + either: + - converts it to a boolean + - converts it to NoneType + - returns the value unchanged + + Expectations: + + 1. NdfcVersion._get returns above values + given corresponding responses + + Expected results: + + 1. NdfcVersion_mode_LAN == "LAN" + 2. NdfcVersion_mode_none == None + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcSwitchDetails_get_return_code_200" + return responses_ndfc_switch_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.refresh() + module.ip_address = "172.22.150.110" + assert module._get(item) == expected + + From 1306822985bc73f2f042a047ce976913300176ab Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 30 Oct 2023 13:41:09 -1000 Subject: [PATCH 007/300] Changes for testability Promote verb, path, payload to class-level attributes. --- plugins/modules/dcnm_image_upgrade.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index d55bd1bc0..77b8338aa 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -905,7 +905,7 @@ def _build_params_spec_for_merged_state(): Caller: _validate_input_for_merged_state() Return: params_spec, a dictionary containing the set of - parameter specifications. + playbook parameter specifications. """ params_spec = {} params_spec["policy"] = {} @@ -3541,6 +3541,9 @@ def __init__(self, module): self._init_properties() self.serial_numbers_done = set() self.ndfc_version = None + self.path = None + self.verb = None + self.payload = None self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): @@ -3618,20 +3621,22 @@ def commit(self): self.validate_serial_numbers() self._wait_for_current_actions_to_complete() - path = self.endpoints.image_stage.get("path") - verb = self.endpoints.image_stage.get("verb") + self.path = self.endpoints.image_stage.get("path") + self.verb = self.endpoints.image_stage.get("verb") - payload = {} + self.log_msg(f"REMOVE: {self.class_name}.commit() path: {self.path}") + self.log_msg(f"REMOVE: {self.class_name}.commit() verb: {self.verb}") + self.payload = {} self._populate_ndfc_version() if self.ndfc_version == "12.1.2e": - # Yes, NDFC wants serialNum to be misspelled - payload["sereialNum"] = self.serial_numbers + # Yes, NDFC 12.1.2e wants serialNum to be misspelled + self.payload["sereialNum"] = self.serial_numbers else: - payload["serialNumbers"] = self.serial_numbers + self.payload["serialNumbers"] = self.serial_numbers self.properties["ndfc_response"] = dcnm_send( - self.module, verb, path, data=json.dumps(payload) + self.module, self.verb, self.path, data=json.dumps(self.payload) ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, self.verb) self.log_msg( f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" ) From 4c249683cdb5a04b1eed4acebf66252e19fc2b7c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 30 Oct 2023 13:47:08 -1000 Subject: [PATCH 008/300] Add tests, more... Added the following tests: test_populate_ndfc_version test_commit_serial_numbers test_commit_path_verb test_commit_payload_serial_number_key_name --- ...grade_responses_NdfcSwitchIssuDetails.json | 90 ++++++++++ ...m_image_upgrade_responses_NdfcVersion.json | 32 ++++ .../test_dcnm_image_upgrade_NdfcImageStage.py | 170 +++++++++++++++++- 3 files changed, 283 insertions(+), 9 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json index 07b72260c..7e2f5ad73 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json @@ -1502,6 +1502,96 @@ "message": "" } }, + "NdfcImageStage_test_commit_payload_serial_number_key_name": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, "NdfcImageValidate_test_prune_serial_numbers": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json index b75eb4eb0..5b1970e84 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json @@ -359,5 +359,37 @@ "uuid": "", "is_upgrade_inprogress": "false" } + }, + "NdfcImageStage_12_1_2e": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.2e", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "NdfcImageStage_12_1_3b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py index f754b40ab..99e837245 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py @@ -1,10 +1,10 @@ """ ndfc_version: 12 description: Verify functionality of NdfcImageStage -TODO:2 NdfcImageStage._populate_ndfc_version unit test TODO:2 NdfcImageStage.commit unit test """ +from contextlib import contextmanager from typing import Any, Dict import pytest @@ -13,9 +13,19 @@ from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( NdfcEndpoints, NdfcImageStage, NdfcSwitchIssuDetailsBySerialNumber, NdfcVersion) + from .fixture import load_fixture -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +@contextmanager +def does_not_raise(): + yield + + +dcnm_send_patch = ( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +) + def response_data_issu_details(key: str) -> Dict[str, str]: response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" @@ -31,13 +41,6 @@ def response_data_ndfc_version(key: str) -> Dict[str, str]: return response -def response_data_ndfc_image_stage(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcImageStage" - response = load_fixture(response_file).get(key) - print(f"response_data_ndfc_image_stage: {key} : {response}") - return response - - class MockAnsibleModule: params = {} @@ -68,6 +71,9 @@ class attributes are initialized to expected values assert isinstance(module.properties, dict) assert isinstance(module.serial_numbers_done, set) assert module.ndfc_version == None + assert module.path == None + assert module.verb == None + assert module.payload == None assert isinstance(module.issu_detail, NdfcSwitchIssuDetailsBySerialNumber) assert isinstance(module.endpoints, NdfcEndpoints) @@ -89,6 +95,40 @@ def test_init_properties(module) -> None: assert module.properties.get("check_timeout") == 1800 +# test_populate_ndfc_version + + +@pytest.mark.parametrize( + "key, expected", + [ + ("NdfcImageStage_12_1_2e", "12.1.2e"), + ("NdfcImageStage_12_1_3b", "12.1.3b"), + ], +) +def test_populate_ndfc_version(monkeypatch, module, key, expected) -> None: + """ + _populate_ndfc_version retrieves the controller version from NDFC. + This is used in commit() to populate the payload with either a misspelled + "sereialNum" key/value (12.1.2e) or a correctly-spelled "serialNumbers" + key/value (12.1.3b). + + Expectations: + 1. module.ndfc_version should be set + + Expected results: + 1. NdfcImageStage_12_1_2e -> module.ndfc_version == "12.1.2e" + 2. NdfcImageStage_12_1_3b -> module.ndfc_version == "12.1.3b" + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + return response_data_ndfc_version(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module._populate_ndfc_version() + assert module.ndfc_version == expected + + # test_prune_serial_numbers @@ -160,6 +200,118 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: module.validate_serial_numbers() +# test_commit_serial_numbers + +match = r"NdfcImageStage.commit\(\) call instance.serial_numbers " +match += r"before calling commit\(\)." + + +@pytest.mark.parametrize( + "serial_numbers_is_set, expected", + [ + (True, does_not_raise()), + (False, pytest.raises(AnsibleFailJson, match=match)), + ], +) +def test_commit_serial_numbers( + monkeypatch, module, serial_numbers_is_set, expected +) -> None: + """ + fail_json is called when NdfcImageStage.commit() is called without + setting instance.serial_numbers. + + Expectations: + + 1. fail_json is called when serial_numbers is None + 2. fail_json is not called when serial_numbers is set + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_validate_serial_numbers" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + if serial_numbers_is_set: + module.serial_numbers = ["FDO21120U5D"] + with expected: + module.commit() + + +# test_commit_path_verb + + +def test_commit_path_verb(monkeypatch, module) -> None: + """ + NdfcImageStage.path should be set to: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image + + NdfcImageStage.verb should be set to: + POST + + Expectations: + + 1. both self.path and self.verb should be set, per above + """ + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_validate_serial_numbers" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.serial_numbers = ["FDO21120U5D"] + module.commit() + assert ( + module.path + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image" + ) + assert module.verb == "POST" + + +# test_commit_payload_serial_number_key_name + + +@pytest.mark.parametrize( + "ndfc_version, expected_serial_number_key", + [ + ("12.1.2e", "sereialNum"), + ("12.1.3b", "serialNumbers"), + ], +) +def test_commit_payload_serial_number_key_name( + monkeypatch, module, ndfc_version, expected_serial_number_key +) -> None: + """ + commit() will set the payload key name for the serial number + based on the NDFC version, per Expected Results below: + + Expectations: + 1. The correct serial number key name should be used based on NDFC version + + Expected results: + ndfc_version 12.1.2e -> key name "sereialNum" (yes, misspelled) + ndfc_version 12.1.3b -> key name "serialNumbers + """ + + def mock_ndfc_version(*args, **kwargs) -> None: + module.ndfc_version = ndfc_version + + ndfc_version_patch = "ansible_collections.cisco.dcnm.plugins.modules." + ndfc_version_patch += "dcnm_image_upgrade.NdfcImageStage._populate_ndfc_version" + monkeypatch.setattr(ndfc_version_patch, mock_ndfc_version) + + def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_commit_payload_serial_number_key_name" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + + module.serial_numbers = ["FDO21120U5D"] + module.commit() + assert expected_serial_number_key in module.payload.keys() + + # test_wait_for_image_stage_to_complete From ad04c9e29720dfd755f2948196758542be137ab8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 30 Oct 2023 13:49:44 -1000 Subject: [PATCH 009/300] Remove uneeded code, run through black and isort --- .../test_dcnm_image_upgrade_NdfcEndpoints.py | 25 +--------- ...cnm_image_upgrade_NdfcImagePolicyAction.py | 50 ++++--------------- 2 files changed, 10 insertions(+), 65 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py index d5823f7a3..42a6fef67 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py @@ -1,6 +1,5 @@ -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ NdfcEndpoints -) """ ndfc_version: 12 @@ -217,25 +216,3 @@ def test_dcnm_image_upgrade_endpoints_switches_info() -> None: endpoints.switches_info.get("path") == "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches" ) - - -# Example of using monkeypatch if we need to patch a property -# Not needed in this case, but keep this around for reference -# def test_dcnm_image_upgrade_endpoints_image_stage_monkeypatch(monkeypatch) -> None: -# """ -# :param monkeypatch: -# :return: None -# """ -# @property -# def mock_image_stage(self) -> dict: -# path = f"/stage-image" -# endpoint = {} -# endpoint["path"] = path -# endpoint["verb"] = "POST" -# return endpoint - -# monkeypatch.setattr("dcnm_image_upgrade.dcnm_image_upgrade.NdfcEndpoints.image_stage", mock_image_stage) - -# endpoints = NdfcEndpoints() -# assert endpoints.image_stage.get("verb") == "POST" -# assert endpoints.image_stage.get("path") == "/stage-image" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py index a41249a6c..da4d4beb2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py @@ -12,6 +12,7 @@ from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( NdfcImagePolicies, NdfcImagePolicyAction, NdfcSwitchIssuDetailsBySerialNumber) + from .fixture import load_fixture @@ -19,7 +20,11 @@ def does_not_raise(): yield -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +dcnm_send_patch = ( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +) + def response_data_issu_details(key: str) -> Dict[str, str]: response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" @@ -28,20 +33,6 @@ def response_data_issu_details(key: str) -> Dict[str, str]: return response -def response_data_ndfc_image_policies(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcImagePolicies" - response = load_fixture(response_file).get(key) - print(f"response_data_ndfc_image_policies: {key} : {response}") - return response - - -def response_data_ndfc_image_policy_actioin(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcImagePolicyAction" - response = load_fixture(response_file).get(key) - print(f"response_data_ndfc_image_policy_action: {key} : {response}") - return response - - class MockAnsibleModule: params = {} @@ -59,11 +50,6 @@ def mock_issu_details() -> NdfcSwitchIssuDetailsBySerialNumber: return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) -@pytest.fixture -def mock_ndfc_image_policies() -> NdfcImagePolicies: - return NdfcImagePolicies(MockAnsibleModule) - - # test_init @@ -163,7 +149,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: # test_validate_request_policy_name_none -def test_validate_request_action_none(monkeypatch, module, mock_issu_details) -> None: +def test_validate_request_action_none(module, mock_issu_details) -> None: """ validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a @@ -173,12 +159,6 @@ def test_validate_request_action_none(monkeypatch, module, mock_issu_details) -> 1. module.fail_json should be called because module.action is None """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImagePolicyAction_test_build_attach_payload_fail_json" - return response_data_issu_details(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - module.switch_issu_details = mock_issu_details module.policy_name = "KR5M" module.serial_numbers = [ @@ -205,7 +185,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: ], ) def test_validate_request_policy_name_none( - monkeypatch, action, expected, module, mock_issu_details + action, expected, module, mock_issu_details ) -> None: """ validate_request performs a number of validations prior to calling commit @@ -216,12 +196,6 @@ def test_validate_request_policy_name_none( 1. module.fail_json should be called because module.policy_name is None """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImagePolicyAction_test_build_attach_payload_fail_json" - return response_data_issu_details(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - module.switch_issu_details = mock_issu_details module.action = action module.serial_numbers = [ @@ -247,7 +221,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: ], ) def test_validate_request_serial_numbers_none( - monkeypatch, action, expected, module, mock_issu_details + action, expected, module, mock_issu_details ) -> None: """ validate_request performs a number of validations prior to calling commit @@ -260,12 +234,6 @@ def test_validate_request_serial_numbers_none( 3. action == query module.fail_json should NOT be called """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImagePolicyAction_test_build_attach_payload_fail_json" - return response_data_issu_details(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - module.switch_issu_details = mock_issu_details module.action = action module.policy_name = "KR5M" From 81061190d22d4b47e217b121e979089a79812be3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 30 Oct 2023 15:22:00 -1000 Subject: [PATCH 010/300] Remove unused functions --- .../test_dcnm_image_upgrade_NdfcImageValidate.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py index d0e39974d..a1e5f3d0f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py @@ -21,20 +21,6 @@ def response_data_issu_details(key: str) -> Dict[str, str]: return response -def response_data_ndfc_version(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcVersion" - response = load_fixture(response_file).get(key) - print(f"response_data_ndfc_version: {key} : {response}") - return response - - -def response_data_ndfc_image_validate(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcImageValidate" - response = load_fixture(response_file).get(key) - print(f"response_data_ndfc_image_validate: {key} : {response}") - return response - - class MockAnsibleModule: params = {} From a03b21b08d72bb5fc6188bcb5bcbf37a043a1e01 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 30 Oct 2023 15:26:34 -1000 Subject: [PATCH 011/300] Remove ununsed function, run thru black/isort --- ...st_dcnm_image_upgrade_NdfcSwitchDetails.py | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py index f32ca3661..c15570e67 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py @@ -8,14 +8,21 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import NdfcSwitchDetails, NdfcVersion, NdfcAnsibleImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcAnsibleImageUpgradeCommon, NdfcSwitchDetails, NdfcVersion) + from .fixture import load_fixture + @contextmanager def does_not_raise(): yield -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +dcnm_send_patch = ( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +) + def responses_ndfc_switch_details(key: str) -> Dict[str, str]: response_file = f"dcnm_image_upgrade_responses_NdfcSwitchDetails" @@ -34,12 +41,6 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture def module(): return NdfcSwitchDetails(MockAnsibleModule) - # return NdfcSwitchDetails(NdfcAnsibleImageUpgradeCommon(MockAnsibleModule)) - - -@pytest.fixture -def mock_ndfc_version() -> NdfcVersion: - return NdfcVersion(MockAnsibleModule) def test_init(module) -> None: @@ -47,6 +48,7 @@ def test_init(module) -> None: assert isinstance(module, NdfcSwitchDetails) assert module.class_name == "NdfcSwitchDetails" + def test_init_properties(module) -> None: """ Properties are initialized to expected values @@ -58,6 +60,7 @@ def test_init_properties(module) -> None: assert module.properties.get("ndfc_response") == None assert module.properties.get("ndfc_result") == None + def test_ip_address_not_set(module) -> None: """ Function description: @@ -72,6 +75,7 @@ def test_ip_address_not_set(module) -> None: """ assert module.ip_address == None + def test_ip_address_is_set(module) -> None: """ Function description: @@ -87,6 +91,7 @@ def test_ip_address_is_set(module) -> None: module.ip_address = "1.2.3.4" assert module.ip_address == "1.2.3.4" + def test_refresh(monkeypatch, module) -> None: """ Function description: @@ -150,13 +155,21 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: match = "Unable to retrieve switch information from NDFC. " + + @pytest.mark.parametrize( "key,expected", [ ("NdfcSwitchDetails_get_return_code_200", does_not_raise()), - ("NdfcSwitchDetails_get_return_code_404", pytest.raises(AnsibleFailJson, match=match)), - ("NdfcSwitchDetails_get_return_code_500", pytest.raises(AnsibleFailJson, match=match)), - ] + ( + "NdfcSwitchDetails_get_return_code_404", + pytest.raises(AnsibleFailJson, match=match), + ), + ( + "NdfcSwitchDetails_get_return_code_500", + pytest.raises(AnsibleFailJson, match=match), + ), + ], ) def test_ndfc_result(monkeypatch, module, key, expected) -> None: """ @@ -175,7 +188,7 @@ def test_ndfc_result(monkeypatch, module, key, expected) -> None: 3. 500 RETURN_CODE, MESSAGE ~= "Internal Server Error", NdfcSwitchDetails.ndfc_result == {'found': False, 'success': False} - + Expected results: 1. NdfcSwitchDetails_result_200 == {'found': True, 'success': True} @@ -193,7 +206,8 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( - "item, expected", [ + "item, expected", + [ ("fabricName", "easy"), ("hostName", "cvd-1111-bgw"), ("licenseViolation", False), @@ -239,5 +253,3 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: module.refresh() module.ip_address = "172.22.150.110" assert module._get(item) == expected - - From f1e32cf957ff053e91bf6eb8c632fbfa63bc1566 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 30 Oct 2023 16:28:53 -1000 Subject: [PATCH 012/300] Fix off-by-one error, more... __init__() - remove NdfcSwitchIssuDetailsByIpAddress.refresh() _init_properties() - Fix off-by-one error when setting valid_epld_module. _populate_ndfc_version() - Remove unused function --- plugins/modules/dcnm_image_upgrade.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 77b8338aa..6f921d15e 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -4311,9 +4311,7 @@ def __init__(self, module): self._init_defaults() self._init_properties() - self._populate_ndfc_version() self.issu_detail = NdfcSwitchIssuDetailsByIpAddress(self.module) - self.issu_detail.refresh() def _init_defaults(self): self.defaults = {} @@ -4365,27 +4363,16 @@ def _init_properties(self): self.properties["reboot"] = False self.properties["write_erase"] = False - self.valid_nxos_mode = set() - self.valid_nxos_mode.add("disruptive") - self.valid_nxos_mode.add("non_disruptive") - self.valid_nxos_mode.add("force_non_disruptive") - self.valid_epld_module = set() self.valid_epld_module.add("ALL") - for module in range(1, self.max_module_number): + for module in range(1, self.max_module_number + 1): self.valid_epld_module.add(str(module)) - def _populate_ndfc_version(self): - """ - Populate self.ndfc_version with the NDFC version. + self.valid_nxos_mode = set() + self.valid_nxos_mode.add("disruptive") + self.valid_nxos_mode.add("non_disruptive") + self.valid_nxos_mode.add("force_non_disruptive") - Notes: - 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular - imports resulting in RecursionError - """ - instance = NdfcVersion(self.module) - instance.refresh() - self.ndfc_version = instance.version # def prune_devices(self): # """ From 41a514cfc3d14193b6f01a6e19cb845179e81917 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 30 Oct 2023 16:34:15 -1000 Subject: [PATCH 013/300] NdfcImageUpgrade - initial test cases --- ...est_dcnm_image_upgrade_NdfcImageUpgrade.py | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageUpgrade.py diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageUpgrade.py new file mode 100644 index 000000000..623364938 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageUpgrade.py @@ -0,0 +1,292 @@ +""" +ndfc_version: 12 +description: Verify functionality of NdfcSwitchUpgrade +""" +from contextlib import contextmanager +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( + NdfcImageUpgrade, NdfcSwitchDetails, NdfcVersion) + +from .fixture import load_fixture + + +@contextmanager +def does_not_raise(): + yield + + +dcnm_send_patch = ( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +) + + +def responses_ndfc_switch_details(key: str) -> Dict[str, str]: + response_file = f"dcnm_image_upgrade_responses_NdfcSwitchDetails" + response = load_fixture(response_file).get(key) + print(f"responses_ndfc_switch_details: {key} : {response}") + return response + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +@pytest.fixture +def module(): + return NdfcImageUpgrade(MockAnsibleModule) + + +def test_init(module) -> None: + module.__init__(MockAnsibleModule) + assert isinstance(module, NdfcImageUpgrade) + assert module.class_name == "NdfcImageUpgrade" + assert module.max_module_number == 9 + +def test_init_defaults(module) -> None: + """ + Defaults are initialized to expected values + """ + module._init_defaults() + assert isinstance(module.defaults, dict) + assert module.defaults["reboot"] == False + assert module.defaults["stage"] == True + assert module.defaults["validate"] == True + assert module.defaults["upgrade"]["nxos"] == True + assert module.defaults["upgrade"]["epld"] == False + assert module.defaults["options"]["nxos"]["mode"] == "disruptive" + assert module.defaults["options"]["nxos"]["bios_force"] == False + assert module.defaults["options"]["epld"]["module"] == "ALL" + assert module.defaults["options"]["epld"]["golden"] == False + assert module.defaults["options"]["reboot"]["config_reload"] == False + assert module.defaults["options"]["reboot"]["write_erase"] == False + assert module.defaults["options"]["package"]["install"] == False + assert module.defaults["options"]["package"]["uninstall"] == False + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("bios_force") == False + assert module.properties.get("check_interval") == 10 + assert module.properties.get("check_timeout") == 1800 + assert module.properties.get("config_reload") == False + assert module.properties.get("devices") == None + assert module.properties.get("disruptive") == True + assert module.properties.get("epld_golden") == False + assert module.properties.get("epld_module") == "ALL" + assert module.properties.get("epld_upgrade") == False + assert module.properties.get("force_non_disruptive") == False + assert module.properties.get("ndfc_data") == None + assert module.properties.get("ndfc_response") == None + assert module.properties.get("ndfc_result") == None + assert module.properties.get("non_disruptive") == False + assert module.properties.get("force_non_disruptive") == False + assert module.properties.get("package_install") == False + assert module.properties.get("package_uninstall") == False + assert module.properties.get("reboot") == False + assert module.properties.get("write_erase") == False + assert module.valid_epld_module == {"ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9"} + assert module.valid_nxos_mode == {"disruptive", "non_disruptive", "force_non_disruptive"} + + +# def test_ip_address_not_set(module) -> None: +# """ +# Function description: + +# NdfcSwitchDetails.ip_address returns: +# - IP Address, if the user has set ip_address +# - None, if the user has not already set ip_address + +# Expected results: + +# 1. instance.ip_address will return None +# """ +# assert module.ip_address == None + + +# def test_ip_address_is_set(module) -> None: +# """ +# Function description: + +# NdfcSwitchDetails.ip_address returns: +# - IP Address, if the user has set ip_address +# - None, if the user has not already set ip_address + +# Expected results: + +# 1. instance.ip_address will return the value set by the user +# """ +# module.ip_address = "1.2.3.4" +# assert module.ip_address == "1.2.3.4" + + +# def test_refresh(monkeypatch, module) -> None: +# """ +# Function description: + +# NdfcSwitchDetails.refresh sets the following properties: +# - ndfc_data +# - ndfc_response +# - ndfc_result + +# Expected results: + +# 1. instance.ndfc_data is a dictionary +# 2. instance.ndfc_response is a dictionary +# 3. instance.ndfc_data is a list +# """ + +# def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: +# key = "NdfcSwitchDetails_get_return_code_200" +# return responses_ndfc_switch_details(key) + +# monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + +# module.refresh() +# assert isinstance(module.ndfc_data, dict) +# assert isinstance(module.ndfc_result, dict) +# assert isinstance(module.ndfc_response, dict) + + +# def test_refresh_ndfc_data(monkeypatch, module) -> None: +# """ +# Function description: + +# See test_refresh + +# Expected results: + +# 1. instance.ndfc_data is a dictionary +# 2. When instance.ip_address is set, getter properties will return values specific to ip_address +# """ + +# def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: +# key = "NdfcSwitchDetails_get_return_code_200" +# return responses_ndfc_switch_details(key) + +# monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + +# module.refresh() +# assert isinstance(module.ndfc_data, dict) +# module.ip_address = "172.22.150.110" +# assert module.hostname == "cvd-1111-bgw" +# module.ip_address = "172.22.150.111" +# # We use the above IP address to test the remaining properties +# assert module.fabric_name == "easy" +# assert module.hostname == "cvd-1112-bgw" +# assert module.logical_name == "cvd-1112-bgw" +# assert module.model == "N9K-C9504" +# # This is derived from "model" and is not in the NDFC response +# assert module.platform == "N9K" +# assert module.role == "border gateway" +# assert module.serial_number == "FOX2109PGD1" + + +# match = "Unable to retrieve switch information from NDFC. " + + +# @pytest.mark.parametrize( +# "key,expected", +# [ +# ("NdfcSwitchDetails_get_return_code_200", does_not_raise()), +# ( +# "NdfcSwitchDetails_get_return_code_404", +# pytest.raises(AnsibleFailJson, match=match), +# ), +# ( +# "NdfcSwitchDetails_get_return_code_500", +# pytest.raises(AnsibleFailJson, match=match), +# ), +# ], +# ) +# def test_ndfc_result(monkeypatch, module, key, expected) -> None: +# """ +# Function description: + +# NdfcSwitchDetails.ndfc_result returns the result of its superclass +# method NdfcAnsibleImageUpgradeCommon._handle_response() + +# Expectations: + +# 1. 200 RETURN_CODE, MESSAGE == "OK", +# NdfcSwitchDetails.ndfc_result == {'found': True, 'success': True} + +# 2. 404 RETURN_CODE, MESSAGE == "Not Found", +# NdfcSwitchDetails.ndfc_result == {'found': False, 'success': True} + +# 3. 500 RETURN_CODE, MESSAGE ~= "Internal Server Error", +# NdfcSwitchDetails.ndfc_result == {'found': False, 'success': False} + +# Expected results: + +# 1. NdfcSwitchDetails_result_200 == {'found': True, 'success': True} +# 2. NdfcSwitchDetails_result_404 == {'found': False, 'success': True} +# 3. NdfcSwitchDetails_result_500 == {'found': False, 'success': False} +# """ + +# def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: +# return responses_ndfc_switch_details(key) + +# monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + +# with expected: +# module.refresh() + + +# @pytest.mark.parametrize( +# "item, expected", +# [ +# ("fabricName", "easy"), +# ("hostName", "cvd-1111-bgw"), +# ("licenseViolation", False), +# ("location", None), +# ("logicalName", "cvd-1111-bgw"), +# ("managable", True), +# ("model", "N9K-C9504"), +# ("present", True), +# ("serialNumber", "FOX2109PGCT"), +# ("switchRole", "border gateway"), +# ], +# ) +# def test_get_with_ip_address_set(monkeypatch, module, item, expected) -> None: +# """ +# Function description: + +# NdfcSwitchDetails._get is called by all getter properties. +# It raises AnsibleFailJson if the user has not set ip_address. +# It returns the value of the requested property if the user has set ip_address. +# The property value is passed to both make_boolean() and make_none(), which +# either: +# - converts it to a boolean +# - converts it to NoneType +# - returns the value unchanged + +# Expectations: + +# 1. NdfcVersion._get returns above values +# given corresponding responses + +# Expected results: + +# 1. NdfcVersion_mode_LAN == "LAN" +# 2. NdfcVersion_mode_none == None +# """ + +# def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: +# key = "NdfcSwitchDetails_get_return_code_200" +# return responses_ndfc_switch_details(key) + +# monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + +# module.refresh() +# module.ip_address = "172.22.150.110" +# assert module._get(item) == expected From 4863377562f202208844a84f4beadcb948969379 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 30 Oct 2023 16:44:33 -1000 Subject: [PATCH 014/300] parametrize test_ip_address --- ...st_dcnm_image_upgrade_NdfcSwitchDetails.py | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py index c15570e67..77e689c52 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py @@ -61,22 +61,17 @@ def test_init_properties(module) -> None: assert module.properties.get("ndfc_result") == None -def test_ip_address_not_set(module) -> None: - """ - Function description: - - NdfcSwitchDetails.ip_address returns: - - IP Address, if the user has set ip_address - - None, if the user has not already set ip_address - - Expected results: - - 1. instance.ip_address will return None - """ - assert module.ip_address == None +# test_ip_address -def test_ip_address_is_set(module) -> None: +@pytest.mark.parametrize( + "ip_address_is_set, expected", + [ + (True, "1.2.3.4"), + (False, None), + ], +) +def test_ip_address(module, ip_address_is_set, expected) -> None: """ Function description: @@ -87,9 +82,11 @@ def test_ip_address_is_set(module) -> None: Expected results: 1. instance.ip_address will return the value set by the user + 2. instance.ip_address will return None """ - module.ip_address = "1.2.3.4" - assert module.ip_address == "1.2.3.4" + if ip_address_is_set: + module.ip_address = "1.2.3.4" + assert module.ip_address == expected def test_refresh(monkeypatch, module) -> None: @@ -105,7 +102,7 @@ def test_refresh(monkeypatch, module) -> None: 1. instance.ndfc_data is a dictionary 2. instance.ndfc_response is a dictionary - 3. instance.ndfc_data is a list + 3. instance.ndfc_data is a dictionary """ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: From af2ab5665a8cab54534013ea03508c18e0e52e14 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 31 Oct 2023 18:17:10 -1000 Subject: [PATCH 015/300] Move classes to separate files For some testcases, we need to patch dcnm_send several times which cannot be done if dcnm_send is imported once in dcnm_image_upgrade.py. To fix this (hopefully), have broken out all classes into separate files, which file importing dcnm_send, so that it can be patched to return appropriate imformation related to each class. There are still more test cases to fix. Will have these completed in the next commit. --- plugins/module_utils/common/__init__.py | 0 .../module_utils/common/controller_version.py | 262 + plugins/module_utils/common/ndfc_common.py | 134 + plugins/module_utils/image_mgmt/__init__.py | 0 plugins/module_utils/image_mgmt/endpoints.py | 147 + .../module_utils/image_mgmt/image_policies.py | 244 + .../image_mgmt/image_policy_action.py | 277 + .../module_utils/image_mgmt/image_stage.py | 381 ++ .../module_utils/image_mgmt/image_upgrade.py | 803 +++ .../module_utils/image_mgmt/image_validate.py | 373 ++ .../image_mgmt/install_options.py | 461 ++ .../module_utils/image_mgmt/switch_details.py | 184 + .../image_mgmt/switch_issu_details.py | 828 +++ plugins/modules/dcnm_image_upgrade.py | 4039 +------------ plugins/modules/dcnm_image_upgrade_orig.py | 5306 +++++++++++++++++ ...e_upgrade_NdfcAnsibleImageUpgradeCommon.py | 15 +- .../test_dcnm_image_upgrade_NdfcEndpoints.py | 6 +- ...m_image_upgrade_NdfcImageInstallOptions.py | 12 +- ...st_dcnm_image_upgrade_NdfcImagePolicies.py | 12 +- ...cnm_image_upgrade_NdfcImagePolicyAction.py | 18 +- .../test_dcnm_image_upgrade_NdfcImageStage.py | 80 +- 21 files changed, 9505 insertions(+), 4077 deletions(-) create mode 100644 plugins/module_utils/common/__init__.py create mode 100644 plugins/module_utils/common/controller_version.py create mode 100644 plugins/module_utils/common/ndfc_common.py create mode 100644 plugins/module_utils/image_mgmt/__init__.py create mode 100644 plugins/module_utils/image_mgmt/endpoints.py create mode 100644 plugins/module_utils/image_mgmt/image_policies.py create mode 100644 plugins/module_utils/image_mgmt/image_policy_action.py create mode 100644 plugins/module_utils/image_mgmt/image_stage.py create mode 100644 plugins/module_utils/image_mgmt/image_upgrade.py create mode 100644 plugins/module_utils/image_mgmt/image_validate.py create mode 100644 plugins/module_utils/image_mgmt/install_options.py create mode 100644 plugins/module_utils/image_mgmt/switch_details.py create mode 100644 plugins/module_utils/image_mgmt/switch_issu_details.py create mode 100644 plugins/modules/dcnm_image_upgrade_orig.py diff --git a/plugins/module_utils/common/__init__.py b/plugins/module_utils/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py new file mode 100644 index 000000000..548ceb4b9 --- /dev/null +++ b/plugins/module_utils/common/controller_version.py @@ -0,0 +1,262 @@ +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import (dcnm_send) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints + +class NdfcVersion(NdfcCommon): + """ + Return image version information from NDFC + + NOTES: + 1. considered using dcnm_version_supported() but it does not return + minor release info, which is needed due to key changes between + 12.1.2e and 12.1.3b. For example, see NdfcImageStage().commit() + + Endpoint: + /appcenter/cisco/ndfc/api/v1/fm/about/version + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcVersion(module) + instance.refresh() + if instance.version == "12.1.2e": + do 12.1.2e stuff + else: + do other stuff + + Response: + { + "version": "12.1.2e", + "mode": "LAN", + "isMediaController": false, + "dev": false, + "isHaEnabled": false, + "install": "EASYFABRIC", + "uuid": "f49e6088-ad4f-4406-bef6-2419de914ff1", + "is_upgrade_inprogress": false + } + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + + def refresh(self): + """ + Refresh self.ndfc_data with current version info from NDFC + """ + path = self.endpoints.ndfc_version.get("path") + verb = self.endpoints.ndfc_version.get("verb") + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_response: {self.ndfc_response}" + self.log_msg(msg) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_result: {self.ndfc_result}" + self.log_msg(msg) + + if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: + msg = f"{self.class_name}.refresh() failed: {self.ndfc_result}" + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + if self.ndfc_data is None: + msg = f"{self.class_name}.refresh() failed: NDFC response " + msg += "does not contain DATA key. NDFC response: " + msg += f"{self.ndfc_response}" + self.module.fail_json(msg) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_data: {self.ndfc_data}" + self.log_msg(msg) + + def _get(self, item): + return self.make_boolean(self.make_none(self.ndfc_data.get(item))) + + @property + def dev(self): + """ + Return True if NDFC is a development release. + Return False if NDFC is not a development release. + Return None otherwise + + Possible values: + True + False + None + """ + return self._get("dev") + + @property + def install(self): + """ + Return the value of install, if it exists. + Return None otherwise + + Possible values: + EASYFABRIC + (probably other values) + None + """ + return self._get("install") + + @property + def is_ha_enabled(self): + """ + Return True if NDFC is a media controller. + Return False if NDFC is not a media controller. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("isHaEnabled")) + + @property + def is_media_controller(self): + """ + Return True if NDFC is a media controller. + Return False if NDFC is not a media controller. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("isMediaController")) + + @property + def is_upgrade_inprogress(self): + """ + Return True if an NDFC upgrade is in progress. + Return False if an NDFC upgrade is not in progress. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("is_upgrade_inprogress")) + + @property + def ndfc_data(self): + """ + Return the data retrieved from the request + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the GET result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the GET response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def mode(self): + """ + Return the NDFC mode, if it exists. + Return None otherwise + + Possible values: + LAN + None + """ + return self._get("mode") + + @property + def uuid(self): + """ + Return the value of uuid, if it exists. + Return None otherwise + + Possible values: + uuid e.g. "f49e6088-ad4f-4406-bef6-2419de914df1" + None + """ + return self._get("uuid") + + @property + def version(self): + """ + Return the NDFC version, if it exists. + Return None otherwise + + Possible values: + version, e.g. "12.1.2e" + None + """ + return self._get("version") + + @property + def version_major(self): + """ + Return the NDFC major version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 12 + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[0] + + @property + def version_minor(self): + """ + Return the NDFC minor version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 1 + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[1] + + @property + def version_patch(self): + """ + Return the NDFC minor version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 2e + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[2] diff --git a/plugins/module_utils/common/ndfc_common.py b/plugins/module_utils/common/ndfc_common.py new file mode 100644 index 000000000..6d08e0e86 --- /dev/null +++ b/plugins/module_utils/common/ndfc_common.py @@ -0,0 +1,134 @@ +class NdfcCommon: + """ + Base class for the other NDFC classes + + Usage (where module is an instance of AnsibleModule): + + class MyNdfcClass(NdfcCommon): + def __init__(self, module): + super().__init__(module) + ... + """ + + def __init__(self, module): + self.module = module + self.params = module.params + self.debug = True + self.fd = None + self.logfile = "/tmp/ndfc.log" + self.module = module + + def _handle_response(self, response, verb): + if verb == "GET": + return self._handle_get_response(response) + if verb in {"POST", "PUT", "DELETE"}: + return self._handle_post_put_delete_response(response) + return self._handle_unknown_request_verbs(response, verb) + + def _handle_unknown_request_verbs(self, response, verb): + msg = f"Unknown request verb ({verb}) in _handle_response() for response {response}." + self.module.fail_json(msg) + + def _handle_get_response(self, response): + """ + Caller: + - self._handle_response() + Handle NDFC responses to GET requests + Returns: dict() with the following keys: + - found: + - False, if request error was "Not found" and RETURN_CODE == 404 + - True otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + success_return_codes = {200, 404} + if ( + response.get("RETURN_CODE") == 404 + and response.get("MESSAGE") == "Not Found" + ): + result["found"] = False + result["success"] = True + return result + if ( + response.get("RETURN_CODE") not in success_return_codes + or response.get("MESSAGE") != "OK" + ): + result["found"] = False + result["success"] = False + return result + result["found"] = True + result["success"] = True + return result + + def _handle_post_put_delete_response(self, response): + """ + Caller: + - self.self._handle_response() + + Handle POST, PUT responses from NDFC. + + Returns: dict() with the following keys: + - changed: + - True if changes were made to NDFC + - False otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + if response.get("ERROR") is not None: + result["success"] = False + result["changed"] = False + return result + if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: + result["success"] = False + result["changed"] = False + return result + result["success"] = True + result["changed"] = True + return result + + def log_msg(self, msg): + """ + used for debugging. disable this when committing to main + by setting __init__().debug to False + """ + if self.debug is False: + return + if self.fd is None: + try: + self.fd = open(f"{self.logfile}", "a+", encoding="UTF-8") + except IOError as err: + msg = f"error opening logfile {self.logfile}. " + msg += f"detail: {err}" + self.module.fail_json(msg) + + self.fd.write(msg) + self.fd.write("\n") + self.fd.flush() + + def make_boolean(self, value): + """ + Return value converted to boolean, if possible. + Return value, if value cannot be converted. + """ + if isinstance(value, bool): + return value + if isinstance(value, str): + if value.lower() in ["true", "yes"]: + return True + if value.lower() in ["false", "no"]: + return False + return value + + def make_none(self, value): + """ + Return None if value is an empty string, or a string + representation of a None type + Return value otherwise + """ + if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: + return None + return value diff --git a/plugins/module_utils/image_mgmt/__init__.py b/plugins/module_utils/image_mgmt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/image_mgmt/endpoints.py b/plugins/module_utils/image_mgmt/endpoints.py new file mode 100644 index 000000000..b1c66fea3 --- /dev/null +++ b/plugins/module_utils/image_mgmt/endpoints.py @@ -0,0 +1,147 @@ +class NdfcEndpoints: + """ + Endpoints for NDFC image management API calls + """ + def __init__(self): + self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" + + self.endpoint_feature_manager = f"{self.endpoint_api_v1}/fm" + self.endpoint_lan_fabric = f"{self.endpoint_api_v1}/lan-fabric" + + self.endpoint_image_management = f"{self.endpoint_api_v1}" + self.endpoint_image_management += "/imagemanagement" + + self.endpoint_image_upgrade = f"{self.endpoint_image_management}" + self.endpoint_image_upgrade += "/rest/imageupgrade" + + self.endpoint_package_mgnt = f"{self.endpoint_image_management}" + self.endpoint_package_mgnt += "/rest/packagemgnt" + + self.endpoint_policy_mgnt = f"{self.endpoint_image_management}" + self.endpoint_policy_mgnt += "/rest/policymgnt" + + self.endpoint_staging_management = f"{self.endpoint_image_management}" + self.endpoint_staging_management += "/rest/stagingmanagement" + + @property + def bootflash_info(self): + path = f"{self.endpoint_image_management}/rest/imagemgnt/bootFlash" + path += f"/bootflash-info" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def install_options(self): + path = f"{self.endpoint_image_upgrade}/install-options" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_stage(self): + path = f"{self.endpoint_staging_management}/stage-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_upgrade(self): + path = f"{self.endpoint_image_upgrade}/upgrade-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_validate(self): + path = f"{self.endpoint_staging_management}/validate-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def issu_info(self): + path = f"{self.endpoint_package_mgnt}/issu" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def ndfc_version(self): + path = f"{self.endpoint_feature_manager}/about/version" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policies_attached_info(self): + path = f"{self.endpoint_policy_mgnt}/all-attached-policies" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policies_info(self): + path = f"{self.endpoint_policy_mgnt}/policies" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policy_attach(self): + path = f"{self.endpoint_policy_mgnt}/attach-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def policy_create(self): + path = f"{self.endpoint_policy_mgnt}/platform-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def policy_detach(self): + path = f"{self.endpoint_policy_mgnt}/detach-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "DELETE" + return endpoint + + @property + def policy_info(self): + # Replace __POLICY_NAME__ with the policy_name to query + # e.g. path.replace("__POLICY_NAME__", "NR1F") + path = f"{self.endpoint_policy_mgnt}/image-policy/__POLICY_NAME__" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def stage_info(self): + path = f"{self.endpoint_staging_management}/stage-info" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def switches_info(self): + path = f"{self.endpoint_lan_fabric}/rest/inventory/allswitches" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py new file mode 100644 index 000000000..7e8354560 --- /dev/null +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -0,0 +1,244 @@ +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints + +class NdfcImagePolicies(NdfcCommon): + """ + Retrieve image policy details from NDFC and provide property accessors + for the policy attributes. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImagePolicies(module).refresh() + instance.policy_name = "NR3F" + if instance.name is None: + print("policy NR3F does not exist on NDFC") + exit(1) + policy_name = instance.name + platform = instance.platform + epd_image_name = instance.epld_image_name + etc... + + Policies are retrieved on instantiation of this class. + Policies can be refreshed by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + self.log_msg(f"{self.class_name}.__init__ entered") + self._init_properties() + # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it + #self.refresh() + + def _init_properties(self): + self.properties = {} + self.properties["policy_name"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + + def refresh(self): + """ + Refresh self.image_policies with current image policies from NDFC + """ + path = self.endpoints.policies_info.get("path") + verb = self.endpoints.policies_info.get("verb") + + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"result: {self.ndfc_result}" + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retriving image policy " + msg += "information from NDFC." + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA").get("lastOperDataObject") + if data is None: + msg = f"{self.class_name}.refresh: " + msg += "Bad response when retrieving image policy " + msg += "information from NDFC." + self.module.fail_json(msg) + if len(data) == 0: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no defined image policies." + self.module.fail_json(msg) + self.properties["ndfc_data"] = {} + for policy in data: + policy_name = policy.get("policyName") + if policy_name is None: + msg = f"{self.class_name}.refresh: " + msg += "Cannot parse NDFC policy information" + self.module.fail_json(msg) + self.properties["ndfc_data"][policy_name] = policy + + def _get(self, item): + if self.policy_name is None: + msg = f"{self.class_name}._get: " + msg += f"instance.policy_name must be set before " + msg += f"accessing property {item}." + self.module.fail_json(msg) + if self.properties['ndfc_data'].get(self.policy_name) is None: + msg = f"{self.class_name}._get: " + msg += f"policy_name {self.policy_name} is not defined in NDFC" + self.module.fail_json(msg) + return_item = self.make_boolean(self.properties["ndfc_data"][self.policy_name].get(item)) + return_item = self.make_none(return_item) + return return_item + + @property + def description(self): + """ + Return the policyDescr of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyDescr") + + @property + def epld_image_name(self): + """ + Return the epldImgName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("epldImgName") + + @property + def name(self): + """ + Return the name of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyName") + + @property + def ndfc_data(self): + """ + Return the parsed data from the NDFC response as a dictionary, + keyed on policy_name. + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the NDFC response. + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result from the NDFC response. + """ + return self.properties["ndfc_result"] + + @property + def policy_name(self): + """ + Set the name of the policy to query. + + This must be set prior to accessing any other properties + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def policy_type(self): + """ + Return the policyType of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyType") + + @property + def nxos_version(self): + """ + Return the nxosVersion of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("nxosVersion") + + @property + def package_name(self): + """ + Return the packageName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("packageName") + + @property + def platform(self): + """ + Return the platform of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("platform") + + @property + def platform_policies(self): + """ + Return the platformPolicies of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("platformPolicies") + + @property + def ref_count(self): + """ + Return the reference count of the policy matching self.policy_name, + if it exists. The reference count is the number of switches using + this policy. + Return None otherwise + """ + return self._get("ref_count") + + @property + def rpm_images(self): + """ + Return the rpmimages of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("rpmimages") + + @property + def image_name(self): + """ + Return the imageName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("imageName") + + @property + def agnostic(self): + """ + Return the value of agnostic for the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("agnostic") + diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py new file mode 100644 index 000000000..b0b2de731 --- /dev/null +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -0,0 +1,277 @@ +import json +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import NdfcImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber + + +class NdfcImagePolicyAction(NdfcCommon): + """ + Perform image policy actions on NDFC on one or more switches. + + Support for the following actions: + - attach + - detach + - query + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImagePolicyAction(module) + instance.policy_name = "NR3F" + instance.action = "attach" # or detach, or query + instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] + instance.commit() + # for query only + query_result = instance.query_result + + Endpoints: + For action == attach: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy + For action == detach: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy + For action == query: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + self._init_properties() + self.image_policies = NdfcImagePolicies(self.module) + self.switch_issu_details = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.valid_actions = {"attach", "detach", "query"} + + def _init_properties(self): + self.properties = {} + self.properties["action"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["policy_name"] = None + self.properties["query_result"] = None + self.properties["serial_numbers"] = None + + def build_attach_payload(self): + """ + build the payload to send in the POST request + to attach policies to devices + + caller _attach_policy() + """ + self.payloads = [] + # TODO:2 this results in more calls than necessary to NDFC. Need a better way to handle this. + self.switch_issu_details.refresh() + for serial_number in self.serial_numbers: + self.switch_issu_details.serial_number = serial_number + payload = {} + payload["policyName"] = self.policy_name + payload["hostName"] = self.switch_issu_details.device_name + payload["ipAddr"] = self.switch_issu_details.ip_address + payload["platform"] = self.switch_issu_details.platform + payload["serialNumber"] = self.switch_issu_details.serial_number + for item in payload: + if payload[item] is None: + msg = f"Unable to determine {item} for switch " + msg += f"{self.switch_issu_details.ip_address}, " + msg += f"{self.switch_issu_details.serial_number}, " + msg += f"{self.switch_issu_details.device_name}. " + msg += "Please verify that the switch is managed by NDFC." + self.module.fail_json(msg) + self.payloads.append(payload) + + def validate_request(self): + """ + validations prior to commit() should be added here. + """ + self.log_msg(f"REMOVE: {self.class_name}.validate_request: Entered") + if self.action is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.action must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + if self.policy_name is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.policy_name must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + self.log_msg(f"REMOVE: {self.class_name}.validate_request: action {self.action}") + + if self.action == "query": + return + + self.log_msg(f"REMOVE: {self.class_name}.validate_request: serial_numbers {self.serial_numbers}") + + if self.serial_numbers is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.serial_numbers must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + + # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it + self.image_policies.refresh() + self.switch_issu_details.refresh() + # Fail if the image policy does not support the switch platform + self.image_policies.policy_name = self.policy_name + for serial_number in self.serial_numbers: + self.switch_issu_details.serial_number = serial_number + if self.switch_issu_details.platform not in self.image_policies.platform: + msg = f"policy {self.policy_name} does not support platform " + msg += f"{self.switch_issu_details.platform}. {self.policy_name} " + msg += "supports the following platform(s): " + msg += f"{self.image_policies.platform}" + self.module.fail_json(msg) + + def commit(self): + self.validate_request() + if self.action == "attach": + self._attach_policy() + elif self.action == "detach": + self._detach_policy() + elif self.action == "query": + self._query_policy() + else: + msg = f"{self.class_name}.commit: " + msg += f"Unknown action {self.action}." + self.module.fail_json(msg) + + def _attach_policy(self): + """ + Attach policy_name to the switch(es) associated with serial_numbers + + NOTES: + 1. This method creates a list of responses and results which + are accessible via properties ndfc_response and ndfc_result, + respectively. + """ + self.build_attach_payload() + path = self.endpoints.policy_attach.get("path") + verb = self.endpoints.policy_attach.get("verb") + responses = [] + results = [] + for payload in self.payloads: + response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) + result = self._handle_response(response, verb) + if not result["success"]: + msg = f"{self.class_name}._attach_policy: " + msg += f"Bad result when attaching policy {self.policy_name} " + msg += f"to switch {payload['ipAddr']}." + self.module.fail_json(msg) + responses.append(response) + results.append(result) + self.properties["ndfc_response"] = responses + self.properties["ndfc_result"] = results + + def _detach_policy(self): + """ + Detach policy_name from the switch(es) associated with serial_numbers + verb: DELETE + endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy + query_params: ?serialNumber=FDO211218GC,FDO21120U5D + """ + path = self.endpoints.policy_detach.get("path") + verb = self.endpoints.policy_detach.get("verb") + query_params = ",".join(self.serial_numbers) + path += f"?serialNumber={query_params}" + response = dcnm_send(self.module, verb, path) + result = self._handle_response(response, verb) + if not result["success"]: + self._failure(response) + self.properties["ndfc_response"] = response + self.properties["ndfc_result"] = result + + def _query_policy(self): + """ + Query the image policy + verb: GET + endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ + """ + path = self.endpoints.policy_info.get("path") + verb = self.endpoints.policy_info.get("verb") + path = path.replace("__POLICY_NAME__", self.policy_name) + response = dcnm_send(self.module, verb, path) + result = self._handle_response(response, verb) + if not result["success"]: + self._failure(response) + self.properties["query_result"] = response.get("DATA") + self.properties["ndfc_response"] = response + self.properties["ndfc_result"] = result + + @property + def query_result(self): + """ + Return the value of properties["query_result"]. + """ + return self.properties.get("query_result") + + @property + def action(self): + """ + Set the action to take. Either "attach" or "detach". + + Must be set prior to calling instance.commit() + """ + return self.properties.get("action") + + @action.setter + def action(self, value): + if value not in self.valid_actions: + msg = f"{self.class_name}: instance.action must be " + msg += f"one of {','.join(sorted(self.valid_actions))}" + self.module.fail_json(msg) + self.properties["action"] = value + + @property + def ndfc_response(self): + """ + Return the raw response from NDFC after calling commit(). + + In the case of attach, this is a list of responses. + """ + return self.properties.get("ndfc_response") + + @property + def ndfc_result(self): + """ + Return the raw result from NDFC after calling commit(). + + In the case of attach, this is a list of results. + """ + return self.properties.get("ndfc_result") + + @property + def policy_name(self): + """ + Set the name of the policy to attach, detach, query. + + Must be set prior to calling instance.commit() + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to/from which + policy_name will be attached or detached. + + Must be set prior to calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.class_name}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py new file mode 100644 index 000000000..40a99bbe7 --- /dev/null +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -0,0 +1,381 @@ +import copy +import json +from time import sleep +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import NdfcVersion + +class NdfcImageStage(NdfcCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image + + Verb: POST + + Usage (where module is an instance of AnsibleModule): + + stage = NdfcImageStage(module) + stage.serial_numbers = ["FDO211218HH", "FDO211218GC"] + stage.commit() + data = stage.data + + Request body (12.1.2e) (yes, serialNum is misspelled): + { + "sereialNum": [ + "FDO211218HH", + "FDO211218GC" + ] + } + Request body (12.1.3b): + { + "serialNumbers": [ + "FDO211218HH", + "FDO211218GC" + ] + } + + Response: + Unfortunately, the response does not contain consistent data. + Would be better if all responses contained serial numbers as keys so that + we could verify against a set() of serial numbers. Sigh. It is what it is. + { + 'RETURN_CODE': 200, + 'METHOD': 'POST', + 'REQUEST_PATH': 'https: //172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image', + 'MESSAGE': 'OK', + 'DATA': [ + { + 'key': 'success', + 'value': '' + }, + { + 'key': 'success', + 'value': '' + } + ] + } + + Response when there are no files to stage: + [ + { + "key": "FDO211218GC", + "value": "No files to stage" + }, + { + "key": "FDO211218HH", + "value": "No files to stage" + } + ] + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + self._init_properties() + self.serial_numbers_done = set() + self.ndfc_version = None + self.path = None + self.verb = None + self.payload = None + self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + + def _init_properties(self): + self.properties = {} + self.properties["serial_numbers"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + + def _populate_ndfc_version(self): + """ + Populate self.ndfc_version with the NDFC version. + + Notes: + 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + imports resulting in RecursionError + """ + instance = NdfcVersion(self.module) + instance.refresh() + self.ndfc_version = instance.version + + def prune_serial_numbers(self): + """ + If the image is already staged on a switch, remove that switch's + serial number from the list of serial numbers to stage. + """ + serial_numbers = copy.copy(self.serial_numbers) + for serial_number in serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.image_staged == "Success": + msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " + msg += "image already staged for " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}." + self.log_msg(msg) + self.serial_numbers.remove(serial_number) + + def validate_serial_numbers(self): + """ + Fail if the image_staged state for any serial_number + is Failed. + """ + for serial_number in self.serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.image_staged == "Failed": + msg = "Image staging is failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += f"Check the switch connectivity to NDFC " + msg += "and try again." + self.module.fail_json(msg) + else: + self.log_msg(f"Image staging is not failing for the following switch: {self.issu_detail.serial_number}") + + def commit(self): + """ + Commit the image staging request to NDFC and wait + for the images to be staged. + """ + if self.serial_numbers is None: + msg = f"{self.class_name}.commit() call instance.serial_numbers " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.serial_numbers) == 0: + msg = f"REMOVE: {self.class_name}.commit() no serial numbers to stage." + self.log_msg(msg) + return + self.prune_serial_numbers() + self.validate_serial_numbers() + self._wait_for_current_actions_to_complete() + + self.path = self.endpoints.image_stage.get("path") + self.verb = self.endpoints.image_stage.get("verb") + + self.log_msg(f"REMOVE: {self.class_name}.commit() path: {self.path}") + self.log_msg(f"REMOVE: {self.class_name}.commit() verb: {self.verb}") + self.payload = {} + self._populate_ndfc_version() + if self.ndfc_version == "12.1.2e": + # Yes, NDFC 12.1.2e wants serialNum to be misspelled + self.payload["sereialNum"] = self.serial_numbers + else: + self.payload["serialNumbers"] = self.serial_numbers + self.properties["ndfc_response"] = dcnm_send( + self.module, self.verb, self.path, data=json.dumps(self.payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, self.verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_stage_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not stage an image if there are any actions in progress. + Wait for all actions to complete before staging image. + Actions include image staging, image upgrade, and image validation. + """ + self.serial_numbers_done = set() + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + timeout = self.check_timeout + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} actions in progress: " + msg += f"{self.issu_detail.actions_in_progress}, " + msg += f"{timeout} seconds remaining." + self.log_msg(msg) + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"Timed out waiting for actions to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + def _wait_for_image_stage_to_complete(self): + """ + # Wait for image stage to complete + """ + # We're promoting serial_numbers_done to a class-level attribute + # so that it can be used in unit test asserts. + self.serial_numbers_done = set() + timeout = self.check_timeout + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" + self.log_msg(msg) + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + staged_percent = self.issu_detail.image_staged_percent + staged_status = self.issu_detail.image_staged + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Seconds remaining {timeout}: " + msg += f"{device_name}, {serial_number}, {ip_address} " + msg += f"image staged percent: {staged_percent}" + self.log_msg(msg) + + if staged_status == "Failed": + msg = f"Seconds remaining {timeout}: stage image failed " + msg += f"for {device_name}, {serial_number}, {ip_address}. " + msg += f"image staged percent: {staged_percent}" + self.module.fail_json(msg) + + if staged_status == "Success": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Seconds remaining {timeout}: stage image complete for " + msg += f"for {device_name}, {serial_number}, {ip_address}. " + msg += f"image staged percent: {staged_percent}" + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + + # if staged_status == None: + # msg = f"REMOVE: {self.class_name}." + # msg += "_wait_for_image_stage_to_complete: " + # msg += f"Seconds remaining {timeout}: stage image " + # msg += "not started for " + # msg += f"{device_name}, {serial_number}, {ip_address}. " + # msg += f"image staged percent: {staged_percent}" + # self.log_msg(msg) + + # if staged_status == "In Progress": + # msg = f"REMOVE: {self.class_name}." + # msg += "_wait_for_image_stage_to_complete: " + # msg += f"Seconds remaining {timeout}: stage image " + # msg += f"{staged_status} for " + # msg += f"{device_name}, {serial_number}, {ip_address}. " + # msg += f"image staged percent: {staged_percent}" + # self.log_msg(msg) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Timed out waiting for image stage to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to stage. + + This must be set before calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.__class__.__name__}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + + @property + def ndfc_data(self): + """ + Return the result of the image staging request + for serial_numbers. + + instance.serial_numbers must be set first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def check_interval(self): + """ + Return the stage check interval in seconds + """ + return self.properties.get("check_interval") + + @check_interval.setter + def check_interval(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_interval must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_interval"] = value + + @property + def check_timeout(self): + """ + Return the stage check timeout in seconds + """ + return self.properties.get("check_timeout") + + @check_timeout.setter + def check_timeout(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_timeout must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py new file mode 100644 index 000000000..bc17686bb --- /dev/null +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -0,0 +1,803 @@ +import copy +import json +from time import sleep + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsByIpAddress + +class NdfcImageUpgrade(NdfcCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image + Verb: POST + + Usage (where module is an instance of AnsibleModule): + + upgrade = NdfcImageUpgrade(module) + upgrade.devices = devices + upgrade.commit() + data = upgrade.data + + Where devices is a list of dict. Example structure: + + [ + { + 'policy': 'KR3F', + 'ip_address': '172.22.150.102', + 'policy_changed': False + 'stage': False, + 'validate': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'nxos': { + 'mode': 'non_disruptive' + 'bios_force': False + }, + 'epld': { + 'module': 'ALL', + 'golden': False + }, + 'reboot': { + 'config_reload': False, + 'write_erase': False + }, + 'package': { + 'install': False, + 'uninstall': False + } + }, + }, + etc... + ] + + Request body: + Yes, the keys below are misspelled in the request body: + pacakgeInstall + pacakgeUnInstall + + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + } + ], + "issuUpgrade": true, + "issuUpgradeOptions1": { + "nonDisruptive": true, + "forceNonDisruptive": false, + "disruptive": false + }, + "issuUpgradeOptions2": { + "biosForce": false + }, + "epldUpgrade": false, + "epldOptions": { + "moduleNumber": "ALL", + "golden": false + }, + "reboot": false, + "rebootOptions": { + "configReload": "false", + "writeErase": "false" + }, + "pacakgeInstall": false, + "pacakgeUnInstall": false + } + Response bodies: + Responses are text, not JSON, and are returned immediately. + They do not contain useful information. We need to poll NDFC + to determine when the upgrade is complete. Basically, we ignore + these responses in favor of the poll responses. + - If an action is in progress, text is returned: + "Action in progress for some of selected device(s). Please try again after completing current action." + - If an action is not in progress, text is returned: + "3" + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + # Maximum number of modules/linecards in a switch + self.max_module_number = 9 + + self._init_defaults() + self._init_properties() + self.issu_detail = NdfcSwitchIssuDetailsByIpAddress(self.module) + + def _init_defaults(self): + self.defaults = {} + self.defaults["reboot"] = False + self.defaults["stage"] = True + self.defaults["validate"] = True + self.defaults["upgrade"] = {} + self.defaults["upgrade"]["nxos"] = True + self.defaults["upgrade"]["epld"] = False + self.defaults["options"] = {} + self.defaults["options"]["nxos"] = {} + self.defaults["options"]["nxos"]["mode"] = "disruptive" + self.defaults["options"]["nxos"]["bios_force"] = False + self.defaults["options"]["epld"] = {} + self.defaults["options"]["epld"]["module"] = "ALL" + self.defaults["options"]["epld"]["golden"] = False + self.defaults["options"]["reboot"] = {} + self.defaults["options"]["reboot"]["config_reload"] = False + self.defaults["options"]["reboot"]["write_erase"] = False + self.defaults["options"]["package"] = {} + self.defaults["options"]["package"]["install"] = False + self.defaults["options"]["package"]["uninstall"] = False + + def _init_properties(self): + # self.ip_addresses is used in: + # self._wait_for_current_actions_to_complete() + # self._wait_for_image_upgrade_to_complete() + self.ip_addresses = set() + # TODO:1 Review these properties since we are no longer + # calling this class per-switch given the payload structure + # is not amenable to that. + self.properties = {} + self.properties["bios_force"] = False + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + self.properties["config_reload"] = False + self.properties["devices"] = None + self.properties["disruptive"] = True + self.properties["epld_golden"] = False + self.properties["epld_module"] = "ALL" + self.properties["epld_upgrade"] = False + self.properties["force_non_disruptive"] = False + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["non_disruptive"] = False + self.properties["package_install"] = False + self.properties["package_uninstall"] = False + self.properties["reboot"] = False + self.properties["write_erase"] = False + + self.valid_epld_module = set() + self.valid_epld_module.add("ALL") + for module in range(1, self.max_module_number + 1): + self.valid_epld_module.add(str(module)) + + self.valid_nxos_mode = set() + self.valid_nxos_mode.add("disruptive") + self.valid_nxos_mode.add("non_disruptive") + self.valid_nxos_mode.add("force_non_disruptive") + + + # def prune_devices(self): + # """ + # If the image is already upgraded on a device, remove that device + # from self.devices. self.devices dict has already been validated, + # so no further error checking is needed here. + + # TODO:1 This prunes devices only based on the image upgrade state. + # TODO:1 It does not check other image states and EPLD states. + # """ + # # issu = NdfcSwitchIssuDetailsBySerialNumber(self.module) + # pruned_devices = set() + # instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + # instance.refresh() + # for device in self.devices: + # msg = f"REMOVE: {self.class_name}.prune_devices() device: {device}" + # self.log_msg(msg) + # instance.ip_address = device.get("ip_address") + # instance.refresh() + # if instance.upgrade == "Success": + # msg = f"REMOVE: {self.class_name}.prune_devices: " + # msg = "image already upgraded for " + # msg += f"{instance.device_name}, " + # msg += f"{instance.serial_number}, " + # msg += f"{instance.ip_address}" + # self.log_msg(msg) + # pruned_devices.add(instance.ip_address) + # self.devices = [ + # device + # for device in self.devices + # if device.get("ip_address") not in pruned_devices + # ] + + def validate_devices(self): + """ + Fail if the upgrade state for any device is Failed. + """ + for device in self.devices: + self.issu_detail.ip_address = device.get("ip_address") + self.issu_detail.refresh() + if self.issu_detail.upgrade == "Failed": + msg = f"{self.class_name}.validate_devices: Image upgrade is " + msg += "failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += "Please check the switch " + msg += "to determine the cause and try again." + self.module.fail_json(msg) + # used in self._wait_for_current_actions_to_complete() + self.ip_addresses.add(self.issu_detail.ip_address) + + def _merge_defaults_to_switch_config(self, config): + if config.get("stage") is None: + config["stage"] = self.defaults["stage"] + if config.get("reboot") is None: + config["reboot"] = self.defaults["reboot"] + if config.get("validate") is None: + config["validate"] = self.defaults["validate"] + if config.get("upgrade") is None: + config["upgrade"] = self.defaults["upgrade"] + if config.get("upgrade").get("nxos") is None: + config["upgrade"]["nxos"] = self.defaults["upgrade"]["nxos"] + if config.get("upgrade").get("epld") is None: + config["upgrade"]["epld"] = self.defaults["upgrade"]["epld"] + if config.get("options") is None: + config["options"] = self.defaults["options"] + if config["options"].get("nxos") is None: + config["options"]["nxos"] = self.defaults["options"]["nxos"] + if config["options"]["nxos"].get("mode") is None: + config["options"]["nxos"]["mode"] = self.defaults["options"]["nxos"]["mode"] + if config["options"]["nxos"].get("bios_force") is None: + config["options"]["nxos"]["bios_force"] = self.defaults["options"]["nxos"]["bios_force"] + if config["options"].get("epld") is None: + config["options"]["epld"] = self.defaults["options"]["epld"] + if config["options"]["epld"].get("module") is None: + config["options"]["epld"]["module"] = self.defaults["options"]["epld"]["module"] + if config["options"]["epld"].get("golden") is None: + config["options"]["epld"]["golden"] = self.defaults["options"]["epld"]["golden"] + if config["options"].get("reboot") is None: + config["options"]["reboot"] = self.defaults["options"]["reboot"] + if config["options"]["reboot"].get("config_reload") is None: + config["options"]["reboot"]["config_reload"] = self.defaults["options"]["reboot"]["config_reload"] + if config["options"]["reboot"].get("write_erase") is None: + config["options"]["reboot"]["write_erase"] = self.defaults["options"]["reboot"]["write_erase"] + if config["options"].get("package") is None: + config["options"]["package"] = self.defaults["options"]["package"] + if config["options"]["package"].get("install") is None: + config["options"]["package"]["install"] = self.defaults["options"]["package"]["install"] + if config["options"]["package"].get("uninstall") is None: + config["options"]["package"]["uninstall"] = self.defaults["options"]["package"]["uninstall"] + return config + + def build_payload(self, device): + """ + Build the request payload to upgrade the switches. + """ + msg = f"REMOVE: {self.class_name}.build_payload() PRE_DEFAULTS: " + msg += f"device: {device}" + self.log_msg(msg) + device = self._merge_defaults_to_switch_config(device) + msg = f"REMOVE: {self.class_name}.build_payload() POST_DEFAULTS: " + msg += f"device: {device}" + self.log_msg(msg) + + + # devices_to_upgrade must currently be a single device + devices_to_upgrade = [] + self.issu_detail.ip_address = device.get("ip_address") + self.issu_detail.refresh() + payload_device = {} + payload_device["serialNumber"] = self.issu_detail.serial_number + payload_device["policyName"] = device.get("policy") + devices_to_upgrade.append(payload_device) + + self.payload = {} + self.payload["devices"] = devices_to_upgrade + self.payload["issuUpgrade"] = device.get("upgrade").get("nxos") + + # nxos_mode: The choices for nxos_mode are mutually-exclusive. + # If one is set to True, the others must be False. + # nonDisruptive corresponds to NDFC Allow Non-Disruptive GUI option + self.payload["issuUpgradeOptions1"] = {} + self.payload["issuUpgradeOptions1"]["nonDisruptive"] = False + self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = False + self.payload["issuUpgradeOptions1"]["disruptive"] = False + + nxos_mode = device.get("options").get("nxos").get("mode") + if nxos_mode not in self.valid_nxos_mode: + msg = f"{self.class_name}.build_payload() options.nxos.mode must " + msg += f"be one of {self.valid_nxos_mode}. Got {nxos_mode}." + self.module.fail_json(msg) + if nxos_mode == "non_disruptive": + self.payload["issuUpgradeOptions1"]["nonDisruptive"] = True + if nxos_mode == "disruptive": + self.payload["issuUpgradeOptions1"]["disruptive"] = True + if nxos_mode == "force_non_disruptive": + self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True + + # biosForce corresponds to NDFC BIOS Force GUI option + bios_force = device.get("options").get("nxos").get("bios_force") + if not isinstance(bios_force, bool): + msg = f"{self.class_name}.build_payload() options.nxos.bios_force " + msg += f"must be a boolean. Got {bios_force}." + self.module.fail_json(msg) + self.payload["issuUpgradeOptions2"] = {} + self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force + + # EPLD + epld_module = device.get("options").get("epld").get("module") + epld_golden = device.get("options").get("epld").get("golden") + if epld_module not in self.valid_epld_module: + msg = f"{self.class_name}.build_payload() options.epld.module must " + msg += f"be one of {self.valid_epld_module}. Got {epld_module}." + self.module.fail_json(msg) + if not isinstance(epld_golden, bool): + msg = f"{self.class_name}.build_payload() options.epld.golden " + msg += f"must be a boolean. Got {epld_golden}." + self.module.fail_json(msg) + self.payload["epldUpgrade"] = device.get("upgrade").get("epld") + self.payload["epldOptions"] = {} + self.payload["epldOptions"]["moduleNumber"] = epld_module + self.payload["epldOptions"]["golden"] = epld_golden + + # Reboot + reboot = device.get("reboot") + if not isinstance(reboot, bool): + msg = f"{self.class_name}.build_payload() reboot must " + msg += f"be a boolean. Got {reboot}." + self.module.fail_json(msg) + self.payload["reboot"] = reboot + + # Reboot options + config_reload = device.get("options").get("reboot").get("config_reload") + write_erase = device.get("options").get("reboot").get("write_erase") + if not isinstance(config_reload, bool): + msg = f"{self.class_name}.build_payload() options.reboot.config_reload " + msg += f"must be a boolean. Got {config_reload}." + self.module.fail_json(msg) + if not isinstance(write_erase, bool): + msg = f"{self.class_name}.build_payload() options.reboot.write_erase " + msg += f"must be a boolean. Got {write_erase}." + self.module.fail_json(msg) + self.payload["rebootOptions"] = {} + self.payload["rebootOptions"]["configReload"] = config_reload + self.payload["rebootOptions"]["writeErase"] = write_erase + + # Packages + package_install = device.get("options").get("package").get("install") + package_uninstall = device.get("options").get("package").get("uninstall") + if not isinstance(package_install, bool): + msg = f"{self.class_name}.build_payload() options.package.install " + msg += f"must be a boolean. Got {package_install}." + self.module.fail_json(msg) + if not isinstance(package_uninstall, bool): + msg = f"{self.class_name}.build_payload() options.package.uninstall " + msg += f"must be a boolean. Got {package_uninstall}." + self.module.fail_json(msg) + self.payload["pacakgeInstall"] = package_install + self.payload["pacakgeUnInstall"] = package_uninstall + + def commit(self): + """ + Commit the image upgrade request to NDFC and wait + for the images to be upgraded. + """ + if self.devices is None: + msg = f"{self.class_name}.commit() call instance.devices " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.devices) == 0: + msg = f"REMOVE: {self.class_name}.commit() no devices to upgrade." + self.log_msg(msg) + return + #self.prune_devices() + self.validate_devices() + self._wait_for_current_actions_to_complete() + path = self.endpoints.image_upgrade.get("path") + verb = self.endpoints.image_upgrade.get("verb") + for device in self.devices: + self.build_payload(device) + self.log_msg(f"REMOVE: {self.class_name}.commit() upgrade payload: {self.payload}") + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_upgrade_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not upgrade an image if there are any actions in progress. + Wait for all actions to complete before upgrading image. + Actions include image staging, image upgrade, and image validation. + """ + ipv4_todo = copy.copy(self.ip_addresses) + timeout = self.check_timeout + while len(ipv4_todo) > 0 and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for ipv4 in self.ip_addresses: + if ipv4 not in ipv4_todo: + continue + self.issu_detail.ip_address = ipv4 + self.issu_detail.refresh() + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{ipv4} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + ipv4_todo.remove(ipv4) + continue + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{ipv4} actions in progress. " + msg += f"Waiting. {timeout} seconds remaining." + self.log_msg(msg) + + def _wait_for_image_upgrade_to_complete(self): + """ + Wait for image upgrade to complete + """ + ipv4_done = set() + timeout = self.check_timeout + ipv4_todo = set(copy.copy(self.ip_addresses)) + while ipv4_done != ipv4_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"seconds remaining {timeout}, " + msg += f"ipv4_todo: {sorted(list(ipv4_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"seconds remaining {timeout}, " + msg += f"ipv4_done: {sorted(list(ipv4_done))}" + self.log_msg(msg) + for ipv4 in self.ip_addresses: + if ipv4 in ipv4_done: + continue + self.issu_detail.ip_address = ipv4 + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + upgrade_percent = self.issu_detail.upgrade_percent + upgrade_status = self.issu_detail.upgrade + serial_number = self.issu_detail.serial_number + + if upgrade_status == "Failed": + msg = f"{self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"Seconds remaining {timeout}: upgrade image " + msg += f"{upgrade_status} for " + msg += f"{device_name}, {serial_number}, {ip_address}" + self.module.fail_json(msg) + + if upgrade_status == "Success": + ipv4_done.add(ipv4) + status = "succeeded" + if upgrade_status == None: + status = "not started" + if upgrade_status == "In-Progress": + status = "in progress" + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"Seconds remaining {timeout}, " + msg += f"Percent complete {upgrade_percent}, " + msg += f"Status {status}, " + msg += f"{device_name}, {serial_number}, {ip_address}" + self.log_msg(msg) + + if ipv4_done != ipv4_todo: + msg = f"{self.class_name}._wait_for_image_upgrade_to_complete(): " + msg += "The following device(s) did not complete upgrade: " + msg += f"{ipv4_todo.difference(ipv4_done)}. " + msg += "Try increasing issu timeout in the playbook, or check " + msg += "the device(s) to determine the cause " + msg += "(e.g. show install all status)." + self.module.fail_json(msg) + + # setter properties + @property + def bios_force(self): + """ + Set the bios_force flag to True or False. + + Default: False + """ + return self.properties.get("bios_force") + + @bios_force.setter + def bios_force(self, value): + name = "bios_force" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def config_reload(self): + """ + Set the config_reload flag to True or False. + + Default: False + """ + return self.properties.get("config_reload") + + @config_reload.setter + def config_reload(self, value): + name = "config_reload" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def devices(self): + """ + Set the devices to upgrade. + + list() of dict() with the following structure: + { + "serial_number": "FDO211218HH", + "policy_name": "NR1F" + } + + Must be set before calling instance.commit() + """ + return self.properties.get("devices") + + @devices.setter + def devices(self, value): + name = "devices" + if not isinstance(value, list): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a python list of dict." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def disruptive(self): + """ + Set the disruptive flag to True or False. + + Default: False + """ + return self.properties.get("disruptive") + + @disruptive.setter + def disruptive(self, value): + name = "disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_golden(self): + """ + Set the epld_golden flag to True or False. + + Default: False + """ + return self.properties.get("epld_golden") + + @epld_golden.setter + def epld_golden(self, value): + name = "epld_golden" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_upgrade(self): + """ + Set the epld_upgrade flag to True or False. + + Default: False + """ + return self.properties.get("epld_upgrade") + + @epld_upgrade.setter + def epld_upgrade(self, value): + name = "epld_upgrade" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_module(self): + """ + Set the epld_module to upgrade. + + Ignored if epld_upgrade is set to False + Valid values: integer or "ALL" + Default: "ALL" + """ + return self.properties.get("epld_module") + + @epld_module.setter + def epld_module(self, value): + name = "epld_module" + try: + value = value.upper() + except AttributeError: + pass + if not isinstance(value, int) and value != "ALL": + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be an integer or 'ALL'" + self.module.fail_json(msg) + self.properties[name] = value + + @property + def force_non_disruptive(self): + """ + Set the force_non_disruptive flag to True or False. + + Default: False + """ + return self.properties.get("force_non_disruptive") + + @force_non_disruptive.setter + def force_non_disruptive(self, value): + name = "force_non_disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def non_disruptive(self): + """ + Set the non_disruptive flag to True or False. + + Default: True + """ + return self.properties.get("non_disruptive") + + @non_disruptive.setter + def non_disruptive(self, value): + name = "non_disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def package_install(self): + """ + Set the package_install flag to True or False. + + Default: False + """ + return self.properties.get("package_install") + + @package_install.setter + def package_install(self, value): + name = "package_install" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def package_uninstall(self): + """ + Set the package_uninstall flag to True or False. + + Default: False + """ + return self.properties.get("package_uninstall") + + @package_uninstall.setter + def package_uninstall(self, value): + name = "package_uninstall" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def reboot(self): + """ + Set the reboot flag to True or False. + + Default: False + """ + return self.properties.get("reboot") + + @reboot.setter + def reboot(self, value): + name = "reboot" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def write_erase(self): + """ + Set the write_erase flag to True or False. + + Default: False + """ + return self.properties.get("write_erase") + + @write_erase.setter + def write_erase(self, value): + name = "write_erase" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + + # getter properties + @property + def check_interval(self): + """ + Return the image upgrade check interval in seconds + """ + return self.properties.get("check_interval") + + @property + def check_timeout(self): + """ + Return the image upgrade check timeout in seconds + """ + return self.properties.get("check_timeout") + + @property + def ndfc_data(self): + """ + Return the data retrieved from NDFC for the image upgrade request. + + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_response") + + @property + def serial_numbers(self): + """ + Return a list of serial numbers from self.devices + """ + return [device.get("serial_number") for device in self.devices] diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py new file mode 100644 index 000000000..9af13b65e --- /dev/null +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -0,0 +1,373 @@ +import copy +from time import sleep +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber + +class NdfcImageValidate(NdfcCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image + + Verb: POST + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImageValidate(module) + instance.serial_numbers = ["FDO211218HH", "FDO211218GC"] + # non_disruptive is optional + instance.non_disruptive = True + instance.commit() + data = instance.ndfc_data + + Request body: + { + "serialNum": ["FDO21120U5D"], + "nonDisruptive":"true" + } + + Response body when nonDisruptive is True: + [StageResponse [key=success, value=]] + + Response body when nonDisruptive is False: + [StageResponse [key=success, value=]] + + The response is not JSON, nor is it very useful. + Instead, we poll for validation status using + NdfcSwitchIssuDetailsBySerialNumber. + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + self._init_properties() + self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + + def _init_properties(self): + self.properties = {} + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["non_disruptive"] = False + self.properties["serial_numbers"] = None + + # def _populate_ndfc_version(self): + # """ + # Populate self.ndfc_version with the NDFC version. + + # TODO:3 Remove if 12.1.3b works with no changes to request/response payloads. + + # Notes: + # 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + # imports resulting in RecursionError + # """ + # instance = NdfcVersion(self.module) + # instance.refresh() + # self.ndfc_version = instance.version + + def prune_serial_numbers(self): + """ + If the image is already validated on a switch, remove that switch's + serial number from the list of serial numbers to validate. + """ + serial_numbers = copy.copy(self.serial_numbers) + for serial_number in serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.validated == "Success": + msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " + msg += "image already validated for " + msg += f"{self.issu_detail.serial_number}, " + msg += f"{self.issu_detail.ip_address}" + self.log_msg(msg) + self.serial_numbers.remove(self.issu_detail.serial_number) + + def validate_serial_numbers(self): + """ + Log a warning if the validated state for any serial_number + is Failed. + + TODO:1 Need a way to compare current image_policy with the image policy in the response + TODO:3 If validate == Failed, it may have been from the last operation. + TODO:3 We can't fail here based on this until we can verify the failure is happening for the current image_policy. + TODO:3 Change this to a log message and update the unit test if we can't verify the failure is happening for the current image_policy. + """ + for serial_number in self.serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.validated == "Failed": + msg = f"{self.class_name}.validate_serial_numbers: " + msg += "image validation is failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += "If this persists, check the switch connectivity to NDFC and " + msg += "try again." + #self.log_msg(msg) + self.module.fail_json(msg) + + def build_payload(self): + self.payload = {} + self.payload["serialNum"] = self.serial_numbers + self.payload["nonDisruptive"] = self.non_disruptive + + def commit(self): + """ + Commit the image validation request to NDFC and wait + for the images to be validated. + """ + if self.serial_numbers is None: + msg = f"{self.class_name}.commit() call instance.serial_numbers " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.serial_numbers) == 0: + msg = f"REMOVE: {self.class_name}.commit() no serial numbers " + msg += "to validate." + self.log_msg(msg) + return + self.prune_serial_numbers() + self.validate_serial_numbers() + self._wait_for_current_actions_to_complete() + path = self.endpoints.image_validate.get("path") + verb = self.endpoints.image_validate.get("verb") + self.build_payload() + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_validate_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not validate an image if there are any actions in progress. + Wait for all actions to complete before validating image. + Actions include image staging, image upgrade, and image validation. + """ + self.serial_numbers_done = set() + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + timeout = self.check_timeout + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} actions in progress: " + msg += f"{self.issu_detail.actions_in_progress}, " + msg += f"{timeout} seconds remaining." + self.log_msg(msg) + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"Timed out waiting for actions to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + def _wait_for_image_validate_to_complete(self): + """ + Wait for image validation to complete + """ + # We're promiting serial_numbers_done to a class-level attribute + # so that it can be used in unit test asserts. + self.serial_numbers_done = set() + timeout = self.check_timeout + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" + self.log_msg(msg) + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + validated_percent = self.issu_detail.validated_percent + validated_status = self.issu_detail.validated + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"validated_percent: {validated_percent} " + msg += f"validated_state: {validated_status}" + self.log_msg(msg) + + if validated_status == "Failed": + msg = f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}. " + msg += "Check the switch e.g. show install log detail, " + msg += "show incompatibility-all nxos . Or " + msg += "check NDFC Operations > Image Management > " + msg += "Devices > View Details > Validate for " + msg += "more details." + self.module.fail_json(msg) + + if validated_status == "Success": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + + if validated_status == None: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += "not started for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + + if validated_status == "In Progress": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Timed out waiting for image validation to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to stage. + + This must be set before calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.__class__.__name__}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + + @property + def non_disruptive(self): + """ + Set the non_disruptive flag to True or False. + """ + return self.properties.get("non_disruptive") + + @non_disruptive.setter + def non_disruptive(self, value): + value = self.make_boolean(value) + if not isinstance(value, bool): + msg = f"{self.class_name}.non_disruptive: " + msg += "instance.non_disruptive must " + msg += f"be a boolean. Got {value}." + self.module.fail_json(msg) + self.properties["non_disruptive"] = value + + @property + def ndfc_data(self): + """ + Return the result of the image staging request + for serial_numbers. + + instance.serial_numbers must be set first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def check_interval(self): + """ + Return the validate check interval in seconds + """ + return self.properties.get("check_interval") + + @check_interval.setter + def check_interval(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_interval must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_interval"] = value + + @property + def check_timeout(self): + """ + Return the validate check timeout in seconds + """ + return self.properties.get("check_timeout") + + @check_timeout.setter + def check_timeout(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_timeout must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_timeout"] = value + diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py new file mode 100644 index 000000000..898e81241 --- /dev/null +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -0,0 +1,461 @@ +from time import sleep +import json +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints + +class NdfcImageInstallOptions(NdfcCommon): + """ + Retrieve install-options details for ONE switch from NDFC and + provide property accessors for the policy attributes. + + Caveats: + - This retrieves for a SINGLE switch only. + - Set serial_number and policy_name and call refresh() for + each switch separately. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImageInstallOptions(module) + # Mandatory + instance.policy_name = "NR3F" + instance.serial_number = "FDO211218GC" + # Optional + instance.epld = True + instance.package_install = True + instance.issu = True + # Retrieve install-options details from NDFC + instance.refresh() + if instance.device_name is None: + print("Cannot retrieve policy/serial_number combination from NDFC") + exit(1) + status = instance.status + platform = instance.platform + etc... + + install-options are retrieved by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options + Request body: + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + }, + { + "serialNumber": "FDO211218GC", + "policyName": "NR3F" + } + ], + "issu": true, + "epld": false, + "packageInstall": false + } + Response body: + NOTES: + 1. epldModules will be null if epld is false in the request body. + This class converts this to None (python NoneType) in this case. + + { + "compatibilityStatusList": [ + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "platform": "N9K/N3K", + "version": "10.2.5", + "osType": "64bit", + "status": "Skipped", + "installOption": "NA", + "compDisp": "Compatibility status skipped.", + "versionCheck": "Compatibility status skipped.", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "timestamp": "NA" + } + ], + "epldModules": { + "moduleList": [ + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "IO FPGA", + "oldVersion": "0x15", + "newVersion": "0x15" + }, + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "MI FPGA", + "oldVersion": "0x4", + "newVersion": "0x04" + } + ], + "bException": false, + "exceptionReason": null + }, + "installPacakges": null, + "errMessage": "" + } + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["epld"] = False + self.properties["issu"] = True + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["package_install"] = False + self.properties["policy_name"] = None + self.properties["serial_number"] = None + self.properties["epld_modules"] = None + + def refresh(self): + """ + Refresh self.data with current install-options from NDFC + """ + if self.policy_name is None: + msg = f"{self.class_name}.refresh: " + msg += "instance.policy_name must be set before " + msg += "calling refresh()" + self.module.fail_json(msg) + if self.serial_number is None: + msg = f"{self.class_name}.refresh: " + msg += f"instance.serial_number must be set before " + msg += f"calling refresh()" + self.module.fail_json(msg) + + path = self.endpoints.install_options.get("path") + verb = self.endpoints.install_options.get("verb") + self._build_payload() + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"payload: {self.payload}" + self.log_msg(msg) + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"ndfc_response: {self.ndfc_response}" + self.log_msg(msg) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + # TODO:2 should error message contain full response or just DATA.error? + if self.ndfc_result["success"] is False: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retrieving install-options from NDFC. " + msg += f"NDFC response: {self.ndfc_response}" + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.data = self.properties["ndfc_data"] + if self.data.get("compatibilityStatusList") is None: + self.compatibility_status = {} + else: + self.compatibility_status = self.data.get("compatibilityStatusList")[0] + + def _build_payload(self): + """ + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + } + ], + "issu": true, + "epld": false, + "packageInstall": false + } + """ + self.payload = {} + self.payload["devices"] = [] + devices = {} + devices["serialNumber"] = self.serial_number + devices["policyName"] = self.policy_name + self.payload["devices"].append(devices) + self.payload["issu"] = self.issu + self.payload["epld"] = self.epld + self.payload["packageInstall"] = self.package_install + + def _get(self, item): + return self.data.get(item) + + # Mandatory properties + @property + def policy_name(self): + """ + Set the policy_name of the policy to query. + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def serial_number(self): + """ + Set the serial_number of the device to query. + """ + return self.properties.get("serial_number") + + @serial_number.setter + def serial_number(self, value): + self.properties["serial_number"] = value + + # Optional properties + @property + def issu(self): + """ + Enable (True) or disable (False) issu compatibility check. + Valid values: + True - Enable issu compatibility check + False - Disable issu compatibility check + Default: True + """ + return self.properties.get("issu") + + @issu.setter + def issu(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.issu.setter: " + msg += "issu must be a boolean value" + self.module.fail_json(msg) + self.properties["issu"] = value + + @property + def epld(self): + """ + Enable (True) or disable (False) epld compatibility check. + + Valid values: + True - Enable epld compatibility check + False - Disable epld compatibility check + Default: False + """ + return self.properties.get("epld") + + @epld.setter + def epld(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.epld.setter: " + msg += "epld must be a boolean value" + self.module.fail_json(msg) + self.properties["epld"] = value + + @property + def package_install(self): + """ + Enable (True) or disable (False) package_install compatibility check. + Valid values: + True - Enable package_install compatibility check + False - Disable package_install compatibility check + Default: False + """ + return self.properties.get("package_install") + + @package_install.setter + def package_install(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.package_install.setter: " + msg += "package_install must be a boolean value" + self.module.fail_json(msg) + self.properties["package_install"] = value + + # Retrievable properties + @property + def comp_disp(self): + """ + Return the compDisp (CLI output from show install all status) + of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("compDisp") + + @property + def device_name(self): + """ + Return the deviceName of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("deviceName") + + @property + def epld_modules(self): + """ + Return the epldModules of the install-options response, + if it exists. + Return None otherwise + + epldModules will be "null" if self.epld is False, so + make_none() will convert to NoneType in this case. + """ + return self.make_none(self._get("epldModules")) + + @property + def err_message(self): + """ + Return the errMessage of the install-options response, + if it exists. + Return None otherwise + + epldModules will be "null" if self.epld is False, so + make_none() will convert to NoneType in this case. + """ + return self._get("errMessage") + + @property + def install_option(self): + """ + Return the installOption of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("installOption") + + @property + def install_packages(self): + """ + Return the installPackages of the install-options response, + if it exists. + Return None otherwise + + NOTE: yes, installPacakges is misspelled in the response in the + following versions (at least): + 12.1.2e + 12.1.3b + """ + return self.make_none(self._get("installPacakges")) + + @property + def ip_address(self): + """ + Return the ipAddress of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("ipAddress") + + @property + def ndfc_data(self): + """ + Return the raw data from the NDFC response. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_response(self): + """ + Return the response from NDFC of the query. + """ + return self.properties.get("ndfc_response") + + @property + def ndfc_result(self): + """ + Return the result from NDFC of the query. + """ + return self.properties.get("ndfc_result") + + @property + def os_type(self): + """ + Return the osType of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("osType") + + @property + def platform(self): + """ + Return the platform of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("platform") + + @property + def pre_issu_link(self): + """ + Return the preIssuLink of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("preIssuLink") + + @property + def raw_data(self): + """ + Return the raw data of the install-options response, if it exists. + """ + return self.data + + @property + def raw_response(self): + """ + Return the raw response, if it exists. + """ + return self.response + + @property + def rep_status(self): + """ + Return the repStatus of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("repStatus") + + @property + def status(self): + """ + Return the status of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("status") + + @property + def timestamp(self): + """ + Return the timestamp of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("timestamp") + + @property + def version(self): + """ + Return the version of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("version") + + @property + def version_check(self): + """ + Return the versionCheck (version check CLI output) + of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("versionCheck") diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py new file mode 100644 index 000000000..9d1a5c4a8 --- /dev/null +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -0,0 +1,184 @@ +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints + +class NdfcSwitchDetails(NdfcCommon): + """ + Retrieve switch details from NDFC and provide property accessors + for the switch attributes. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchDetails(module) + instance.refresh() + instance.ip_address = 10.1.1.1 + fabric_name = instance.fabric_name + serial_number = instance.serial_number + etc... + + Switch details are retrieved by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["ip_address"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh switch_details with current switch details from NDFC + """ + path = self.endpoints.switches_info.get("path") + verb = self.endpoints.switches_info.get("verb") + self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") + self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response {self.ndfc_response}" + self.log_msg(msg) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_result {self.ndfc_result}" + self.log_msg(msg) + + if self.ndfc_response["RETURN_CODE"] != 200: + msg = "Unable to retrieve switch information from NDFC. " + msg += f"Got response {self.ndfc_response}" + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA") + self.properties["ndfc_data"] = {} + for switch in data: + self.properties["ndfc_data"][switch["ipAddress"]] = switch + + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_data {self.ndfc_data}" + self.log_msg(msg) + + def _get(self, item): + if self.ip_address is None: + msg = f"{self.class_name}: set instance.ip_address " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + return self.make_boolean( + self.make_none( + self.properties["ndfc_data"][self.ip_address].get(item) + ) + ) + + @property + def ip_address(self): + """ + Set the ip_address of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("ip_address") + + @ip_address.setter + def ip_address(self, value): + self.properties["ip_address"] = value + + @property + def fabric_name(self): + """ + Return the fabricName of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("fabricName") + + @property + def hostname(self): + """ + Return the hostName of the switch with ip_address, if it exists. + Return None otherwise + + NOTES: + 1. This is None for 12.1.2e + 2. Better to use logical_name which is populated in both 12.1.2e and 12.1.3b + """ + return self._get("hostName") + + @property + def logical_name(self): + """ + Return the logicalName of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("logicalName") + + @property + def model(self): + """ + Return the model of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("model") + + @property + def ndfc_data(self): + """ + Return parsed data from the GET request. + Return None otherwise + + NOTE: Keyed on ip_address + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the GET request. + Return None otherwise + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result of the GET request. + Return None otherwise + """ + return self.properties["ndfc_result"] + + @property + def platform(self): + """ + Return the platform of the switch with ip_address, if it exists. + Return None otherwise + + NOTE: This is derived from "model" and is not in the NDFC response + """ + model = self._get("model") + if model is None: + return None + return model.split("-")[0] + + @property + def role(self): + """ + Return the switchRole of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("switchRole") + + @property + def serial_number(self): + """ + Return the serialNumber of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("serialNumber") + diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py new file mode 100644 index 000000000..f108c2e1d --- /dev/null +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -0,0 +1,828 @@ +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import dcnm_send +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints + + +class NdfcSwitchIssuDetails(NdfcCommon): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes. + + Usage: See subclasses. + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu + + Response body: + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO211218GC", + "deviceName": "cvd-1312-leaf", + "fabric": "fff", + "version": "10.3(2)", + "policy": "NR3F", + "status": "In-Sync", + "reason": "Compliance", + "imageStaged": "Success", + "validated": "None", + "upgrade": "None", + "upgGroups": "None", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": null, + "vpcPeer": null, + "role": "leaf", + "lastUpgAction": "Never", + "model": "N9K-C93180YC-EX", + "ipAddress": "172.22.150.103", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 8430, + "platform": "N9K", + "vpc_role": null, + "ip_address": "172.22.150.103", + "peer": null, + "vdc_id": -1, + "sys_name": "cvd-1312-leaf", + "id": 3, + "group": "fff", + "fcoEEnabled": false, + "mds": false + }, + {etc...} + ] + + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_data"] = None + # action_keys is used in subclasses to determine if any actions + # are in progress. + # Property actions_in_progress returns True if so, False otherwise + self.properties["action_keys"] = set() + self.properties["action_keys"].add("imageStaged") + self.properties["action_keys"].add("upgrade") + self.properties["action_keys"].add("validated") + + + def refresh(self) -> None: + """ + Refresh current issu details from NDFC + """ + path = self.endpoints.issu_info.get("path") + verb = self.endpoints.issu_info.get("verb") + + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"result: {self.ndfc_result}" + # ndfc_result for 404 response + # {'found': False, 'success': True} + if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retriving switch " + msg += "information from NDFC" + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA").get("lastOperDataObject") + if data is None: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no switch ISSU information." + self.module.fail_json(msg) + if len(data) == 0: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no switch ISSU information." + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA", {}).get( + "lastOperDataObject", [] + ) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + @property + def actions_in_progress(self): + """ + Return True if any actions are in progress + Return False otherwise + """ + for action_key in self.properties["action_keys"]: + if self._get(action_key) == "In-Progress": + return True + return False + + def _get(self, item): + """ + overridden in subclasses + """ + pass + + @property + def ndfc_data(self): + """ + Return the raw data retrieved from NDFC + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the GET request. + Return None otherwise + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result of the GET request. + Return None otherwise + """ + return self.properties["ndfc_result"] + + @property + def device_name(self): + """ + Return the deviceName of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + device name, e.g. "cvd-1312-leaf" + None + """ + return self._get("deviceName") + + @property + def eth_switch_id(self): + """ + Return the ethswitchid of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + integer + None + """ + return self._get("ethswitchid") + + @property + def fabric(self): + """ + Return the fabric of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + fabric name, e.g. "myfabric" + None + """ + return self._get("fabric") + + @property + def fcoe_enabled(self): + """ + Return whether FCOE is enabled on the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + boolean (true/false) + None + """ + return self.make_boolean(self._get("fcoEEnabled")) + + @property + def group(self): + """ + Return the group of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + group name, e.g. "mygroup" + None + """ + return self._get("group") + + @property + # id is a python keyword, so we can't use it as a property name + # so we use switch_id instead + def switch_id(self): + """ + Return the switch ID of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("id") + + @property + def image_staged(self): + """ + Return the imageStaged of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Success + Failed + None + """ + return self._get("imageStaged") + + @property + def image_staged_percent(self): + """ + Return the imageStagedPercent of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("imageStagedPercent") + + @property + def ip_address(self): + """ + Return the ipAddress of the switch, if it exists. + Return None otherwise + + Possible values: + switch IP address + None + """ + return self._get("ipAddress") + + @property + def issu_allowed(self): + """ + Return the issuAllowed value of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + "" + None + """ + return self._get("issuAllowed") + + @property + def last_upg_action(self): + """ + Return the last upgrade action performed on the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + Never + None + """ + return self._get("lastUpgAction") + + @property + def mds(self): + """ + Return whether the switch with ip_address is an MSD, if it exists. + Return None otherwise + + Possible values: + Boolean (True or False) + None + """ + return self.make_boolean(self._get("mds")) + + @property + def mode(self): + """ + Return the ISSU mode of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + "Normal" + None + """ + return self._get("mode") + + @property + def model(self): + """ + Return the model of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + model number e.g. "N9K-C93180YC-EX" + None + """ + return self._get("model") + + @property + def model_type(self): + """ + Return the model type of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("modelType") + + @property + def peer(self): + """ + Return the peer of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + None + """ + return self._get("peer") + + @property + def platform(self): + """ + Return the platform of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + platform, e.g. "N9K" + None + """ + return self._get("platform") + + @property + def policy(self): + """ + Return the policy attached to the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + policy name, e.g. "NR3F" + None + """ + return self._get("policy") + + @property + def reason(self): + """ + Return the reason (?) of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Compliance + Validate + Upgrade + None + """ + return self._get("reason") + + @property + def role(self): + """ + Return the role of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + switch role, e.g. "leaf" + None + """ + return self._get("role") + + @property + def serial_number(self): + """ + Return the serialNumber of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + switch serial number, e.g. "AB1234567CD" + None + """ + return self._get("serialNumber") + + @property + def status(self): + """ + Return the sync status of the switch with ip_address, if it exists. + Return None otherwise + + Details: The sync status is the status of the switch with respect + to the image policy. If the switch is in sync with the image policy, + the status is "In-Sync". If the switch is out of sync with the image + policy, the status is "Out-Of-Sync". + + Possible values: + "In-Sync" + "Out-Of-Sync" + None + """ + return self._get("status") + + @property + def status_percent(self): + """ + Return the upgrade (TODO:3 verify this) percentage completion + of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("statusPercent") + + @property + def sys_name(self): + """ + Return the system name of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + system name, e.g. "cvd-1312-leaf" + None + """ + return self._get("sys_name") + + @property + def system_mode(self): + """ + Return the system mode of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + "Maintenance" (TODO:3 verify this) + "Normal" + None + """ + return self._get("systemMode") + + @property + def upgrade(self): + """ + Return the upgrade status of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + Success + In-Progress + None + """ + return self._get("upgrade") + + @property + def upg_groups(self): + """ + Return the upgGroups (upgrade groups) of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + upgrade group to which the switch belongs e.g. "LEAFS" + None + """ + return self._get("upgGroups") + + @property + def upgrade_percent(self): + """ + Return the upgrade percent complete of the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("upgradePercent") + + @property + def validated(self): + """ + Return the validation status of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + Failed + Success + None + """ + return self._get("validated") + + @property + def validated_percent(self): + """ + Return the validation percent complete of the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("validatedPercent") + + @property + def vdc_id(self): + """ + Return the vdcId of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("vdcId") + + @property + def vdc_id2(self): + """ + Return the vdc_id of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer (negative values are valid) + None + """ + return self._get("vdc_id") + + @property + def version(self): + """ + Return the version of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + version, e.g. "10.3(2)" + None + """ + return self._get("version") + + @property + def vpc_peer(self): + """ + Return the vpcPeer of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + vpc peer e.g.: 10.1.1.1 + None + """ + return self._get("vpcPeer") + + @property + def vpc_role(self): + """ + Return the vpcRole of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + vpc role e.g.: + "primary" + "secondary" + "none" -> This will be translated to None + "none established" (TODO:3 verify this) + "primary, operational secondary" (TODO:3 verify this) + None + """ + return self._get("vpcRole") + + +class NdfcSwitchIssuDetailsByIpAddress(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by ip address. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsByIpAddress(module) + instance.refresh() + instance.ip_address = 10.1.1.1 + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + serial_number = instance.serial_number + etc... + + See NdfcSwitchIssuDetails for more details. + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["ip_address"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh ip_address current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + msg = f"{self.class_name}.refresh: " + msg += f"switch {switch}" + self.data_subclass[switch["ipAddress"]] = switch + + def _get(self, item): + if self.ip_address is None: + msg = f"{self.class_name}: set instance.ip_address " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.ip_address) is None: + msg = f"{self.class_name}: {self.ip_address} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.ip_address].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.ip_address. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.ip_address) + + @property + def ip_address(self): + """ + Set the ip_address of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("ip_address") + + @ip_address.setter + def ip_address(self, value): + self.properties["ip_address"] = value + + +class NdfcSwitchIssuDetailsBySerialNumber(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by serial_number. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsBySerialNumber(module) + instance.refresh() + instance.serial_number = "FDO211218GC" + instance.refresh() + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + ip_address = instance.ip_address + etc... + + See NdfcSwitchIssuDetails for more details. + + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["serial_number"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh serial_number current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + self.data_subclass[switch["serialNumber"]] = switch + + def _get(self, item): + if self.serial_number is None: + msg = f"{self.class_name}: set instance.serial_number " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.serial_number) is None: + msg = f"{self.class_name}: {self.serial_number} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.serial_number].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.serial_number. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.serial_number) + + @property + def serial_number(self): + """ + Set the serial_number of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("serial_number") + + @serial_number.setter + def serial_number(self, value): + self.properties["serial_number"] = value + + +class NdfcSwitchIssuDetailsByDeviceName(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by device_name. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsByDeviceName(module) + instance.refresh() + instance.device_name = "leaf_1" + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + ip_address = instance.ip_address + etc... + + See NdfcSwitchIssuDetails for more details. + + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["device_name"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh device_name current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + self.data_subclass[switch["deviceName"]] = switch + + def _get(self, item): + if self.device_name is None: + msg = f"{self.class_name}: set instance.device_name " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.device_name) is None: + msg = f"{self.class_name}: {self.device_name} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.device_name].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.device_name. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.device_name) + + @property + def device_name(self): + """ + Set the device_name of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("device_name") + + @device_name.setter + def device_name(self, value): + self.properties["device_name"] = value \ No newline at end of file diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 6f921d15e..0f8ef02b9 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -26,9 +26,17 @@ import copy import json -from time import sleep from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import NdfcImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import NdfcImageStage +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import NdfcImageValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import NdfcImageUpgrade +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import NdfcImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import NdfcSwitchDetails +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsByIpAddress from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, validate_list_of_dicts, @@ -395,293 +403,17 @@ - ip_address: 192.168.1.2 """ -class NdfcEndpoints: - """ - Endpoints for NDFC API calls - """ - def __init__(self): - self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" - - self.endpoint_feature_manager = f"{self.endpoint_api_v1}/fm" - self.endpoint_lan_fabric = f"{self.endpoint_api_v1}/lan-fabric" - - self.endpoint_image_management = f"{self.endpoint_api_v1}" - self.endpoint_image_management += "/imagemanagement" - - self.endpoint_image_upgrade = f"{self.endpoint_image_management}" - self.endpoint_image_upgrade += "/rest/imageupgrade" - - self.endpoint_package_mgnt = f"{self.endpoint_image_management}" - self.endpoint_package_mgnt += "/rest/packagemgnt" - - self.endpoint_policy_mgnt = f"{self.endpoint_image_management}" - self.endpoint_policy_mgnt += "/rest/policymgnt" - - self.endpoint_staging_management = f"{self.endpoint_image_management}" - self.endpoint_staging_management += "/rest/stagingmanagement" - - @property - def bootflash_info(self): - path = f"{self.endpoint_image_management}/rest/imagemgnt/bootFlash" - path += f"/bootflash-info" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def install_options(self): - path = f"{self.endpoint_image_upgrade}/install-options" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def image_stage(self): - path = f"{self.endpoint_staging_management}/stage-image" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def image_upgrade(self): - path = f"{self.endpoint_image_upgrade}/upgrade-image" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def image_validate(self): - path = f"{self.endpoint_staging_management}/validate-image" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def issu_info(self): - path = f"{self.endpoint_package_mgnt}/issu" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def ndfc_version(self): - path = f"{self.endpoint_feature_manager}/about/version" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def policies_attached_info(self): - path = f"{self.endpoint_policy_mgnt}/all-attached-policies" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def policies_info(self): - path = f"{self.endpoint_policy_mgnt}/policies" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def policy_attach(self): - path = f"{self.endpoint_policy_mgnt}/attach-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def policy_create(self): - path = f"{self.endpoint_policy_mgnt}/platform-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def policy_detach(self): - path = f"{self.endpoint_policy_mgnt}/detach-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "DELETE" - return endpoint - - @property - def policy_info(self): - # Replace __POLICY_NAME__ with the policy_name to query - # e.g. path.replace("__POLICY_NAME__", "NR1F") - path = f"{self.endpoint_policy_mgnt}/image-policy/__POLICY_NAME__" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def stage_info(self): - path = f"{self.endpoint_staging_management}/stage-info" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def switches_info(self): - path = f"{self.endpoint_lan_fabric}/rest/inventory/allswitches" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - -class NdfcAnsibleImageUpgradeCommon: - """ - Base class for the other classes in this file - """ - - def __init__(self, module): - self.module = module - self.params = module.params - self.debug = True - self.fd = None - self.logfile = "/tmp/dcnm_image_upgrade.log" - self.endpoints = NdfcEndpoints() - - - def _handle_response(self, response, verb): - if verb == "GET": - return self._handle_get_response(response) - if verb in {"POST", "PUT", "DELETE"}: - return self._handle_post_put_delete_response(response) - return self._handle_unknown_request_verbs(response, verb) - - def _handle_unknown_request_verbs(self, response, verb): - msg = f"Unknown request verb ({verb}) in _handle_response() for response {response}." - self.module.fail_json(msg) - - def _handle_get_response(self, response): - """ - Caller: - - self._handle_response() - Handle NDFC responses to GET requests - Returns: dict() with the following keys: - - found: - - False, if request error was "Not found" and RETURN_CODE == 404 - - True otherwise - - success: - - False if RETURN_CODE != 200 or MESSAGE != "OK" - - True otherwise - """ - result = {} - success_return_codes = {200, 404} - if ( - response.get("RETURN_CODE") == 404 - and response.get("MESSAGE") == "Not Found" - ): - result["found"] = False - result["success"] = True - return result - if ( - response.get("RETURN_CODE") not in success_return_codes - or response.get("MESSAGE") != "OK" - ): - result["found"] = False - result["success"] = False - return result - result["found"] = True - result["success"] = True - return result - - def _handle_post_put_delete_response(self, response): - """ - Caller: - - self.self._handle_response() - - Handle POST, PUT responses from NDFC. - - Returns: dict() with the following keys: - - changed: - - True if changes were made to NDFC - - False otherwise - - success: - - False if RETURN_CODE != 200 or MESSAGE != "OK" - - True otherwise - """ - result = {} - if response.get("ERROR") is not None: - result["success"] = False - result["changed"] = False - return result - if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: - result["success"] = False - result["changed"] = False - return result - result["success"] = True - result["changed"] = True - return result - - def log_msg(self, msg): - """ - used for debugging. disable this when committing to main - by setting __init__().debug to False - """ - if self.debug is False: - return - if self.fd is None: - try: - self.fd = open(f"{self.logfile}", "a+", encoding="UTF-8") - except IOError as err: - msg = f"error opening logfile {self.logfile}. " - msg += f"detail: {err}" - self.module.fail_json(msg) - - self.fd.write(msg) - self.fd.write("\n") - self.fd.flush() - - def make_boolean(self, value): - """ - Return value converted to boolean, if possible. - Return value, if value cannot be converted. - """ - if isinstance(value, bool): - return value - if isinstance(value, str): - if value.lower() in ["true", "yes"]: - return True - if value.lower() in ["false", "no"]: - return False - return value - - def make_none(self, value): - """ - Return None if value is an empty string, or a string - representation of a None type - Return value otherwise - """ - if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: - return None - return value - -class NdfcAnsibleImageUpgrade(NdfcAnsibleImageUpgradeCommon): +class NdfcAnsibleImageUpgrade(NdfcCommon): """ Ansible support for image policy attach, detach, and query. """ def __init__(self, module): super().__init__(module) + self.params = self.module.params self.class_name = self.__class__.__name__ + self.endpoints = NdfcEndpoints() self.log_msg(f"{self.class_name}.__init__") # populated in self._build_policy_attach_payload() self.payloads = [] @@ -1508,3754 +1240,7 @@ def _failure(self, resp): self.module.fail_json(msg=res) -class NdfcSwitchDetails(NdfcAnsibleImageUpgradeCommon): - """ - Retrieve switch details from NDFC and provide property accessors - for the switch attributes. - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcSwitchDetails(module) - instance.refresh() - instance.ip_address = 10.1.1.1 - fabric_name = instance.fabric_name - serial_number = instance.serial_number - etc... - - Switch details are retrieved by calling instance.refresh(). - - Endpoint: - /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["ip_address"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None - - def refresh(self): - """ - Caller: __init__() - - Refresh switch_details with current switch details from NDFC - """ - path = self.endpoints.switches_info.get("path") - verb = self.endpoints.switches_info.get("verb") - self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") - self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response {self.ndfc_response}" - self.log_msg(msg) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_result {self.ndfc_result}" - self.log_msg(msg) - - if self.ndfc_response["RETURN_CODE"] != 200: - msg = "Unable to retrieve switch information from NDFC. " - msg += f"Got response {self.ndfc_response}" - self.module.fail_json(msg) - - data = self.ndfc_response.get("DATA") - self.properties["ndfc_data"] = {} - for switch in data: - self.properties["ndfc_data"][switch["ipAddress"]] = switch - - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_data {self.ndfc_data}" - self.log_msg(msg) - - def _get(self, item): - if self.ip_address is None: - msg = f"{self.class_name}: set instance.ip_address " - msg += f"before accessing property {item}." - self.module.fail_json(msg) - return self.make_boolean( - self.make_none( - self.properties["ndfc_data"][self.ip_address].get(item) - ) - ) - - @property - def ip_address(self): - """ - Set the ip_address of the switch to query. - - This needs to be set before accessing this class's properties. - """ - return self.properties.get("ip_address") - - @ip_address.setter - def ip_address(self, value): - self.properties["ip_address"] = value - - @property - def fabric_name(self): - """ - Return the fabricName of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("fabricName") - - @property - def hostname(self): - """ - Return the hostName of the switch with ip_address, if it exists. - Return None otherwise - - NOTES: - 1. This is None for 12.1.2e - 2. Better to use logical_name which is populated in both 12.1.2e and 12.1.3b - """ - return self._get("hostName") - - @property - def logical_name(self): - """ - Return the logicalName of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("logicalName") - - @property - def model(self): - """ - Return the model of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("model") - - @property - def ndfc_data(self): - """ - Return parsed data from the GET request. - Return None otherwise - - NOTE: Keyed on ip_address - """ - return self.properties["ndfc_data"] - - @property - def ndfc_response(self): - """ - Return the raw response from the GET request. - Return None otherwise - """ - return self.properties["ndfc_response"] - - @property - def ndfc_result(self): - """ - Return the raw result of the GET request. - Return None otherwise - """ - return self.properties["ndfc_result"] - - @property - def platform(self): - """ - Return the platform of the switch with ip_address, if it exists. - Return None otherwise - - NOTE: This is derived from "model" and is not in the NDFC response - """ - model = self._get("model") - if model is None: - return None - return model.split("-")[0] - - @property - def role(self): - """ - Return the switchRole of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("switchRole") - - @property - def serial_number(self): - """ - Return the serialNumber of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("serialNumber") - - -class NdfcImageInstallOptions(NdfcAnsibleImageUpgradeCommon): - """ - Retrieve install-options details for ONE switch from NDFC and - provide property accessors for the policy attributes. - - Caveats: - - This retrieves for a SINGLE switch only. - - Set serial_number and policy_name and call refresh() for - each switch separately. - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcImageInstallOptions(module) - # Mandatory - instance.policy_name = "NR3F" - instance.serial_number = "FDO211218GC" - # Optional - instance.epld = True - instance.package_install = True - instance.issu = True - # Retrieve install-options details from NDFC - instance.refresh() - if instance.device_name is None: - print("Cannot retrieve policy/serial_number combination from NDFC") - exit(1) - status = instance.status - platform = instance.platform - etc... - - install-options are retrieved by calling instance.refresh(). - - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options - Request body: - { - "devices": [ - { - "serialNumber": "FDO211218HH", - "policyName": "NR1F" - }, - { - "serialNumber": "FDO211218GC", - "policyName": "NR3F" - } - ], - "issu": true, - "epld": false, - "packageInstall": false - } - Response body: - NOTES: - 1. epldModules will be null if epld is false in the request body. - This class converts this to None (python NoneType) in this case. - - { - "compatibilityStatusList": [ - { - "deviceName": "cvd-1312-leaf", - "ipAddress": "172.22.150.103", - "policyName": "KR5M", - "platform": "N9K/N3K", - "version": "10.2.5", - "osType": "64bit", - "status": "Skipped", - "installOption": "NA", - "compDisp": "Compatibility status skipped.", - "versionCheck": "Compatibility status skipped.", - "preIssuLink": "Not Applicable", - "repStatus": "skipped", - "timestamp": "NA" - } - ], - "epldModules": { - "moduleList": [ - { - "deviceName": "cvd-1312-leaf", - "ipAddress": "172.22.150.103", - "policyName": "KR5M", - "module": 1, - "name": null, - "modelName": "N9K-C93180YC-EX", - "moduleType": "IO FPGA", - "oldVersion": "0x15", - "newVersion": "0x15" - }, - { - "deviceName": "cvd-1312-leaf", - "ipAddress": "172.22.150.103", - "policyName": "KR5M", - "module": 1, - "name": null, - "modelName": "N9K-C93180YC-EX", - "moduleType": "MI FPGA", - "oldVersion": "0x4", - "newVersion": "0x04" - } - ], - "bException": false, - "exceptionReason": null - }, - "installPacakges": null, - "errMessage": "" - } - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["epld"] = False - self.properties["issu"] = True - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None - self.properties["package_install"] = False - self.properties["policy_name"] = None - self.properties["serial_number"] = None - self.properties["epld_modules"] = None - - def refresh(self): - """ - Refresh self.data with current install-options from NDFC - """ - if self.policy_name is None: - msg = f"{self.class_name}.refresh: " - msg += "instance.policy_name must be set before " - msg += "calling refresh()" - self.module.fail_json(msg) - if self.serial_number is None: - msg = f"{self.class_name}.refresh: " - msg += f"instance.serial_number must be set before " - msg += f"calling refresh()" - self.module.fail_json(msg) - - path = self.endpoints.install_options.get("path") - verb = self.endpoints.install_options.get("verb") - self._build_payload() - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"payload: {self.payload}" - self.log_msg(msg) - self.properties["ndfc_response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) - ) - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"ndfc_response: {self.ndfc_response}" - self.log_msg(msg) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - # TODO:2 should error message contain full response or just DATA.error? - if self.ndfc_result["success"] is False: - msg = f"{self.class_name}.refresh: " - msg += "Bad result when retrieving install-options from NDFC. " - msg += f"NDFC response: {self.ndfc_response}" - self.module.fail_json(msg) - - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - self.data = self.properties["ndfc_data"] - if self.data.get("compatibilityStatusList") is None: - self.compatibility_status = {} - else: - self.compatibility_status = self.data.get("compatibilityStatusList")[0] - - def _build_payload(self): - """ - { - "devices": [ - { - "serialNumber": "FDO211218HH", - "policyName": "NR1F" - } - ], - "issu": true, - "epld": false, - "packageInstall": false - } - """ - self.payload = {} - self.payload["devices"] = [] - devices = {} - devices["serialNumber"] = self.serial_number - devices["policyName"] = self.policy_name - self.payload["devices"].append(devices) - self.payload["issu"] = self.issu - self.payload["epld"] = self.epld - self.payload["packageInstall"] = self.package_install - - def _get(self, item): - return self.data.get(item) - - # Mandatory properties - @property - def policy_name(self): - """ - Set the policy_name of the policy to query. - """ - return self.properties.get("policy_name") - - @policy_name.setter - def policy_name(self, value): - self.properties["policy_name"] = value - - @property - def serial_number(self): - """ - Set the serial_number of the device to query. - """ - return self.properties.get("serial_number") - - @serial_number.setter - def serial_number(self, value): - self.properties["serial_number"] = value - - # Optional properties - @property - def issu(self): - """ - Enable (True) or disable (False) issu compatibility check. - Valid values: - True - Enable issu compatibility check - False - Disable issu compatibility check - Default: True - """ - return self.properties.get("issu") - - @issu.setter - def issu(self, value): - if not isinstance(value, bool): - msg = f"{self.class_name}.issu.setter: " - msg += "issu must be a boolean value" - self.module.fail_json(msg) - self.properties["issu"] = value - - @property - def epld(self): - """ - Enable (True) or disable (False) epld compatibility check. - - Valid values: - True - Enable epld compatibility check - False - Disable epld compatibility check - Default: False - """ - return self.properties.get("epld") - - @epld.setter - def epld(self, value): - if not isinstance(value, bool): - msg = f"{self.class_name}.epld.setter: " - msg += "epld must be a boolean value" - self.module.fail_json(msg) - self.properties["epld"] = value - - @property - def package_install(self): - """ - Enable (True) or disable (False) package_install compatibility check. - Valid values: - True - Enable package_install compatibility check - False - Disable package_install compatibility check - Default: False - """ - return self.properties.get("package_install") - - @package_install.setter - def package_install(self, value): - if not isinstance(value, bool): - msg = f"{self.class_name}.package_install.setter: " - msg += "package_install must be a boolean value" - self.module.fail_json(msg) - self.properties["package_install"] = value - - # Retrievable properties - @property - def comp_disp(self): - """ - Return the compDisp (CLI output from show install all status) - of the install-options response, if it exists. - Return None otherwise - """ - return self.compatibility_status.get("compDisp") - - @property - def device_name(self): - """ - Return the deviceName of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("deviceName") - - @property - def epld_modules(self): - """ - Return the epldModules of the install-options response, - if it exists. - Return None otherwise - - epldModules will be "null" if self.epld is False, so - make_none() will convert to NoneType in this case. - """ - return self.make_none(self._get("epldModules")) - - @property - def err_message(self): - """ - Return the errMessage of the install-options response, - if it exists. - Return None otherwise - - epldModules will be "null" if self.epld is False, so - make_none() will convert to NoneType in this case. - """ - return self._get("errMessage") - - @property - def install_option(self): - """ - Return the installOption of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("installOption") - - @property - def install_packages(self): - """ - Return the installPackages of the install-options response, - if it exists. - Return None otherwise - - NOTE: yes, installPacakges is misspelled in the response in the - following versions (at least): - 12.1.2e - 12.1.3b - """ - return self.make_none(self._get("installPacakges")) - - @property - def ip_address(self): - """ - Return the ipAddress of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("ipAddress") - - @property - def ndfc_data(self): - """ - Return the raw data from the NDFC response. - """ - return self.properties.get("ndfc_data") - - @property - def ndfc_response(self): - """ - Return the response from NDFC of the query. - """ - return self.properties.get("ndfc_response") - - @property - def ndfc_result(self): - """ - Return the result from NDFC of the query. - """ - return self.properties.get("ndfc_result") - - @property - def os_type(self): - """ - Return the osType of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("osType") - - @property - def platform(self): - """ - Return the platform of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("platform") - - @property - def pre_issu_link(self): - """ - Return the preIssuLink of the install-options response, if it exists. - Return None otherwise - """ - return self.compatibility_status.get("preIssuLink") - - @property - def raw_data(self): - """ - Return the raw data of the install-options response, if it exists. - """ - return self.data - - @property - def raw_response(self): - """ - Return the raw response, if it exists. - """ - return self.response - - @property - def rep_status(self): - """ - Return the repStatus of the install-options response, if it exists. - Return None otherwise - """ - return self.compatibility_status.get("repStatus") - - @property - def status(self): - """ - Return the status of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("status") - - @property - def timestamp(self): - """ - Return the timestamp of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("timestamp") - - @property - def version(self): - """ - Return the version of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("version") - - @property - def version_check(self): - """ - Return the versionCheck (version check CLI output) - of the install-options response, if it exists. - Return None otherwise - """ - return self.compatibility_status.get("versionCheck") - - -# ============================================================================== -class NdfcImagePolicyAction(NdfcAnsibleImageUpgradeCommon): - """ - Perform image policy actions on NDFC on one or more switches. - - Support for the following actions: - - attach - - detach - - query - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcImagePolicyAction(module) - instance.policy_name = "NR3F" - instance.action = "attach" # or detach, or query - instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] - instance.commit() - # for query only - query_result = instance.query_result - - Endpoints: - For action == attach: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy - For action == detach: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy - For action == query: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - self.image_policies = NdfcImagePolicies(self.module) - self.switch_issu_details = NdfcSwitchIssuDetailsBySerialNumber(self.module) - self.valid_actions = {"attach", "detach", "query"} - - def _init_properties(self): - self.properties = {} - self.properties["action"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None - self.properties["policy_name"] = None - self.properties["query_result"] = None - self.properties["serial_numbers"] = None - - def build_attach_payload(self): - """ - build the payload to send in the POST request - to attach policies to devices - - caller _attach_policy() - """ - self.payloads = [] - # TODO:2 this results in more calls than necessary to NDFC. Need a better way to handle this. - self.switch_issu_details.refresh() - for serial_number in self.serial_numbers: - self.switch_issu_details.serial_number = serial_number - payload = {} - payload["policyName"] = self.policy_name - payload["hostName"] = self.switch_issu_details.device_name - payload["ipAddr"] = self.switch_issu_details.ip_address - payload["platform"] = self.switch_issu_details.platform - payload["serialNumber"] = self.switch_issu_details.serial_number - for item in payload: - if payload[item] is None: - msg = f"Unable to determine {item} for switch " - msg += f"{self.switch_issu_details.ip_address}, " - msg += f"{self.switch_issu_details.serial_number}, " - msg += f"{self.switch_issu_details.device_name}. " - msg += "Please verify that the switch is managed by NDFC." - self.module.fail_json(msg) - self.payloads.append(payload) - - def validate_request(self): - """ - validations prior to commit() should be added here. - """ - self.log_msg(f"REMOVE: {self.class_name}.validate_request: Entered") - if self.action is None: - msg = f"{self.class_name}.validate_request: " - msg += "instance.action must be set before " - msg += "calling commit()" - self.module.fail_json(msg) - - if self.policy_name is None: - msg = f"{self.class_name}.validate_request: " - msg += "instance.policy_name must be set before " - msg += "calling commit()" - self.module.fail_json(msg) - - self.log_msg(f"REMOVE: {self.class_name}.validate_request: action {self.action}") - - if self.action == "query": - return - - self.log_msg(f"REMOVE: {self.class_name}.validate_request: serial_numbers {self.serial_numbers}") - if self.serial_numbers is None: - msg = f"{self.class_name}.validate_request: " - msg += "instance.serial_numbers must be set before " - msg += "calling commit()" - self.module.fail_json(msg) - - - # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it - self.image_policies.refresh() - self.switch_issu_details.refresh() - # Fail if the image policy does not support the switch platform - self.image_policies.policy_name = self.policy_name - for serial_number in self.serial_numbers: - self.switch_issu_details.serial_number = serial_number - if self.switch_issu_details.platform not in self.image_policies.platform: - msg = f"policy {self.policy_name} does not support platform " - msg += f"{self.switch_issu_details.platform}. {self.policy_name} " - msg += "supports the following platform(s): " - msg += f"{self.image_policies.platform}" - self.module.fail_json(msg) - - def commit(self): - self.validate_request() - if self.action == "attach": - self._attach_policy() - elif self.action == "detach": - self._detach_policy() - elif self.action == "query": - self._query_policy() - else: - msg = f"{self.class_name}.commit: " - msg += f"Unknown action {self.action}." - self.module.fail_json(msg) - - def _attach_policy(self): - """ - Attach policy_name to the switch(es) associated with serial_numbers - - NOTES: - 1. This method creates a list of responses and results which - are accessible via properties ndfc_response and ndfc_result, - respectively. - """ - self.build_attach_payload() - path = self.endpoints.policy_attach.get("path") - verb = self.endpoints.policy_attach.get("verb") - responses = [] - results = [] - for payload in self.payloads: - response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) - result = self._handle_response(response, verb) - if not result["success"]: - msg = f"{self.class_name}._attach_policy: " - msg += f"Bad result when attaching policy {self.policy_name} " - msg += f"to switch {payload['ipAddr']}." - self.module.fail_json(msg) - responses.append(response) - results.append(result) - self.properties["ndfc_response"] = responses - self.properties["ndfc_result"] = results - - def _detach_policy(self): - """ - Detach policy_name from the switch(es) associated with serial_numbers - verb: DELETE - endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy - query_params: ?serialNumber=FDO211218GC,FDO21120U5D - """ - path = self.endpoints.policy_detach.get("path") - verb = self.endpoints.policy_detach.get("verb") - query_params = ",".join(self.serial_numbers) - path += f"?serialNumber={query_params}" - response = dcnm_send(self.module, verb, path) - result = self._handle_response(response, verb) - if not result["success"]: - self._failure(response) - self.properties["ndfc_response"] = response - self.properties["ndfc_result"] = result - - def _query_policy(self): - """ - Query the image policy - verb: GET - endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ - """ - path = self.endpoints.policy_info.get("path") - verb = self.endpoints.policy_info.get("verb") - path = path.replace("__POLICY_NAME__", self.policy_name) - response = dcnm_send(self.module, verb, path) - result = self._handle_response(response, verb) - if not result["success"]: - self._failure(response) - self.properties["query_result"] = response.get("DATA") - self.properties["ndfc_response"] = response - self.properties["ndfc_result"] = result - - @property - def query_result(self): - """ - Return the value of properties["query_result"]. - """ - return self.properties.get("query_result") - - @property - def action(self): - """ - Set the action to take. Either "attach" or "detach". - - Must be set prior to calling instance.commit() - """ - return self.properties.get("action") - - @action.setter - def action(self, value): - if value not in self.valid_actions: - msg = f"{self.class_name}: instance.action must be " - msg += f"one of {','.join(sorted(self.valid_actions))}" - self.module.fail_json(msg) - self.properties["action"] = value - - @property - def ndfc_response(self): - """ - Return the raw response from NDFC after calling commit(). - - In the case of attach, this is a list of responses. - """ - return self.properties.get("ndfc_response") - - @property - def ndfc_result(self): - """ - Return the raw result from NDFC after calling commit(). - - In the case of attach, this is a list of results. - """ - return self.properties.get("ndfc_result") - - @property - def policy_name(self): - """ - Set the name of the policy to attach, detach, query. - - Must be set prior to calling instance.commit() - """ - return self.properties.get("policy_name") - - @policy_name.setter - def policy_name(self, value): - self.properties["policy_name"] = value - - @property - def serial_numbers(self): - """ - Set the serial numbers of the switches to/from which - policy_name will be attached or detached. - - Must be set prior to calling instance.commit() - """ - return self.properties.get("serial_numbers") - - @serial_numbers.setter - def serial_numbers(self, value): - if not isinstance(value, list): - msg = f"{self.class_name}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." - self.module.fail_json(msg) - self.properties["serial_numbers"] = value - -# ============================================================================== - - -class NdfcImagePolicies(NdfcAnsibleImageUpgradeCommon): - """ - Retrieve image policy details from NDFC and provide property accessors - for the policy attributes. - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcImagePolicies(module).refresh() - instance.policy_name = "NR3F" - if instance.name is None: - print("policy NR3F does not exist on NDFC") - exit(1) - policy_name = instance.name - platform = instance.platform - epd_image_name = instance.epld_image_name - etc... - - Policies are retrieved on instantiation of this class. - Policies can be refreshed by calling instance.refresh(). - - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self.log_msg(f"{self.class_name}.__init__ entered") - self._init_properties() - # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it - #self.refresh() - - def _init_properties(self): - self.properties = {} - self.properties["policy_name"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None - - def refresh(self): - """ - Refresh self.image_policies with current image policies from NDFC - """ - path = self.endpoints.policies_info.get("path") - verb = self.endpoints.policies_info.get("verb") - - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") - - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") - - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"result: {self.ndfc_result}" - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.refresh: " - msg += "Bad result when retriving image policy " - msg += "information from NDFC." - self.module.fail_json(msg) - - data = self.ndfc_response.get("DATA").get("lastOperDataObject") - if data is None: - msg = f"{self.class_name}.refresh: " - msg += "Bad response when retrieving image policy " - msg += "information from NDFC." - self.module.fail_json(msg) - if len(data) == 0: - msg = f"{self.class_name}.refresh: " - msg += "NDFC has no defined image policies." - self.module.fail_json(msg) - self.properties["ndfc_data"] = {} - for policy in data: - policy_name = policy.get("policyName") - if policy_name is None: - msg = f"{self.class_name}.refresh: " - msg += "Cannot parse NDFC policy information" - self.module.fail_json(msg) - self.properties["ndfc_data"][policy_name] = policy - - def _get(self, item): - if self.policy_name is None: - msg = f"{self.class_name}._get: " - msg += f"instance.policy_name must be set before " - msg += f"accessing property {item}." - self.module.fail_json(msg) - if self.properties['ndfc_data'].get(self.policy_name) is None: - msg = f"{self.class_name}._get: " - msg += f"policy_name {self.policy_name} is not defined in NDFC" - self.module.fail_json(msg) - return_item = self.make_boolean(self.properties["ndfc_data"][self.policy_name].get(item)) - return_item = self.make_none(return_item) - return return_item - - @property - def description(self): - """ - Return the policyDescr of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("policyDescr") - - @property - def epld_image_name(self): - """ - Return the epldImgName of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("epldImgName") - - @property - def name(self): - """ - Return the name of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("policyName") - - @property - def ndfc_data(self): - """ - Return the parsed data from the NDFC response as a dictionary, - keyed on policy_name. - """ - return self.properties["ndfc_data"] - - @property - def ndfc_response(self): - """ - Return the raw response from the NDFC response. - """ - return self.properties["ndfc_response"] - - @property - def ndfc_result(self): - """ - Return the raw result from the NDFC response. - """ - return self.properties["ndfc_result"] - - @property - def policy_name(self): - """ - Set the name of the policy to query. - - This must be set prior to accessing any other properties - """ - return self.properties.get("policy_name") - - @policy_name.setter - def policy_name(self, value): - self.properties["policy_name"] = value - - @property - def policy_type(self): - """ - Return the policyType of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("policyType") - - @property - def nxos_version(self): - """ - Return the nxosVersion of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("nxosVersion") - - @property - def package_name(self): - """ - Return the packageName of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("packageName") - - @property - def platform(self): - """ - Return the platform of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("platform") - - @property - def platform_policies(self): - """ - Return the platformPolicies of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("platformPolicies") - - @property - def ref_count(self): - """ - Return the reference count of the policy matching self.policy_name, - if it exists. The reference count is the number of switches using - this policy. - Return None otherwise - """ - return self._get("ref_count") - - @property - def rpm_images(self): - """ - Return the rpmimages of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("rpmimages") - - @property - def image_name(self): - """ - Return the imageName of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("imageName") - - @property - def agnostic(self): - """ - Return the value of agnostic for the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("agnostic") - - -class NdfcSwitchIssuDetails(NdfcAnsibleImageUpgradeCommon): - """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes. - - Usage: See subclasses. - - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu - - Response body: - { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO211218GC", - "deviceName": "cvd-1312-leaf", - "fabric": "fff", - "version": "10.3(2)", - "policy": "NR3F", - "status": "In-Sync", - "reason": "Compliance", - "imageStaged": "Success", - "validated": "None", - "upgrade": "None", - "upgGroups": "None", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": null, - "vpcPeer": null, - "role": "leaf", - "lastUpgAction": "Never", - "model": "N9K-C93180YC-EX", - "ipAddress": "172.22.150.103", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 0, - "upgradePercent": 0, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 8430, - "platform": "N9K", - "vpc_role": null, - "ip_address": "172.22.150.103", - "peer": null, - "vdc_id": -1, - "sys_name": "cvd-1312-leaf", - "id": 3, - "group": "fff", - "fcoEEnabled": false, - "mds": false - }, - {etc...} - ] - - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_data"] = None - # action_keys is used in subclasses to determine if any actions - # are in progress. - # Property actions_in_progress returns True if so, False otherwise - self.properties["action_keys"] = set() - self.properties["action_keys"].add("imageStaged") - self.properties["action_keys"].add("upgrade") - self.properties["action_keys"].add("validated") - - - def refresh(self) -> None: - """ - Refresh current issu details from NDFC - """ - path = self.endpoints.issu_info.get("path") - verb = self.endpoints.issu_info.get("verb") - - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") - - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") - - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"result: {self.ndfc_result}" - # ndfc_result for 404 response - # {'found': False, 'success': True} - if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: - msg = f"{self.class_name}.refresh: " - msg += "Bad result when retriving switch " - msg += "information from NDFC" - self.module.fail_json(msg) - - data = self.ndfc_response.get("DATA").get("lastOperDataObject") - if data is None: - msg = f"{self.class_name}.refresh: " - msg += "NDFC has no switch ISSU information." - self.module.fail_json(msg) - if len(data) == 0: - msg = f"{self.class_name}.refresh: " - msg += "NDFC has no switch ISSU information." - self.module.fail_json(msg) - - self.properties["ndfc_data"] = self.ndfc_response.get("DATA", {}).get( - "lastOperDataObject", [] - ) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") - - @property - def actions_in_progress(self): - """ - Return True if any actions are in progress - Return False otherwise - """ - for action_key in self.properties["action_keys"]: - if self._get(action_key) == "In-Progress": - return True - return False - - def _get(self, item): - """ - overridden in subclasses - """ - pass - - @property - def ndfc_data(self): - """ - Return the raw data retrieved from NDFC - """ - return self.properties["ndfc_data"] - - @property - def ndfc_response(self): - """ - Return the raw response from the GET request. - Return None otherwise - """ - return self.properties["ndfc_response"] - - @property - def ndfc_result(self): - """ - Return the raw result of the GET request. - Return None otherwise - """ - return self.properties["ndfc_result"] - - @property - def device_name(self): - """ - Return the deviceName of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - device name, e.g. "cvd-1312-leaf" - None - """ - return self._get("deviceName") - - @property - def eth_switch_id(self): - """ - Return the ethswitchid of the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - integer - None - """ - return self._get("ethswitchid") - - @property - def fabric(self): - """ - Return the fabric of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - fabric name, e.g. "myfabric" - None - """ - return self._get("fabric") - - @property - def fcoe_enabled(self): - """ - Return whether FCOE is enabled on the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - boolean (true/false) - None - """ - return self.make_boolean(self._get("fcoEEnabled")) - - @property - def group(self): - """ - Return the group of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - group name, e.g. "mygroup" - None - """ - return self._get("group") - - @property - # id is a python keyword, so we can't use it as a property name - # so we use switch_id instead - def switch_id(self): - """ - Return the switch ID of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer - None - """ - return self._get("id") - - @property - def image_staged(self): - """ - Return the imageStaged of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Success - Failed - None - """ - return self._get("imageStaged") - - @property - def image_staged_percent(self): - """ - Return the imageStagedPercent of the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - Integer in range 0-100 - None - """ - return self._get("imageStagedPercent") - - @property - def ip_address(self): - """ - Return the ipAddress of the switch, if it exists. - Return None otherwise - - Possible values: - switch IP address - None - """ - return self._get("ipAddress") - - @property - def issu_allowed(self): - """ - Return the issuAllowed value of the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - ?? TODO:3 check this - "" - None - """ - return self._get("issuAllowed") - - @property - def last_upg_action(self): - """ - Return the last upgrade action performed on the switch - with ip_address, if it exists. - Return None otherwise - - Possible values: - ?? TODO:3 check this - Never - None - """ - return self._get("lastUpgAction") - - @property - def mds(self): - """ - Return whether the switch with ip_address is an MSD, if it exists. - Return None otherwise - - Possible values: - Boolean (True or False) - None - """ - return self.make_boolean(self._get("mds")) - - @property - def mode(self): - """ - Return the ISSU mode of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - "Normal" - None - """ - return self._get("mode") - - @property - def model(self): - """ - Return the model of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - model number e.g. "N9K-C93180YC-EX" - None - """ - return self._get("model") - - @property - def model_type(self): - """ - Return the model type of the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - Integer - None - """ - return self._get("modelType") - - @property - def peer(self): - """ - Return the peer of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - ?? TODO:3 check this - None - """ - return self._get("peer") - - @property - def platform(self): - """ - Return the platform of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - platform, e.g. "N9K" - None - """ - return self._get("platform") - - @property - def policy(self): - """ - Return the policy attached to the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - policy name, e.g. "NR3F" - None - """ - return self._get("policy") - - @property - def reason(self): - """ - Return the reason (?) of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Compliance - Validate - Upgrade - None - """ - return self._get("reason") - - @property - def role(self): - """ - Return the role of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - switch role, e.g. "leaf" - None - """ - return self._get("role") - - @property - def serial_number(self): - """ - Return the serialNumber of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - switch serial number, e.g. "AB1234567CD" - None - """ - return self._get("serialNumber") - - @property - def status(self): - """ - Return the sync status of the switch with ip_address, if it exists. - Return None otherwise - - Details: The sync status is the status of the switch with respect - to the image policy. If the switch is in sync with the image policy, - the status is "In-Sync". If the switch is out of sync with the image - policy, the status is "Out-Of-Sync". - - Possible values: - "In-Sync" - "Out-Of-Sync" - None - """ - return self._get("status") - - @property - def status_percent(self): - """ - Return the upgrade (TODO:3 verify this) percentage completion - of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer in range 0-100 - None - """ - return self._get("statusPercent") - - @property - def sys_name(self): - """ - Return the system name of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - system name, e.g. "cvd-1312-leaf" - None - """ - return self._get("sys_name") - - @property - def system_mode(self): - """ - Return the system mode of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - "Maintenance" (TODO:3 verify this) - "Normal" - None - """ - return self._get("systemMode") - - @property - def upgrade(self): - """ - Return the upgrade status of the switch with ip_address, - if it exists. - Return None otherwise - - Possible values: - Success - In-Progress - None - """ - return self._get("upgrade") - - @property - def upg_groups(self): - """ - Return the upgGroups (upgrade groups) of the switch with ip_address, - if it exists. - Return None otherwise - - Possible values: - upgrade group to which the switch belongs e.g. "LEAFS" - None - """ - return self._get("upgGroups") - - @property - def upgrade_percent(self): - """ - Return the upgrade percent complete of the switch - with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer in range 0-100 - None - """ - return self._get("upgradePercent") - - @property - def validated(self): - """ - Return the validation status of the switch with ip_address, - if it exists. - Return None otherwise - - Possible values: - Failed - Success - None - """ - return self._get("validated") - - @property - def validated_percent(self): - """ - Return the validation percent complete of the switch - with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer in range 0-100 - None - """ - return self._get("validatedPercent") - - @property - def vdc_id(self): - """ - Return the vdcId of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer - None - """ - return self._get("vdcId") - - @property - def vdc_id2(self): - """ - Return the vdc_id of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer (negative values are valid) - None - """ - return self._get("vdc_id") - - @property - def version(self): - """ - Return the version of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - version, e.g. "10.3(2)" - None - """ - return self._get("version") - - @property - def vpc_peer(self): - """ - Return the vpcPeer of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - vpc peer e.g.: 10.1.1.1 - None - """ - return self._get("vpcPeer") - - @property - def vpc_role(self): - """ - Return the vpcRole of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - vpc role e.g.: - "primary" - "secondary" - "none" -> This will be translated to None - "none established" (TODO:3 verify this) - "primary, operational secondary" (TODO:3 verify this) - None - """ - return self._get("vpcRole") - - -class NdfcSwitchIssuDetailsByIpAddress(NdfcSwitchIssuDetails): - """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes retrieved by ip address. - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcSwitchIssuDetailsByIpAddress(module) - instance.refresh() - instance.ip_address = 10.1.1.1 - image_staged = instance.image_staged - image_upgraded = instance.image_upgraded - serial_number = instance.serial_number - etc... - - See NdfcSwitchIssuDetails for more details. - """ - - def __init__(self, module): - super().__init__(module) - self._init_properties() - - def _init_properties(self): - super()._init_properties() - self.properties["ip_address"] = None - - def refresh(self): - """ - Caller: __init__() - - Refresh ip_address current issu details from NDFC - """ - super().refresh() - self.data_subclass = {} - for switch in self.ndfc_data: - msg = f"{self.class_name}.refresh: " - msg += f"switch {switch}" - self.data_subclass[switch["ipAddress"]] = switch - - def _get(self, item): - if self.ip_address is None: - msg = f"{self.class_name}: set instance.ip_address " - msg += f"before accessing property {item}." - self.module.fail_json(msg) - if self.data_subclass.get(self.ip_address) is None: - msg = f"{self.class_name}: {self.ip_address} is not " - msg += f"defined in NDFC." - self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.ip_address].get(item)) - - @property - def filtered_data(self): - """ - Return a dictionary of the switch matching self.ip_address. - Return None of the switch does not exist in NDFC. - """ - return self.data_subclass.get(self.ip_address) - - @property - def ip_address(self): - """ - Set the ip_address of the switch to query. - - This needs to be set before accessing this class's properties. - """ - return self.properties.get("ip_address") - - @ip_address.setter - def ip_address(self, value): - self.properties["ip_address"] = value - - -class NdfcSwitchIssuDetailsBySerialNumber(NdfcSwitchIssuDetails): - """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes retrieved by serial_number. - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcSwitchIssuDetailsBySerialNumber(module) - instance.refresh() - instance.serial_number = "FDO211218GC" - instance.refresh() - image_staged = instance.image_staged - image_upgraded = instance.image_upgraded - ip_address = instance.ip_address - etc... - - See NdfcSwitchIssuDetails for more details. - - """ - - def __init__(self, module): - super().__init__(module) - self._init_properties() - - def _init_properties(self): - super()._init_properties() - self.properties["serial_number"] = None - - def refresh(self): - """ - Caller: __init__() - - Refresh serial_number current issu details from NDFC - """ - super().refresh() - self.data_subclass = {} - for switch in self.ndfc_data: - self.data_subclass[switch["serialNumber"]] = switch - - def _get(self, item): - if self.serial_number is None: - msg = f"{self.class_name}: set instance.serial_number " - msg += f"before accessing property {item}." - self.module.fail_json(msg) - if self.data_subclass.get(self.serial_number) is None: - msg = f"{self.class_name}: {self.serial_number} is not " - msg += f"defined in NDFC." - self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.serial_number].get(item)) - - @property - def filtered_data(self): - """ - Return a dictionary of the switch matching self.serial_number. - Return None of the switch does not exist in NDFC. - """ - return self.data_subclass.get(self.serial_number) - - @property - def serial_number(self): - """ - Set the serial_number of the switch to query. - - This needs to be set before accessing this class's properties. - """ - return self.properties.get("serial_number") - - @serial_number.setter - def serial_number(self, value): - self.properties["serial_number"] = value - - -class NdfcSwitchIssuDetailsByDeviceName(NdfcSwitchIssuDetails): - """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes retrieved by device_name. - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcSwitchIssuDetailsByDeviceName(module) - instance.refresh() - instance.device_name = "leaf_1" - image_staged = instance.image_staged - image_upgraded = instance.image_upgraded - ip_address = instance.ip_address - etc... - - See NdfcSwitchIssuDetails for more details. - - """ - - def __init__(self, module): - super().__init__(module) - self._init_properties() - - def _init_properties(self): - super()._init_properties() - self.properties["device_name"] = None - - def refresh(self): - """ - Caller: __init__() - - Refresh device_name current issu details from NDFC - """ - super().refresh() - self.data_subclass = {} - for switch in self.ndfc_data: - self.data_subclass[switch["deviceName"]] = switch - - def _get(self, item): - if self.device_name is None: - msg = f"{self.class_name}: set instance.device_name " - msg += f"before accessing property {item}." - self.module.fail_json(msg) - if self.data_subclass.get(self.device_name) is None: - msg = f"{self.class_name}: {self.device_name} is not " - msg += f"defined in NDFC." - self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.device_name].get(item)) - - @property - def filtered_data(self): - """ - Return a dictionary of the switch matching self.device_name. - Return None of the switch does not exist in NDFC. - """ - return self.data_subclass.get(self.device_name) - - @property - def device_name(self): - """ - Set the device_name of the switch to query. - - This needs to be set before accessing this class's properties. - """ - return self.properties.get("device_name") - - @device_name.setter - def device_name(self, value): - self.properties["device_name"] = value - - -class NdfcImageStage(NdfcAnsibleImageUpgradeCommon): - """ - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image - - Verb: POST - - Usage (where module is an instance of AnsibleModule): - - stage = NdfcImageStage(module) - stage.serial_numbers = ["FDO211218HH", "FDO211218GC"] - stage.commit() - data = stage.data - - Request body (12.1.2e) (yes, serialNum is misspelled): - { - "sereialNum": [ - "FDO211218HH", - "FDO211218GC" - ] - } - Request body (12.1.3b): - { - "serialNumbers": [ - "FDO211218HH", - "FDO211218GC" - ] - } - - Response: - Unfortunately, the response does not contain consistent data. - Would be better if all responses contained serial numbers as keys so that - we could verify against a set() of serial numbers. Sigh. It is what it is. - { - 'RETURN_CODE': 200, - 'METHOD': 'POST', - 'REQUEST_PATH': 'https: //172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image', - 'MESSAGE': 'OK', - 'DATA': [ - { - 'key': 'success', - 'value': '' - }, - { - 'key': 'success', - 'value': '' - } - ] - } - - Response when there are no files to stage: - [ - { - "key": "FDO211218GC", - "value": "No files to stage" - }, - { - "key": "FDO211218HH", - "value": "No files to stage" - } - ] - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - self.serial_numbers_done = set() - self.ndfc_version = None - self.path = None - self.verb = None - self.payload = None - self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) - - def _init_properties(self): - self.properties = {} - self.properties["serial_numbers"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None - self.properties["check_interval"] = 10 # seconds - self.properties["check_timeout"] = 1800 # seconds - - def _populate_ndfc_version(self): - """ - Populate self.ndfc_version with the NDFC version. - - Notes: - 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular - imports resulting in RecursionError - """ - instance = NdfcVersion(self.module) - instance.refresh() - self.ndfc_version = instance.version - - def prune_serial_numbers(self): - """ - If the image is already staged on a switch, remove that switch's - serial number from the list of serial numbers to stage. - """ - serial_numbers = copy.copy(self.serial_numbers) - for serial_number in serial_numbers: - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - if self.issu_detail.image_staged == "Success": - msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " - msg += "image already staged for " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}." - self.log_msg(msg) - self.serial_numbers.remove(serial_number) - - def validate_serial_numbers(self): - """ - Fail if the image_staged state for any serial_number - is Failed. - """ - for serial_number in self.serial_numbers: - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - if self.issu_detail.image_staged == "Failed": - msg = "Image staging is failing for the following switch: " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}. " - msg += f"Check the switch connectivity to NDFC " - msg += "and try again." - self.module.fail_json(msg) - else: - self.log_msg(f"Image staging is not failing for the following switch: {self.issu_detail.serial_number}") - - def commit(self): - """ - Commit the image staging request to NDFC and wait - for the images to be staged. - """ - if self.serial_numbers is None: - msg = f"{self.class_name}.commit() call instance.serial_numbers " - msg += "before calling commit()." - self.module.fail_json(msg) - if len(self.serial_numbers) == 0: - msg = f"REMOVE: {self.class_name}.commit() no serial numbers to stage." - self.log_msg(msg) - return - self.prune_serial_numbers() - self.validate_serial_numbers() - self._wait_for_current_actions_to_complete() - - self.path = self.endpoints.image_stage.get("path") - self.verb = self.endpoints.image_stage.get("verb") - - self.log_msg(f"REMOVE: {self.class_name}.commit() path: {self.path}") - self.log_msg(f"REMOVE: {self.class_name}.commit() verb: {self.verb}") - self.payload = {} - self._populate_ndfc_version() - if self.ndfc_version == "12.1.2e": - # Yes, NDFC 12.1.2e wants serialNum to be misspelled - self.payload["sereialNum"] = self.serial_numbers - else: - self.payload["serialNumbers"] = self.serial_numbers - self.properties["ndfc_response"] = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(self.payload) - ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, self.verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" - ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" - self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - self._wait_for_image_stage_to_complete() - - def _wait_for_current_actions_to_complete(self): - """ - NDFC will not stage an image if there are any actions in progress. - Wait for all actions to complete before staging image. - Actions include image staging, image upgrade, and image validation. - """ - self.serial_numbers_done = set() - serial_numbers_todo = set(copy.copy(self.serial_numbers)) - timeout = self.check_timeout - while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - for serial_number in self.serial_numbers: - if serial_number in self.serial_numbers_done: - continue - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} actions in progress: " - msg += f"{self.issu_detail.actions_in_progress}, " - msg += f"{timeout} seconds remaining." - self.log_msg(msg) - if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) - self.serial_numbers_done.add(serial_number) - if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"Timed out waiting for actions to complete. " - msg += f"serial_numbers_done: " - msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " - msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) - self.module.fail_json(msg) - - def _wait_for_image_stage_to_complete(self): - """ - # Wait for image stage to complete - """ - # We're promiting serial_numbers_done to a class-level attribute - # so that it can be used in unit test asserts. - self.serial_numbers_done = set() - timeout = self.check_timeout - serial_numbers_todo = set(copy.copy(self.serial_numbers)) - while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" - self.log_msg(msg) - for serial_number in self.serial_numbers: - if serial_number in self.serial_numbers_done: - continue - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - ip_address = self.issu_detail.ip_address - device_name = self.issu_detail.device_name - staged_percent = self.issu_detail.image_staged_percent - staged_status = self.issu_detail.image_staged - - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Seconds remaining {timeout}: " - msg += f"{device_name}, {serial_number}, {ip_address} " - msg += f"image staged percent: {staged_percent}" - self.log_msg(msg) - - if staged_status == "Failed": - msg = f"Seconds remaining {timeout}: stage image failed " - msg += f"for {device_name}, {serial_number}, {ip_address}. " - msg += f"image staged percent: {staged_percent}" - self.module.fail_json(msg) - - if staged_status == "Success": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Seconds remaining {timeout}: stage image complete for " - msg += f"for {device_name}, {serial_number}, {ip_address}. " - msg += f"image staged percent: {staged_percent}" - self.log_msg(msg) - self.serial_numbers_done.add(serial_number) - - if staged_status == None: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Seconds remaining {timeout}: stage image " - msg += "not started for " - msg += f"{device_name}, {serial_number}, {ip_address}. " - msg += f"image staged percent: {staged_percent}" - self.log_msg(msg) - - if staged_status == "In Progress": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Seconds remaining {timeout}: stage image " - msg += f"{staged_status} for " - msg += f"{device_name}, {serial_number}, {ip_address}. " - msg += f"image staged percent: {staged_percent}" - self.log_msg(msg) - if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Timed out waiting for image stage to complete. " - msg += f"serial_numbers_done: " - msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " - msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) - self.module.fail_json(msg) - - @property - def serial_numbers(self): - """ - Set the serial numbers of the switches to stage. - - This must be set before calling instance.commit() - """ - return self.properties.get("serial_numbers") - - @serial_numbers.setter - def serial_numbers(self, value): - if not isinstance(value, list): - msg = f"{self.__class__.__name__}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." - self.module.fail_json(msg) - self.properties["serial_numbers"] = value - - @property - def ndfc_data(self): - """ - Return the result of the image staging request - for serial_numbers. - - instance.serial_numbers must be set first. - """ - return self.properties.get("ndfc_data") - - @property - def ndfc_result(self): - """ - Return the POST result from NDFC - """ - return self.properties.get("ndfc_result") - - @property - def ndfc_response(self): - """ - Return the POST response from NDFC - """ - return self.properties.get("ndfc_response") - - @property - def check_interval(self): - """ - Return the stage check interval in seconds - """ - return self.properties.get("check_interval") - - @check_interval.setter - def check_interval(self, value): - if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_interval must " - msg += f"be an integer." - self.module.fail_json(msg) - self.properties["check_interval"] = value - - @property - def check_timeout(self): - """ - Return the stage check timeout in seconds - """ - return self.properties.get("check_timeout") - - @check_timeout.setter - def check_timeout(self, value): - if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_timeout must " - msg += f"be an integer." - self.module.fail_json(msg) - self.properties["check_timeout"] = value - -# ============================================================================== -class NdfcImageValidate(NdfcAnsibleImageUpgradeCommon): - """ - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image - - Verb: POST - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcImageValidate(module) - instance.serial_numbers = ["FDO211218HH", "FDO211218GC"] - # non_disruptive is optional - instance.non_disruptive = True - instance.commit() - data = instance.ndfc_data - - Request body: - { - "serialNum": ["FDO21120U5D"], - "nonDisruptive":"true" - } - - Response body when nonDisruptive is True: - [StageResponse [key=success, value=]] - - Response body when nonDisruptive is False: - [StageResponse [key=success, value=]] - - The response is not JSON, nor is it very useful. - Instead, we poll for validation status using - NdfcSwitchIssuDetailsBySerialNumber. - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) - - def _init_properties(self): - self.properties = {} - self.properties["check_interval"] = 10 # seconds - self.properties["check_timeout"] = 1800 # seconds - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None - self.properties["non_disruptive"] = False - self.properties["serial_numbers"] = None - - def _populate_ndfc_version(self): - """ - Populate self.ndfc_version with the NDFC version. - - TODO:3 Remove if 12.1.3b works with no changes to request/response payloads. - - Notes: - 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular - imports resulting in RecursionError - """ - instance = NdfcVersion(self.module) - instance.refresh() - self.ndfc_version = instance.version - - def prune_serial_numbers(self): - """ - If the image is already validated on a switch, remove that switch's - serial number from the list of serial numbers to validate. - """ - serial_numbers = copy.copy(self.serial_numbers) - for serial_number in serial_numbers: - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - if self.issu_detail.validated == "Success": - msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " - msg += "image already validated for " - msg += f"{self.issu_detail.serial_number}, " - msg += f"{self.issu_detail.ip_address}" - self.log_msg(msg) - self.serial_numbers.remove(self.issu_detail.serial_number) - - def validate_serial_numbers(self): - """ - Log a warning if the validated state for any serial_number - is Failed. - - TODO:1 Need a way to compare current image_policy with the image policy in the response - TODO:3 If validate == Failed, it may have been from the last operation. - TODO:3 We can't fail here based on this until we can verify the failure is happening for the current image_policy. - TODO:3 Change this to a log message and update the unit test if we can't verify the failure is happening for the current image_policy. - """ - for serial_number in self.serial_numbers: - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - if self.issu_detail.validated == "Failed": - msg = f"{self.class_name}.validate_serial_numbers: " - msg += "image validation is failing for the following switch: " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}. " - msg += "If this persists, check the switch connectivity to NDFC and " - msg += "try again." - #self.log_msg(msg) - self.module.fail_json(msg) - - def build_payload(self): - self.payload = {} - self.payload["serialNum"] = self.serial_numbers - self.payload["nonDisruptive"] = self.non_disruptive - - def commit(self): - """ - Commit the image validation request to NDFC and wait - for the images to be validated. - """ - if self.serial_numbers is None: - msg = f"{self.class_name}.commit() call instance.serial_numbers " - msg += "before calling commit()." - self.module.fail_json(msg) - if len(self.serial_numbers) == 0: - msg = f"REMOVE: {self.class_name}.commit() no serial numbers " - msg += "to validate." - self.log_msg(msg) - return - self.prune_serial_numbers() - self.validate_serial_numbers() - self._wait_for_current_actions_to_complete() - path = self.endpoints.image_validate.get("path") - verb = self.endpoints.image_validate.get("verb") - self.build_payload() - self.properties["ndfc_response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) - ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" - ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" - self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - self._wait_for_image_validate_to_complete() - - def _wait_for_current_actions_to_complete(self): - """ - NDFC will not validate an image if there are any actions in progress. - Wait for all actions to complete before validating image. - Actions include image staging, image upgrade, and image validation. - """ - self.serial_numbers_done = set() - serial_numbers_todo = set(copy.copy(self.serial_numbers)) - timeout = self.check_timeout - while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - for serial_number in self.serial_numbers: - if serial_number in self.serial_numbers_done: - continue - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} actions in progress: " - msg += f"{self.issu_detail.actions_in_progress}, " - msg += f"{timeout} seconds remaining." - self.log_msg(msg) - if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) - self.serial_numbers_done.add(serial_number) - if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"Timed out waiting for actions to complete. " - msg += f"serial_numbers_done: " - msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " - msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) - self.module.fail_json(msg) - - def _wait_for_image_validate_to_complete(self): - """ - Wait for image validation to complete - """ - # We're promiting serial_numbers_done to a class-level attribute - # so that it can be used in unit test asserts. - self.serial_numbers_done = set() - timeout = self.check_timeout - serial_numbers_todo = set(copy.copy(self.serial_numbers)) - while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" - self.log_msg(msg) - for serial_number in self.serial_numbers: - if serial_number in self.serial_numbers_done: - continue - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - ip_address = self.issu_detail.ip_address - device_name = self.issu_detail.device_name - validated_percent = self.issu_detail.validated_percent - validated_status = self.issu_detail.validated - - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"validated_percent: {validated_percent} " - msg += f"validated_state: {validated_status}" - self.log_msg(msg) - - if validated_status == "Failed": - msg = f"Seconds remaining {timeout}: validate image " - msg += f"{validated_status} for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}. " - msg += "Check the switch e.g. show install log detail, " - msg += "show incompatibility-all nxos . Or " - msg += "check NDFC Operations > Image Management > " - msg += "Devices > View Details > Validate for " - msg += "more details." - self.module.fail_json(msg) - - if validated_status == "Success": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += f"{validated_status} for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) - self.serial_numbers_done.add(serial_number) - - if validated_status == None: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += "not started for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) - - if validated_status == "In Progress": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += f"{validated_status} for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) - if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Timed out waiting for image validation to complete. " - msg += f"serial_numbers_done: " - msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " - msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) - self.module.fail_json(msg) - - @property - def serial_numbers(self): - """ - Set the serial numbers of the switches to stage. - - This must be set before calling instance.commit() - """ - return self.properties.get("serial_numbers") - - @serial_numbers.setter - def serial_numbers(self, value): - if not isinstance(value, list): - msg = f"{self.__class__.__name__}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." - self.module.fail_json(msg) - self.properties["serial_numbers"] = value - - @property - def non_disruptive(self): - """ - Set the non_disruptive flag to True or False. - """ - return self.properties.get("non_disruptive") - - @non_disruptive.setter - def non_disruptive(self, value): - value = self.make_boolean(value) - if not isinstance(value, bool): - msg = f"{self.class_name}.non_disruptive: " - msg += "instance.non_disruptive must " - msg += f"be a boolean. Got {value}." - self.module.fail_json(msg) - self.properties["non_disruptive"] = value - - @property - def ndfc_data(self): - """ - Return the result of the image staging request - for serial_numbers. - - instance.serial_numbers must be set first. - """ - return self.properties.get("ndfc_data") - - @property - def ndfc_result(self): - """ - Return the POST result from NDFC - """ - return self.properties.get("ndfc_result") - - @property - def ndfc_response(self): - """ - Return the POST response from NDFC - """ - return self.properties.get("ndfc_response") - - @property - def check_interval(self): - """ - Return the validate check interval in seconds - """ - return self.properties.get("check_interval") - - @check_interval.setter - def check_interval(self, value): - if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_interval must " - msg += f"be an integer." - self.module.fail_json(msg) - self.properties["check_interval"] = value - - @property - def check_timeout(self): - """ - Return the validate check timeout in seconds - """ - return self.properties.get("check_timeout") - - @check_timeout.setter - def check_timeout(self, value): - if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_timeout must " - msg += f"be an integer." - self.module.fail_json(msg) - self.properties["check_timeout"] = value - - -# ============================================================================== -class NdfcImageUpgrade(NdfcAnsibleImageUpgradeCommon): - """ - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image - Verb: POST - - TODO:3 Discuss with Mike/Shangxin. NdfcImageUpgrade.epld_upgrade, etc - - Usage (where module is an instance of AnsibleModule): - - upgrade = NdfcImageUpgrade(module) - upgrade.devices = devices - upgrade.commit() - data = upgrade.data - - Where devices is a list of dict. Example structure: - - [ - { - 'policy': 'KR3F', - 'ip_address': '172.22.150.102', - 'policy_changed': False - 'stage': False, - 'validate': True, - 'upgrade': { - 'nxos': True, - 'epld': False - }, - 'options': { - 'nxos': { - 'mode': 'non_disruptive' - 'bios_force': False - }, - 'epld': { - 'module': 'ALL', - 'golden': False - }, - 'reboot': { - 'config_reload': False, - 'write_erase': False - }, - 'package': { - 'install': False, - 'uninstall': False - } - }, - }, - etc... - ] - - Request body: - Yes, the keys below are misspelled in the request body: - pacakgeInstall - pacakgeUnInstall - - { - "devices": [ - { - "serialNumber": "FDO211218HH", - "policyName": "NR1F" - } - ], - "issuUpgrade": true, - "issuUpgradeOptions1": { - "nonDisruptive": true, - "forceNonDisruptive": false, - "disruptive": false - }, - "issuUpgradeOptions2": { - "biosForce": false - }, - "epldUpgrade": false, - "epldOptions": { - "moduleNumber": "ALL", - "golden": false - }, - "reboot": false, - "rebootOptions": { - "configReload": "false", - "writeErase": "false" - }, - "pacakgeInstall": false, - "pacakgeUnInstall": false - } - Response bodies: - Responses are text, not JSON, and are returned immediately. - They do not contain useful information. We need to poll NDFC - to determine when the upgrade is complete. Basically, we ignore - these responses in favor of the poll responses. - - If an action is in progress, text is returned: - "Action in progress for some of selected device(s). Please try again after completing current action." - - If an action is not in progress, text is returned: - "3" - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - # Maximum number of modules/linecards in a switch - self.max_module_number = 9 - - self._init_defaults() - self._init_properties() - self.issu_detail = NdfcSwitchIssuDetailsByIpAddress(self.module) - - def _init_defaults(self): - self.defaults = {} - self.defaults["reboot"] = False - self.defaults["stage"] = True - self.defaults["validate"] = True - self.defaults["upgrade"] = {} - self.defaults["upgrade"]["nxos"] = True - self.defaults["upgrade"]["epld"] = False - self.defaults["options"] = {} - self.defaults["options"]["nxos"] = {} - self.defaults["options"]["nxos"]["mode"] = "disruptive" - self.defaults["options"]["nxos"]["bios_force"] = False - self.defaults["options"]["epld"] = {} - self.defaults["options"]["epld"]["module"] = "ALL" - self.defaults["options"]["epld"]["golden"] = False - self.defaults["options"]["reboot"] = {} - self.defaults["options"]["reboot"]["config_reload"] = False - self.defaults["options"]["reboot"]["write_erase"] = False - self.defaults["options"]["package"] = {} - self.defaults["options"]["package"]["install"] = False - self.defaults["options"]["package"]["uninstall"] = False - - def _init_properties(self): - # self.ip_addresses is used in: - # self._wait_for_current_actions_to_complete() - # self._wait_for_image_upgrade_to_complete() - self.ip_addresses = set() - # TODO:1 Review these properties since we are no longer - # calling this class per-switch given the payload structure - # is not amenable to that. - self.properties = {} - self.properties["bios_force"] = False - self.properties["check_interval"] = 10 # seconds - self.properties["check_timeout"] = 1800 # seconds - self.properties["config_reload"] = False - self.properties["devices"] = None - self.properties["disruptive"] = True - self.properties["epld_golden"] = False - self.properties["epld_module"] = "ALL" - self.properties["epld_upgrade"] = False - self.properties["force_non_disruptive"] = False - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None - self.properties["non_disruptive"] = False - self.properties["package_install"] = False - self.properties["package_uninstall"] = False - self.properties["reboot"] = False - self.properties["write_erase"] = False - - self.valid_epld_module = set() - self.valid_epld_module.add("ALL") - for module in range(1, self.max_module_number + 1): - self.valid_epld_module.add(str(module)) - - self.valid_nxos_mode = set() - self.valid_nxos_mode.add("disruptive") - self.valid_nxos_mode.add("non_disruptive") - self.valid_nxos_mode.add("force_non_disruptive") - - - # def prune_devices(self): - # """ - # If the image is already upgraded on a device, remove that device - # from self.devices. self.devices dict has already been validated, - # so no further error checking is needed here. - - # TODO:1 This prunes devices only based on the image upgrade state. - # TODO:1 It does not check other image states and EPLD states. - # """ - # # issu = NdfcSwitchIssuDetailsBySerialNumber(self.module) - # pruned_devices = set() - # instance = NdfcSwitchIssuDetailsByIpAddress(self.module) - # instance.refresh() - # for device in self.devices: - # msg = f"REMOVE: {self.class_name}.prune_devices() device: {device}" - # self.log_msg(msg) - # instance.ip_address = device.get("ip_address") - # instance.refresh() - # if instance.upgrade == "Success": - # msg = f"REMOVE: {self.class_name}.prune_devices: " - # msg = "image already upgraded for " - # msg += f"{instance.device_name}, " - # msg += f"{instance.serial_number}, " - # msg += f"{instance.ip_address}" - # self.log_msg(msg) - # pruned_devices.add(instance.ip_address) - # self.devices = [ - # device - # for device in self.devices - # if device.get("ip_address") not in pruned_devices - # ] - - def validate_devices(self): - """ - Fail if the upgrade state for any device is Failed. - """ - for device in self.devices: - self.issu_detail.ip_address = device.get("ip_address") - self.issu_detail.refresh() - if self.issu_detail.upgrade == "Failed": - msg = f"{self.class_name}.validate_devices: Image upgrade is " - msg += "failing for the following switch: " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}. " - msg += "Please check the switch " - msg += "to determine the cause and try again." - self.module.fail_json(msg) - # used in self._wait_for_current_actions_to_complete() - self.ip_addresses.add(self.issu_detail.ip_address) - - def _merge_defaults_to_switch_config(self, config): - if config.get("stage") is None: - config["stage"] = self.defaults["stage"] - if config.get("reboot") is None: - config["reboot"] = self.defaults["reboot"] - if config.get("validate") is None: - config["validate"] = self.defaults["validate"] - if config.get("upgrade") is None: - config["upgrade"] = self.defaults["upgrade"] - if config.get("upgrade").get("nxos") is None: - config["upgrade"]["nxos"] = self.defaults["upgrade"]["nxos"] - if config.get("upgrade").get("epld") is None: - config["upgrade"]["epld"] = self.defaults["upgrade"]["epld"] - if config.get("options") is None: - config["options"] = self.defaults["options"] - if config["options"].get("nxos") is None: - config["options"]["nxos"] = self.defaults["options"]["nxos"] - if config["options"]["nxos"].get("mode") is None: - config["options"]["nxos"]["mode"] = self.defaults["options"]["nxos"]["mode"] - if config["options"]["nxos"].get("bios_force") is None: - config["options"]["nxos"]["bios_force"] = self.defaults["options"]["nxos"]["bios_force"] - if config["options"].get("epld") is None: - config["options"]["epld"] = self.defaults["options"]["epld"] - if config["options"]["epld"].get("module") is None: - config["options"]["epld"]["module"] = self.defaults["options"]["epld"]["module"] - if config["options"]["epld"].get("golden") is None: - config["options"]["epld"]["golden"] = self.defaults["options"]["epld"]["golden"] - if config["options"].get("reboot") is None: - config["options"]["reboot"] = self.defaults["options"]["reboot"] - if config["options"]["reboot"].get("config_reload") is None: - config["options"]["reboot"]["config_reload"] = self.defaults["options"]["reboot"]["config_reload"] - if config["options"]["reboot"].get("write_erase") is None: - config["options"]["reboot"]["write_erase"] = self.defaults["options"]["reboot"]["write_erase"] - if config["options"].get("package") is None: - config["options"]["package"] = self.defaults["options"]["package"] - if config["options"]["package"].get("install") is None: - config["options"]["package"]["install"] = self.defaults["options"]["package"]["install"] - if config["options"]["package"].get("uninstall") is None: - config["options"]["package"]["uninstall"] = self.defaults["options"]["package"]["uninstall"] - return config - - def build_payload(self, device): - """ - Build the request payload to upgrade the switches. - """ - msg = f"REMOVE: {self.class_name}.build_payload() PRE_DEFAULTS: " - msg += f"device: {device}" - self.log_msg(msg) - device = self._merge_defaults_to_switch_config(device) - msg = f"REMOVE: {self.class_name}.build_payload() POST_DEFAULTS: " - msg += f"device: {device}" - self.log_msg(msg) - - - # devices_to_upgrade must currently be a single device - devices_to_upgrade = [] - self.issu_detail.ip_address = device.get("ip_address") - self.issu_detail.refresh() - payload_device = {} - payload_device["serialNumber"] = self.issu_detail.serial_number - payload_device["policyName"] = device.get("policy") - devices_to_upgrade.append(payload_device) - - self.payload = {} - self.payload["devices"] = devices_to_upgrade - self.payload["issuUpgrade"] = device.get("upgrade").get("nxos") - - # nxos_mode: The choices for nxos_mode are mutually-exclusive. - # If one is set to True, the others must be False. - # nonDisruptive corresponds to NDFC Allow Non-Disruptive GUI option - self.payload["issuUpgradeOptions1"] = {} - self.payload["issuUpgradeOptions1"]["nonDisruptive"] = False - self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = False - self.payload["issuUpgradeOptions1"]["disruptive"] = False - - nxos_mode = device.get("options").get("nxos").get("mode") - if nxos_mode not in self.valid_nxos_mode: - msg = f"{self.class_name}.build_payload() options.nxos.mode must " - msg += f"be one of {self.valid_nxos_mode}. Got {nxos_mode}." - self.module.fail_json(msg) - if nxos_mode == "non_disruptive": - self.payload["issuUpgradeOptions1"]["nonDisruptive"] = True - if nxos_mode == "disruptive": - self.payload["issuUpgradeOptions1"]["disruptive"] = True - if nxos_mode == "force_non_disruptive": - self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True - - # biosForce corresponds to NDFC BIOS Force GUI option - bios_force = device.get("options").get("nxos").get("bios_force") - if not isinstance(bios_force, bool): - msg = f"{self.class_name}.build_payload() options.nxos.bios_force " - msg += f"must be a boolean. Got {bios_force}." - self.module.fail_json(msg) - self.payload["issuUpgradeOptions2"] = {} - self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force - - # EPLD - epld_module = device.get("options").get("epld").get("module") - epld_golden = device.get("options").get("epld").get("golden") - if epld_module not in self.valid_epld_module: - msg = f"{self.class_name}.build_payload() options.epld.module must " - msg += f"be one of {self.valid_epld_module}. Got {epld_module}." - self.module.fail_json(msg) - if not isinstance(epld_golden, bool): - msg = f"{self.class_name}.build_payload() options.epld.golden " - msg += f"must be a boolean. Got {epld_golden}." - self.module.fail_json(msg) - self.payload["epldUpgrade"] = device.get("upgrade").get("epld") - self.payload["epldOptions"] = {} - self.payload["epldOptions"]["moduleNumber"] = epld_module - self.payload["epldOptions"]["golden"] = epld_golden - - # Reboot - reboot = device.get("reboot") - if not isinstance(reboot, bool): - msg = f"{self.class_name}.build_payload() reboot must " - msg += f"be a boolean. Got {reboot}." - self.module.fail_json(msg) - self.payload["reboot"] = reboot - - # Reboot options - config_reload = device.get("options").get("reboot").get("config_reload") - write_erase = device.get("options").get("reboot").get("write_erase") - if not isinstance(config_reload, bool): - msg = f"{self.class_name}.build_payload() options.reboot.config_reload " - msg += f"must be a boolean. Got {config_reload}." - self.module.fail_json(msg) - if not isinstance(write_erase, bool): - msg = f"{self.class_name}.build_payload() options.reboot.write_erase " - msg += f"must be a boolean. Got {write_erase}." - self.module.fail_json(msg) - self.payload["rebootOptions"] = {} - self.payload["rebootOptions"]["configReload"] = config_reload - self.payload["rebootOptions"]["writeErase"] = write_erase - - # Packages - package_install = device.get("options").get("package").get("install") - package_uninstall = device.get("options").get("package").get("uninstall") - if not isinstance(package_install, bool): - msg = f"{self.class_name}.build_payload() options.package.install " - msg += f"must be a boolean. Got {package_install}." - self.module.fail_json(msg) - if not isinstance(package_uninstall, bool): - msg = f"{self.class_name}.build_payload() options.package.uninstall " - msg += f"must be a boolean. Got {package_uninstall}." - self.module.fail_json(msg) - self.payload["pacakgeInstall"] = package_install - self.payload["pacakgeUnInstall"] = package_uninstall - - def commit(self): - """ - Commit the image upgrade request to NDFC and wait - for the images to be upgraded. - """ - if self.devices is None: - msg = f"{self.class_name}.commit() call instance.devices " - msg += "before calling commit()." - self.module.fail_json(msg) - if len(self.devices) == 0: - msg = f"REMOVE: {self.class_name}.commit() no devices to upgrade." - self.log_msg(msg) - return - #self.prune_devices() - self.validate_devices() - self._wait_for_current_actions_to_complete() - path = self.endpoints.image_upgrade.get("path") - verb = self.endpoints.image_upgrade.get("verb") - for device in self.devices: - self.build_payload(device) - self.log_msg(f"REMOVE: {self.class_name}.commit() upgrade payload: {self.payload}") - self.properties["ndfc_response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) - ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" - ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" - self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - self._wait_for_image_upgrade_to_complete() - - def _wait_for_current_actions_to_complete(self): - """ - NDFC will not upgrade an image if there are any actions in progress. - Wait for all actions to complete before upgrading image. - Actions include image staging, image upgrade, and image validation. - """ - ipv4_todo = copy.copy(self.ip_addresses) - timeout = self.check_timeout - while len(ipv4_todo) > 0 and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - for ipv4 in self.ip_addresses: - if ipv4 not in ipv4_todo: - continue - self.issu_detail.ip_address = ipv4 - self.issu_detail.refresh() - if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{ipv4} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) - ipv4_todo.remove(ipv4) - continue - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{ipv4} actions in progress. " - msg += f"Waiting. {timeout} seconds remaining." - self.log_msg(msg) - - def _wait_for_image_upgrade_to_complete(self): - """ - Wait for image upgrade to complete - """ - ipv4_done = set() - timeout = self.check_timeout - ipv4_todo = set(copy.copy(self.ip_addresses)) - while ipv4_done != ipv4_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"seconds remaining {timeout}, " - msg += f"ipv4_todo: {sorted(list(ipv4_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"seconds remaining {timeout}, " - msg += f"ipv4_done: {sorted(list(ipv4_done))}" - self.log_msg(msg) - for ipv4 in self.ip_addresses: - if ipv4 in ipv4_done: - continue - self.issu_detail.ip_address = ipv4 - self.issu_detail.refresh() - ip_address = self.issu_detail.ip_address - device_name = self.issu_detail.device_name - upgrade_percent = self.issu_detail.upgrade_percent - upgrade_status = self.issu_detail.upgrade - serial_number = self.issu_detail.serial_number - - if upgrade_status == "Failed": - msg = f"{self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"Seconds remaining {timeout}: upgrade image " - msg += f"{upgrade_status} for " - msg += f"{device_name}, {serial_number}, {ip_address}" - self.module.fail_json(msg) - - if upgrade_status == "Success": - ipv4_done.add(ipv4) - status = "succeeded" - if upgrade_status == None: - status = "not started" - if upgrade_status == "In-Progress": - status = "in progress" - - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"Seconds remaining {timeout}, " - msg += f"Percent complete {upgrade_percent}, " - msg += f"Status {status}, " - msg += f"{device_name}, {serial_number}, {ip_address}" - self.log_msg(msg) - - if ipv4_done != ipv4_todo: - msg = f"{self.class_name}._wait_for_image_upgrade_to_complete(): " - msg += "The following device(s) did not complete upgrade: " - msg += f"{ipv4_todo.difference(ipv4_done)}. " - msg += "Try increasing issu timeout in the playbook, or check " - msg += "the device(s) to determine the cause " - msg += "(e.g. show install all status)." - self.module.fail_json(msg) - - # setter properties - @property - def bios_force(self): - """ - Set the bios_force flag to True or False. - - Default: False - """ - return self.properties.get("bios_force") - - @bios_force.setter - def bios_force(self, value): - name = "bios_force" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def config_reload(self): - """ - Set the config_reload flag to True or False. - - Default: False - """ - return self.properties.get("config_reload") - - @config_reload.setter - def config_reload(self, value): - name = "config_reload" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def devices(self): - """ - Set the devices to upgrade. - - list() of dict() with the following structure: - { - "serial_number": "FDO211218HH", - "policy_name": "NR1F" - } - - Must be set before calling instance.commit() - """ - return self.properties.get("devices") - - @devices.setter - def devices(self, value): - name = "devices" - if not isinstance(value, list): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a python list of dict." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def disruptive(self): - """ - Set the disruptive flag to True or False. - - Default: False - """ - return self.properties.get("disruptive") - - @disruptive.setter - def disruptive(self, value): - name = "disruptive" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def epld_golden(self): - """ - Set the epld_golden flag to True or False. - - Default: False - """ - return self.properties.get("epld_golden") - - @epld_golden.setter - def epld_golden(self, value): - name = "epld_golden" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def epld_upgrade(self): - """ - Set the epld_upgrade flag to True or False. - - Default: False - """ - return self.properties.get("epld_upgrade") - - @epld_upgrade.setter - def epld_upgrade(self, value): - name = "epld_upgrade" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def epld_module(self): - """ - Set the epld_module to upgrade. - - Ignored if epld_upgrade is set to False - Valid values: integer or "ALL" - Default: "ALL" - """ - return self.properties.get("epld_module") - - @epld_module.setter - def epld_module(self, value): - name = "epld_module" - try: - value = value.upper() - except AttributeError: - pass - if not isinstance(value, int) and value != "ALL": - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be an integer or 'ALL'" - self.module.fail_json(msg) - self.properties[name] = value - - @property - def force_non_disruptive(self): - """ - Set the force_non_disruptive flag to True or False. - - Default: False - """ - return self.properties.get("force_non_disruptive") - - @force_non_disruptive.setter - def force_non_disruptive(self, value): - name = "force_non_disruptive" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def non_disruptive(self): - """ - Set the non_disruptive flag to True or False. - - Default: True - """ - return self.properties.get("non_disruptive") - - @non_disruptive.setter - def non_disruptive(self, value): - name = "non_disruptive" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def package_install(self): - """ - Set the package_install flag to True or False. - - Default: False - """ - return self.properties.get("package_install") - - @package_install.setter - def package_install(self, value): - name = "package_install" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def package_uninstall(self): - """ - Set the package_uninstall flag to True or False. - - Default: False - """ - return self.properties.get("package_uninstall") - - @package_uninstall.setter - def package_uninstall(self, value): - name = "package_uninstall" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def reboot(self): - """ - Set the reboot flag to True or False. - - Default: False - """ - return self.properties.get("reboot") - - @reboot.setter - def reboot(self, value): - name = "reboot" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def write_erase(self): - """ - Set the write_erase flag to True or False. - - Default: False - """ - return self.properties.get("write_erase") - - @write_erase.setter - def write_erase(self, value): - name = "write_erase" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - - # getter properties - @property - def check_interval(self): - """ - Return the image upgrade check interval in seconds - """ - return self.properties.get("check_interval") - - @property - def check_timeout(self): - """ - Return the image upgrade check timeout in seconds - """ - return self.properties.get("check_timeout") - - @property - def ndfc_data(self): - """ - Return the data retrieved from NDFC for the image upgrade request. - - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("ndfc_data") - - @property - def ndfc_result(self): - """ - Return the POST result from NDFC - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("ndfc_result") - - @property - def ndfc_response(self): - """ - Return the POST response from NDFC - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("ndfc_response") - - @property - def serial_numbers(self): - """ - Return a list of serial numbers from self.devices - """ - return [device.get("serial_number") for device in self.devices] - - -class NdfcVersion(NdfcAnsibleImageUpgradeCommon): - """ - Return image version information from NDFC - - NOTES: - 1. considered using dcnm_version_supported() but it does not return - minor release info, which is needed due to key changes between - 12.1.2e and 12.1.3b. For example, see NdfcImageStage().commit() - - Endpoint: - /appcenter/cisco/ndfc/api/v1/fm/about/version - - Usage (where module is an instance of AnsibleModule): - - instance = NdfcVersion(module) - instance.refresh() - if instance.version == "12.1.2e": - do 12.1.2e stuff - else: - do other stuff - - Response: - { - "version": "12.1.2e", - "mode": "LAN", - "isMediaController": false, - "dev": false, - "isHaEnabled": false, - "install": "EASYFABRIC", - "uuid": "f49e6088-ad4f-4406-bef6-2419de914ff1", - "is_upgrade_inprogress": false - } - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None - - def refresh(self): - """ - Refresh self.ndfc_data with current version info from NDFC - """ - path = self.endpoints.ndfc_version.get("path") - verb = self.endpoints.ndfc_version.get("verb") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - - msg = f"REMOVE: {self.class_name}.refresh() ndfc_response: {self.ndfc_response}" - self.log_msg(msg) - - msg = f"REMOVE: {self.class_name}.refresh() ndfc_result: {self.ndfc_result}" - self.log_msg(msg) - - if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: - msg = f"{self.class_name}.refresh() failed: {self.ndfc_result}" - self.module.fail_json(msg) - - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - if self.ndfc_data is None: - msg = f"{self.class_name}.refresh() failed: NDFC response " - msg += "does not contain DATA key. NDFC response: " - msg += f"{self.ndfc_response}" - self.module.fail_json(msg) - - msg = f"REMOVE: {self.class_name}.refresh() ndfc_data: {self.ndfc_data}" - self.log_msg(msg) - - def _get(self, item): - return self.make_boolean(self.make_none(self.ndfc_data.get(item))) - - @property - def dev(self): - """ - Return True if NDFC is a development release. - Return False if NDFC is not a development release. - Return None otherwise - - Possible values: - True - False - None - """ - return self._get("dev") - - @property - def install(self): - """ - Return the value of install, if it exists. - Return None otherwise - - Possible values: - EASYFABRIC - (probably other values) - None - """ - return self._get("install") - - @property - def is_ha_enabled(self): - """ - Return True if NDFC is a media controller. - Return False if NDFC is not a media controller. - Return None otherwise - - Possible values: - True - False - None - """ - return self.make_boolean(self._get("isHaEnabled")) - - @property - def is_media_controller(self): - """ - Return True if NDFC is a media controller. - Return False if NDFC is not a media controller. - Return None otherwise - - Possible values: - True - False - None - """ - return self.make_boolean(self._get("isMediaController")) - - @property - def is_upgrade_inprogress(self): - """ - Return True if an NDFC upgrade is in progress. - Return False if an NDFC upgrade is not in progress. - Return None otherwise - - Possible values: - True - False - None - """ - return self.make_boolean(self._get("is_upgrade_inprogress")) - - @property - def ndfc_data(self): - """ - Return the data retrieved from the request - """ - return self.properties.get("ndfc_data") - - @property - def ndfc_result(self): - """ - Return the GET result from NDFC - """ - return self.properties.get("ndfc_result") - - @property - def ndfc_response(self): - """ - Return the GET response from NDFC - """ - return self.properties.get("ndfc_response") - - @property - def mode(self): - """ - Return the NDFC mode, if it exists. - Return None otherwise - - Possible values: - LAN - None - """ - return self._get("mode") - - @property - def uuid(self): - """ - Return the value of uuid, if it exists. - Return None otherwise - - Possible values: - uuid e.g. "f49e6088-ad4f-4406-bef6-2419de914df1" - None - """ - return self._get("uuid") - - @property - def version(self): - """ - Return the NDFC version, if it exists. - Return None otherwise - - Possible values: - version, e.g. "12.1.2e" - None - """ - return self._get("version") - - @property - def version_major(self): - """ - Return the NDFC major version, if it exists. - Return None otherwise - - We are assuming semantic versioning based on: - https://semver.org - - Possible values: - if version is 12.1.2e, return 12 - None - """ - if self.version is None: - return None - return (self._get("version").split("."))[0] - - @property - def version_minor(self): - """ - Return the NDFC minor version, if it exists. - Return None otherwise - - We are assuming semantic versioning based on: - https://semver.org - - Possible values: - if version is 12.1.2e, return 1 - None - """ - if self.version is None: - return None - return (self._get("version").split("."))[1] - - @property - def version_patch(self): - """ - Return the NDFC minor version, if it exists. - Return None otherwise - - We are assuming semantic versioning based on: - https://semver.org - - Possible values: - if version is 12.1.2e, return 2e - None - """ - if self.version is None: - return None - return (self._get("version").split("."))[2] def main(): diff --git a/plugins/modules/dcnm_image_upgrade_orig.py b/plugins/modules/dcnm_image_upgrade_orig.py new file mode 100644 index 000000000..347d5c3eb --- /dev/null +++ b/plugins/modules/dcnm_image_upgrade_orig.py @@ -0,0 +1,5306 @@ +#!/usr/bin/python +# +# Copyright (c) 2020-2024 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. +""" +Classes and methods for Ansible support of Nexus image upgrade. + +Ansible states "merged", "deleted", and "query" are implemented. + +merged: attach image policy to one or more devices +deleted: delete image policy from one or more devices +query: return image policy details for one or more devices +""" +from __future__ import absolute_import, division, print_function + +import copy +import json +from time import sleep + +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, +) + +__metaclass__ = type +__author__ = "Cisco Systems, Inc." + +DOCUMENTATION = """ +--- +module: dcnm_image_upgrade +short_description: Attach, detach, and query device image policies. +version_added: "0.9.0" +description: + - Attach, detach, and query device image policies. +author: Cisco Systems, Inc. +options: + state: + description: + - The state of DCNM after module completion. + - I(merged) and I(query) are the only states supported. + type: str + choices: + - merged + - deleted + - query + default: merged + config: + description: + - A dictionary containing the image policy configuration. + type: dict + suboptions: + policy: + description: + - Image policy name + type: str + required: true + default: False + stage: + description: + - Stage (True) or unstage (False) an image policy + type: bool + required: false + default: True + validate: + description: + - Validate (True) or do not validate (False) the image + - after staging + type: bool + required: false + default: True + reboot: + description: + - Reboot the switch after upgrade + type: bool + required: false + default: False + upgrade: + description: + - A dictionary containing upgrade toggles for nxos and epld + type: dict + suboptions: + nxos: + description: + - Enable (True) or disable (False) image upgrade + type: bool + required: false + default: True + epld: + description: + - Enable (True) or disable (False) EPLD upgrade + - If upgrade.nxos is false, epld and packages cannot both be true + - If epld is true, nxos_option must be disruptive + type: bool + required: false + default: False + options: + description: + - A dictionary containing options for each of the upgrade types + type: dict + suboptions: + nxos: + description: + - A dictionary containing nxos upgrade options + type: dict + suboptions: + mode: + description: + - nxos upgrade mode + - Choose between distruptive, non_disruptive, force_non_disruptive + type: string + required: false + default: distruptive + bios_force: + description: + - Force BIOS upgrade + type: bool + required: false + default: False + epld: + description: + - A dictionary containing epld upgrade options + type: dict + suboptions: + module: + description: + - The switch module to upgrade + - Choose between ALL, or integer values + type: string + required: false + default: ALL + golden: + description: + - Enable (True) or disable (False) reverting to the golden EPLD image + type: bool + required: false + default: False + reboot: + description: + - A dictionary containing reboot options + type: dict + suboptions: + config_reload: + description: + - Reload the configuration + type: bool + required: false + default: False + write_erase: + description: + - Erase the startup configuration + type: bool + required: false + default: False + package: + description: + - A dictionary containing package upgrade options + type: dict + suboptions: + install: + description: + - Install the package + type: bool + required: false + default: False + uninstall: + description: + - Uninstall the package + type: bool + required: false + default: False + switches: + description: + - A list of devices to attach the image policy to. + type: list + elements: dict + required: true + suboptions: + ip_address: + description: + - The IP address of the device to which the policy will be attached. + type: str + required: true + policy: + description: + - Image policy name + type: str + required: true + default: False + stage: + description: + - Stage (True) or unstage (False) an image policy + type: bool + required: false + default: True + validate: + description: + - Validate (True) or do not validate (False) the image + - after staging + type: bool + required: false + default: True + reboot: + description: + - Reboot the switch after upgrade + type: bool + required: false + default: False + upgrade: + description: + - A dictionary containing upgrade toggles for nxos and epld + type: dict + suboptions: + nxos: + description: + - Enable (True) or disable (False) image upgrade + type: bool + required: false + default: True + epld: + description: + - Enable (True) or disable (False) EPLD upgrade + - If upgrade.nxos is false, epld and packages cannot both be true + - If epld is true, nxos_option must be disruptive + type: bool + required: false + default: False + options: + description: + - A dictionary containing options for each of the upgrade types + type: dict + suboptions: + nxos: + description: + - A dictionary containing nxos upgrade options + type: dict + suboptions: + mode: + description: + - nxos upgrade mode + - Choose between distruptive, non_disruptive, force_non_disruptive + type: string + required: false + default: distruptive + bios_force: + description: + - Force BIOS upgrade + type: bool + required: false + default: False + epld: + description: + - A dictionary containing epld upgrade options + type: dict + suboptions: + module: + description: + - The switch module to upgrade + - Choose between ALL, or integer values + type: string + required: false + default: ALL + golden: + description: + - Enable (True) or disable (False) reverting to the golden EPLD image + type: bool + required: false + default: False + reboot: + description: + - A dictionary containing reboot options + type: dict + suboptions: + config_reload: + description: + - Reload the configuration + type: bool + required: false + default: False + write_erase: + description: + - Erase the startup configuration + type: bool + required: false + default: False + package: + description: + - A dictionary containing package upgrade options + type: dict + suboptions: + install: + description: + - Install the package + type: bool + required: false + default: False + uninstall: + description: + - Uninstall the package + type: bool + required: false + default: False + +""" + +EXAMPLES = """ +# This module supports the following states: +# +# merged: +# Attach image policy to one or more devices. +# +# query: +# Return image policy details for one or more devices. +# +# deleted: +# Delete image policy from one or more devices +# + +# Attach image policy NR3F to two devices +# Stage and validate the image on two devices but do not upgrade + - name: stage/validate images + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + policy: NR3F + stage: true + validate: true + upgrade: + nxos: false + epld: false + switches: + - ip_address: 192.168.1.1 + - ip_address: 192.168.1.2 + +# Attach image policy NR1F to device 192.168.1.1 +# Attach image policy NR2F to device 192.168.1.2 +# Stage the image on device 192.168.1.1, but do not upgrade +# Stage the image and upgrade device 192.168.1.2 + - name: stage/upgrade devices + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + validate: false + stage: false + upgrade: + nxos: false + epld: false + options: + nxos: + type: disruptive + epld: + module: ALL + golden: false + switches: + - ip_address: 192.168.1.1 + policy: NR1F + stage: true + validate: true + upgrade: + nxos: true + epld: false + - ip_address: 192.168.1.2 + policy: NR2F + stage: true + validate: true + upgrade: + nxos: true + epld: true + options: + nxos: + type: disruptive + epld: + module: ALL + golden: false + +# Detach image policy NR3F from two devices + - name: stage/upgrade devices + cisco.dcnm.dcnm_image_upgrade: + state: deleted + config: + policy: NR3F + switches: + - ip_address: 192.168.1.1 + - ip_address: 192.168.1.2 + +""" +class NdfcEndpoints: + """ + Endpoints for NDFC API calls + """ + def __init__(self): + self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" + + self.endpoint_feature_manager = f"{self.endpoint_api_v1}/fm" + self.endpoint_lan_fabric = f"{self.endpoint_api_v1}/lan-fabric" + + self.endpoint_image_management = f"{self.endpoint_api_v1}" + self.endpoint_image_management += "/imagemanagement" + + self.endpoint_image_upgrade = f"{self.endpoint_image_management}" + self.endpoint_image_upgrade += "/rest/imageupgrade" + + self.endpoint_package_mgnt = f"{self.endpoint_image_management}" + self.endpoint_package_mgnt += "/rest/packagemgnt" + + self.endpoint_policy_mgnt = f"{self.endpoint_image_management}" + self.endpoint_policy_mgnt += "/rest/policymgnt" + + self.endpoint_staging_management = f"{self.endpoint_image_management}" + self.endpoint_staging_management += "/rest/stagingmanagement" + + @property + def bootflash_info(self): + path = f"{self.endpoint_image_management}/rest/imagemgnt/bootFlash" + path += f"/bootflash-info" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def install_options(self): + path = f"{self.endpoint_image_upgrade}/install-options" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_stage(self): + path = f"{self.endpoint_staging_management}/stage-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_upgrade(self): + path = f"{self.endpoint_image_upgrade}/upgrade-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def image_validate(self): + path = f"{self.endpoint_staging_management}/validate-image" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def issu_info(self): + path = f"{self.endpoint_package_mgnt}/issu" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def ndfc_version(self): + path = f"{self.endpoint_feature_manager}/about/version" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policies_attached_info(self): + path = f"{self.endpoint_policy_mgnt}/all-attached-policies" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policies_info(self): + path = f"{self.endpoint_policy_mgnt}/policies" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def policy_attach(self): + path = f"{self.endpoint_policy_mgnt}/attach-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def policy_create(self): + path = f"{self.endpoint_policy_mgnt}/platform-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "POST" + return endpoint + + @property + def policy_detach(self): + path = f"{self.endpoint_policy_mgnt}/detach-policy" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "DELETE" + return endpoint + + @property + def policy_info(self): + # Replace __POLICY_NAME__ with the policy_name to query + # e.g. path.replace("__POLICY_NAME__", "NR1F") + path = f"{self.endpoint_policy_mgnt}/image-policy/__POLICY_NAME__" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def stage_info(self): + path = f"{self.endpoint_staging_management}/stage-info" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + @property + def switches_info(self): + path = f"{self.endpoint_lan_fabric}/rest/inventory/allswitches" + endpoint = {} + endpoint["path"] = path + endpoint["verb"] = "GET" + return endpoint + + +class NdfcAnsibleImageUpgradeCommon: + """ + Base class for the other classes in this file + """ + + def __init__(self, module): + self.module = module + self.params = module.params + self.debug = True + self.fd = None + self.logfile = "/tmp/dcnm_image_upgrade.log" + self.endpoints = NdfcEndpoints() + + + def _handle_response(self, response, verb): + if verb == "GET": + return self._handle_get_response(response) + if verb in {"POST", "PUT", "DELETE"}: + return self._handle_post_put_delete_response(response) + return self._handle_unknown_request_verbs(response, verb) + + def _handle_unknown_request_verbs(self, response, verb): + msg = f"Unknown request verb ({verb}) in _handle_response() for response {response}." + self.module.fail_json(msg) + + def _handle_get_response(self, response): + """ + Caller: + - self._handle_response() + Handle NDFC responses to GET requests + Returns: dict() with the following keys: + - found: + - False, if request error was "Not found" and RETURN_CODE == 404 + - True otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + success_return_codes = {200, 404} + if ( + response.get("RETURN_CODE") == 404 + and response.get("MESSAGE") == "Not Found" + ): + result["found"] = False + result["success"] = True + return result + if ( + response.get("RETURN_CODE") not in success_return_codes + or response.get("MESSAGE") != "OK" + ): + result["found"] = False + result["success"] = False + return result + result["found"] = True + result["success"] = True + return result + + def _handle_post_put_delete_response(self, response): + """ + Caller: + - self.self._handle_response() + + Handle POST, PUT responses from NDFC. + + Returns: dict() with the following keys: + - changed: + - True if changes were made to NDFC + - False otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + if response.get("ERROR") is not None: + result["success"] = False + result["changed"] = False + return result + if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: + result["success"] = False + result["changed"] = False + return result + result["success"] = True + result["changed"] = True + return result + + def log_msg(self, msg): + """ + used for debugging. disable this when committing to main + by setting __init__().debug to False + """ + if self.debug is False: + return + if self.fd is None: + try: + self.fd = open(f"{self.logfile}", "a+", encoding="UTF-8") + except IOError as err: + msg = f"error opening logfile {self.logfile}. " + msg += f"detail: {err}" + self.module.fail_json(msg) + + self.fd.write(msg) + self.fd.write("\n") + self.fd.flush() + + def make_boolean(self, value): + """ + Return value converted to boolean, if possible. + Return value, if value cannot be converted. + """ + if isinstance(value, bool): + return value + if isinstance(value, str): + if value.lower() in ["true", "yes"]: + return True + if value.lower() in ["false", "no"]: + return False + return value + + def make_none(self, value): + """ + Return None if value is an empty string, or a string + representation of a None type + Return value otherwise + """ + if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: + return None + return value + + +class NdfcAnsibleImageUpgrade(NdfcAnsibleImageUpgradeCommon): + """ + Ansible support for image policy attach, detach, and query. + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.log_msg(f"{self.class_name}.__init__") + # populated in self._build_policy_attach_payload() + self.payloads = [] + + self.config = module.params.get("config") + self.log_msg(f"{self.class_name}.__init__() self.config {self.config}") + if not isinstance(self.config, dict): + msg = "expected dict type for self.config. " + msg = +f"got {type(self.config).__name__}" + self.module.fail_json(msg) + + self.check_mode = False + self.validated = [] + self.have_create = [] + self.want_create = [] + self.need = [] + self.diff_save = {} + self.query = [] + self.result = dict(changed=False, diff=[], response=[]) + + self.mandatory_global_keys = {"switches"} + self.mandatory_switch_keys = {"ip_address"} + + if not self.mandatory_global_keys.issubset(self.config): + msg = f"{self.class_name}.__init__: " + msg += "Missing mandatory key(s) in playbook global config. " + msg += f"expected {self.mandatory_global_keys}, " + msg += f"got {self.config.keys()}" + self.module.fail_json(msg) + + if self.config["switches"] is None: + msg = f"{self.class_name}.__init__: " + msg += "missing list of switches in playbook config." + self.module.fail_json(msg) + + for switch in self.config["switches"]: + if not self.mandatory_switch_keys.issubset(switch): + msg = f"{self.class_name}.__init__: " + msg += f"missing mandatory key(s) in playbook switch config. " + msg += f"expected {self.mandatory_switch_keys}, " + msg += f"got {switch.keys()}" + self.module.fail_json(msg) + + self.log_msg(f"{self.class_name}.__init__: instantiate NdfcSwitchDetails") + self.switch_details = NdfcSwitchDetails(self.module) + self.log_msg(f"{self.class_name}.__init__: instantiate NdfcImagePolicies") + self.image_policies = NdfcImagePolicies(self.module) + + def get_have(self): + """ + Caller: main() + + Determine current switch ISSU state on NDFC + """ + self.have = NdfcSwitchIssuDetailsByIpAddress(self.module) + self.have.refresh() + + def get_want(self): + """ + Caller: main() + + Update self.want_create for all switches defined in the playbook + """ + self._merge_global_and_switch_configs(self.config) + self._validate_switch_configs() + if not self.switch_configs: + return + self.log_msg(f"{self.class_name}.get_want() self.switch_configs: {self.switch_configs}") + self.want_create = self.switch_configs + + def _get_idempotent_want(self, want): + """ + Return an itempotent want item based on the have item contents. + + The have item is obtained from an instance of NdfcSwitchIssuDetails + created in self.get_have(). + + want structure passed to this method: + + { + 'policy': 'KR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'nxos': { + 'mode': 'non_disruptive' + 'bios_force': False + }, + 'epld': { + 'module': 'ALL', + 'golden': False + } + }, + 'validate': True, + 'ip_address': '172.22.150.102' + } + + The returned idempotent_want structure is identical to the + above structure, except that the policy_changed key is added, + and values are modified based on results from the have item, + and the information returned by NdfcImageInstallOptions. + + Caller: self.get_need_merged() + """ + self.log_msg(f"{self.class_name}._get_idempotent_want() want: {want}") + self.have.ip_address = want["ip_address"] + + want["policy_changed"] = True + # In NDFC, the switch does not have an image policy attached + # Return the want item as-is with policy_changed = True + if self.have.serial_number is None: + self.log_msg(f"{self.class_name}._get_idempotent_want() no serial_number. returning want: {want}") + return want + # The switch has an image policy attached which is + # different from the want policy. + # Return the want item as-is with policy_changed = True + if want["policy"] != self.have.policy: + self.log_msg(f"{self.class_name}._get_idempotent_want() different policy attached. returning want: {want}") + return want + + # start with a copy of the want item + idempotent_want = copy.deepcopy(want) + # Give an indication to the caller that the policy has not changed + # We can use this later to determine if we need to do anything in + # the case where the image is already staged and/or upgraded. + idempotent_want["policy_changed"] = False + + # if the image is already staged, don't stage it again + if self.have.image_staged == "Success": + idempotent_want["stage"] = False + # if the image is already validated, don't validate it again + if self.have.validated == "Success": + idempotent_want["validate"] = False + # if the image is already upgraded, don't upgrade it again + if self.have.status == "In-Sync" and self.have.reason == "Upgrade" and self.have.policy == want["policy"]: + idempotent_want["upgrade"]["nxos"] = False + + # Get relevant install options from NDFC based on the + # options in our want item + instance = NdfcImageInstallOptions(self.module) + instance.policy_name = want["policy"] + msg = f"REMOVE: {self.class_name}._get_idempotent_want() " + msg += f"calling NdfcImageInstallOptions.refresh() with " + msg += f"serial_number {self.have.serial_number} " + msg += f"ip_address {self.have.ip_address} " + msg += f"device_name {self.have.device_name}" + self.log_msg(msg) + instance.serial_number = self.have.serial_number + instance.epld = want["upgrade"]["epld"] + instance.issu = want["upgrade"]["nxos"] + instance.refresh() + + if instance.epld_modules is None: + idempotent_want["upgrade"]["epld"] = False + self.log_msg(f"REMOVE: {self.class_name}._get_idempotent_want() returned idempotent_want: {idempotent_want}") + return idempotent_want + + def get_need_merged(self): + """ + Caller: main() + + For merged state, populate self.need list() with items from + our want list that are not in our have list. These items will be sent + to NDFC. + """ + need = [] + + for want_create in self.want_create: + self.have.ip_address = want_create["ip_address"] + if self.have.serial_number is not None: + idempotent_want = self._get_idempotent_want(want_create) + if ( + idempotent_want["policy_changed"] is False + and idempotent_want["stage"] is False + and idempotent_want["upgrade"]["nxos"] is False + and idempotent_want["upgrade"]["epld"] is False + ): + continue + need.append(idempotent_want) + self.need = need + msg = f"REMOVE: {self.class_name}.get_need_merged: " + msg += f"need: {self.need}" + self.log_msg(msg) + + def get_need_deleted(self): + """ + Caller: main() + + For deleted state, populate self.need list() with items from our want + list that are not in our have list. These items will be sent to NDFC. + """ + need = [] + for want in self.want_create: + self.have.ip_address = want["ip_address"] + if self.have.serial_number is None: + continue + if self.have.policy is None: + continue + need.append(want) + self.need = need + + def get_need_query(self): + """ + Caller: main() + + For query state, populate self.need list() with all items from our want + list. These items will be sent to NDFC. + """ + need = [] + for want in self.want_create: + need.append(want) + self.need = need + + @staticmethod + def _build_params_spec_for_merged_state(): + """ + Build the specs for the parameters expected when state == merged. + + Caller: _validate_input_for_merged_state() + Return: params_spec, a dictionary containing the set of + playbook parameter specifications. + """ + params_spec = {} + params_spec["policy"] = {} + params_spec["policy"]["required"] = False + params_spec["policy"]["type"] = "str" + + params_spec["stage"] = {} + params_spec["stage"]["required"] = False + params_spec["stage"]["type"] = "bool" + params_spec["stage"]["default"] = True + + params_spec["validate"] = {} + params_spec["validate"]["required"] = False + params_spec["validate"]["type"] = "bool" + params_spec["validate"]["default"] = True + + params_spec["upgrade"] = {} + params_spec["upgrade"]["required"] = False + params_spec["upgrade"]["type"] = "dict" + params_spec["upgrade"]["default"] = {} + + section = "options" + params_spec[section] = {} + params_spec[section]["required"] = False + params_spec[section]["type"] = "dict" + params_spec[section]["default"] = {} + + sub_section = "nxos" + params_spec[section][sub_section] = {} + params_spec[section][sub_section]["required"] = False + params_spec[section][sub_section]["type"] = "dict" + params_spec[section][sub_section]["default"] = {} + + params_spec[section][sub_section]["mode"] = {} + params_spec[section][sub_section]["mode"]["required"] = False + params_spec[section][sub_section]["mode"]["type"] = "str" + params_spec[section][sub_section]["mode"]["default"] = "disruptive" + + params_spec[section][sub_section]["bios_force"] = {} + params_spec[section][sub_section]["bios_force"]["required"] = False + params_spec[section][sub_section]["bios_force"]["type"] = "bool" + params_spec[section][sub_section]["bios_force"]["default"] = False + + sub_section = "epld" + params_spec[section][sub_section] = {} + params_spec[section][sub_section]["required"] = False + params_spec[section][sub_section]["type"] = "dict" + params_spec[section][sub_section]["default"] = {} + + params_spec[section][sub_section]["module"] = {} + params_spec[section][sub_section]["module"] ["required"] = False + params_spec[section][sub_section]["module"] ["type"] = "str" + params_spec[section][sub_section]["module"] ["default"] = "ALL" + + params_spec[section][sub_section]["golden"] = {} + params_spec[section][sub_section]["golden"] ["required"] = False + params_spec[section][sub_section]["golden"] ["type"] = "bool" + params_spec[section][sub_section]["golden"] ["default"] = False + + sub_section = "reboot" + params_spec[section][sub_section] = {} + params_spec[section][sub_section]["required"] = False + params_spec[section][sub_section]["type"] = "dict" + params_spec[section][sub_section]["default"] = {} + + params_spec[section][sub_section]["config_reload"] = {} + params_spec[section][sub_section]["config_reload"] ["required"] = False + params_spec[section][sub_section]["config_reload"] ["type"] = "bool" + params_spec[section][sub_section]["config_reload"] ["default"] = False + + params_spec[section][sub_section]["write_erase"] = {} + params_spec[section][sub_section]["write_erase"] ["required"] = False + params_spec[section][sub_section]["write_erase"] ["type"] = "bool" + params_spec[section][sub_section]["write_erase"] ["default"] = False + + sub_section = "package" + params_spec[section][sub_section] = {} + params_spec[section][sub_section]["required"] = False + params_spec[section][sub_section]["type"] = "dict" + params_spec[section][sub_section]["default"] = {} + + params_spec[section][sub_section]["install"] = {} + params_spec[section][sub_section]["install"] ["required"] = False + params_spec[section][sub_section]["install"] ["type"] = "bool" + params_spec[section][sub_section]["install"] ["default"] = False + + params_spec[section][sub_section]["uninstall"] = {} + params_spec[section][sub_section]["uninstall"] ["required"] = False + params_spec[section][sub_section]["uninstall"] ["type"] = "bool" + params_spec[section][sub_section]["uninstall"] ["default"] = False + + return copy.deepcopy(params_spec) + + def validate_input(self): + """ + Caller: main() + + Validate the playbook parameters + """ + state = self.params["state"] + self.log_msg(f"{self.class_name}.validate_input: state: {state}") + + if state not in ["merged", "deleted", "query"]: + msg = f"This module supports deleted, merged, and query states. Got state {state}" + self.module.fail_json(msg) + + if state == "merged": + self.log_msg(f"{self.class_name}.validate_input: call _validate_input_for_merged_state()") + self._validate_input_for_merged_state() + return + if state == "deleted": + self._validate_input_for_deleted_state() + return + if state == "query": + self._validate_input_for_query_state() + return + + def _validate_input_for_merged_state(self): + """ + Caller: self.validate_input() + + Validate that self.config contains appropriate values for merged state + """ + if not self.config: + msg = "config: element is mandatory for state merged" + self.module.fail_json(msg) + + params_spec = self._build_params_spec_for_merged_state() + + self.log_msg(f"{self.class_name}._validate_input_for_merged_state: params_spec: {params_spec}") + self.log_msg(f"{self.class_name}._validate_input_for_merged_state: self.config: {self.config}") + valid_params, invalid_params = validate_list_of_dicts( + self.config.get("switches"), params_spec, self.module + ) + # We're not using self.validated. Keeping this to avoid + # linter error due to non-use of valid_params + self.validated = copy.deepcopy(valid_params) + + if invalid_params: + msg = "Invalid parameters in playbook: " + msg += f"{','.join(invalid_params)}" + self.module.fail_json(msg) + + def _validate_input_for_deleted_state(self): + """ + Caller: self.validate_input() + + Validate that self.config contains appropriate values for deleted state + + NOTES: + 1. This is currently identical to _validate_input_for_merged_state() + 2. Adding in case there are differences in the future + """ + params_spec = self._build_params_spec_for_merged_state() + if not self.config: + msg = "config: element is mandatory for state deleted" + self.module.fail_json(msg) + + valid_params, invalid_params = validate_list_of_dicts( + self.config.get("switches"), params_spec, self.module + ) + # We're not using self.validated. Keeping this to avoid + # linter error due to non-use of valid_params + self.validated = copy.deepcopy(valid_params) + + if invalid_params: + msg = "Invalid parameters in playbook: " + msg += f"{','.join(invalid_params)}" + self.module.fail_json(msg) + + def _validate_input_for_query_state(self): + """ + Caller: self.validate_input() + + Validate that self.config contains appropriate values for query state + + NOTES: + 1. This is currently identical to _validate_input_for_merged_state() + 2. Adding in case there are differences in the future + """ + params_spec = self._build_params_spec_for_merged_state() + if not self.config: + msg = "config: element is mandatory for state query" + self.module.fail_json(msg) + + valid_params, invalid_params = validate_list_of_dicts( + self.config.get("switches"), params_spec, self.module + ) + # We're not using self.validated. Keeping this to avoid + # linter error due to non-use of valid_params + self.validated = copy.deepcopy(valid_params) + + if invalid_params: + msg = "Invalid parameters in playbook: " + msg += f"{','.join(invalid_params)}" + self.module.fail_json(msg) + + def _merge_global_and_switch_configs(self, config): + """ + Merge the global config with each switch config and return + a dict of switch configs keyed on switch ip_address. + + Merge rules: + 1. switch_config takes precedence over global_config. + 2. If switch_config is missing a parameter, use parameter + from global_config. + 3. If a switch_config has a parameter, use it. + 4. If global_config and switch_config are both missing an + optional parameter, use the parameter's default value. + 5. If global_config and switch_config are both missing a + mandatory parameter, fail. + """ + if not config.get("switches"): + msg = f"{self.class_name}._merge_global_and_switch_configs: " + msg += "playbook is missing list of switches" + self.module.fail_json(msg) + + msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + msg += f"config: {config}" + self.log_msg(msg) + global_config = {} + global_config["policy"] = config.get("policy") + global_config["stage"] = config.get("stage") + global_config["upgrade"] = config.get("upgrade") + global_config["options"] = config.get("options") + global_config["validate"] = config.get("validate") + msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + msg += f"global_config: {global_config}" + self.log_msg(msg) + self.switch_configs = [] + for switch in config["switches"]: + switch_config = global_config.copy() | switch.copy() + msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + msg += f"merged switch_config: {switch_config}" + self.log_msg(msg) + self.switch_configs.append(switch_config) + + def _validate_switch_configs(self): + """ + Ensure mandatory parameters are present for each switch + - fail_json if this isn't the case + Set defaults for missing optional parameters + + NOTES: + 1. Final application of missing default parameters is done in + NdfcImageUpgrade.commit() + + Callers: + - self.get_want + """ + for switch in self.switch_configs: + msg = f"REMOVE: {self.class_name}._validate_switch_configs: " + msg += f"switch: {switch}" + self.log_msg(msg) + if not switch.get("ip_address"): + msg = "playbook is missing ip_address for at least one switch" + self.module.fail_json(msg) + # for query state, the only mandatory parameter is ip_address + # so skip the remaining checks + if self.params.get("state") == "query": + continue + if switch.get("policy") is None: + msg = "playbook is missing image policy for switch " + msg += f"{switch.get('ip_address')} " + msg += "and global image policy is not defined." + self.module.fail_json(msg) + + def _build_policy_attach_payload(self): + """ + Build the payload for the policy attach request to NDFC + Verify that the image policy exists on NDFC + Verify that the image policy supports the switch platform + + Callers: + - self.handle_merged_state + """ + self.payloads = [] + # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests + self.switch_details.refresh() + self.image_policies.refresh() + for switch in self.need: + if switch.get("policy_changed") is False: + continue + self.switch_details.ip_address = switch.get("ip_address") + self.image_policies.policy_name = switch.get("policy") + + # Fail if the image policy does not exist. + # Image policy creation is handled by a different module. + if self.image_policies.name is None: + msg = f"policy {switch.get('policy')} does not exist on NDFC" + self.module.fail_json(msg) + + # Fail if the image policy does not support the switch platform + if self.switch_details.platform not in self.image_policies.platform: + msg = f"policy {switch.get('policy')} does not support platform " + msg += f"{self.switch_details.platform}. {switch.get('policy')} " + msg += "supports the following platform(s): " + msg += f"{self.image_policies.platform}" + self.module.fail_json(msg) + + payload = {} + payload["policyName"] = self.image_policies.name + # switch_details.host_name is always None in 12.1.2e + # so we're using logical_name instead + payload["hostName"] = self.switch_details.logical_name + payload["ipAddr"] = self.switch_details.ip_address + payload["platform"] = self.switch_details.platform + payload["serialNumber"] = self.switch_details.serial_number + # payload["bootstrapMode"] = switch.get('bootstrap_mode') + + for item in payload: + if payload[item] is None: + msg = f"Unable to determine {item} for switch {switch.get('ip_address')}. " + msg += "Please verify that the switch is managed by NDFC." + self.module.fail_json(msg) + self.payloads.append(payload) + + def _send_policy_attach_payload(self): + """ + Send the policy attach payload to NDFC and handle the response + + Callers: + - self.handle_merged_state + """ + if len(self.payloads) == 0: + return + path = self.endpoints.policy_attach.get("path") + verb = self.endpoints.policy_attach.get("verb") + payload = {} + payload["mappingList"] = self.payloads + response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) + result = self._handle_response(response, verb) + + if not result["success"]: + self._failure(response) + + def _stage_images(self, serial_numbers): + """ + Initiate image staging to the switch(es) associated with serial_numbers + + Callers: + - handle_merged_state + """ + instance = NdfcImageStage(self.module) + instance.serial_numbers = serial_numbers + instance.commit() + + def _validate_images(self, serial_numbers): + """ + Validate the image staged to the switch(es) + + Callers: + - handle_merged_state + """ + instance = NdfcImageValidate(self.module) + instance.serial_numbers = serial_numbers + # TODO:2 Discuss with Mike/Shangxin - NdfcImageValidate.non_disruptive + # Should we add this option to the playbook? + # It's supported in NdfcImageValidate with default of False + # instance.non_disruptive = False + instance.commit() + + def _verify_install_options(self, devices): + """ + Verify that the install options for the devices(es) are valid + + Example devices structure: + + [ + { + 'policy': 'KR3F', + 'stage': False, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'nxos': { + 'mode': 'non_disruptive' + }, + 'epld': { + 'module': 'ALL', + 'golden': False + } + }, + 'validate': False, + 'ip_address': '172.22.150.102', + 'policy_changed': False + }, + etc... + ] + + Callers: + - self.handle_merged_state + """ + if len(devices) == 0: + return + install_options = NdfcImageInstallOptions(self.module) + # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests + self.switch_details.refresh() + for device in devices: + self.log_msg(f"REMOVE: {self.class_name}._verify_install_options: device: {device}") + self.switch_details.ip_address = device.get("ip_address") + install_options.serial_number = self.switch_details.serial_number + install_options.policy_name = device["policy"] + install_options.epld = device["upgrade"]["epld"] + install_options.issu = device["upgrade"]["nxos"] + install_options.refresh() + if install_options.status not in ["Success", "Skipped"] and device["upgrade"]["nxos"] is True: + msg = f"NXOS upgrade is set to True for switch " + msg += f"{device['ip_address']}, but the image policy " + msg += f"{install_options.policy_name} does not contain an " + msg += f"NX-OS image" + self.module.fail_json(msg) + if install_options.epld_modules is None and device["upgrade"]["epld"] is True: + msg = f"EPLD upgrade is set to True for switch " + msg += f"{device['ip_address']}, but the image policy " + msg += f"{install_options.policy_name} does not contain an " + msg += f"EPLD image." + self.module.fail_json(msg) + + def _upgrade_images(self, devices): + """ + Upgrade the switch(es) to the specified image + + Callers: + - handle_merged_state + """ + self.log_msg(f"REMOVE: {self.class_name}._upgrade_images: devices: {devices}") + upgrade = NdfcImageUpgrade(self.module) + upgrade.devices = devices + # TODO:2 Discuss with Mike/Shangxin. Upgrade option handling mutex options. + # I'm leaning toward doing this in NdfcImageUpgrade().validate_options() + # which would cover the various scenarios and fail_json() on invalid + # combinations. + # For epld upgrade disrutive must be True and non_disruptive must be False + # upgrade.epld_upgrade = True + # upgrade.disruptive = True + # upgrade.non_disruptive = False + # upgrade.epld_module = "ALL" + upgrade.commit() + + def handle_merged_state(self): + """ + Update the switch policy if it has changed. + Stage the image if requested. + Validate the image if requested. + Upgrade the image if requested. + + Caller: main() + """ + # TODO:1 Replace these with NdfcImagePolicyAction + # See commented code below + self._build_policy_attach_payload() + self._send_policy_attach_payload() + + # Use (or not) below for policy attach/detach + # instance = NdfcImagePolicyAction(self.module) + # instance.policy_name = "NR3F" + # instance.action = "attach" # or detach + # instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] + # instance.commit() + # policy_attach_devices = [] + # policy_detach_devices = [] + + stage_devices = [] + validate_devices = [] + upgrade_devices = [] + + # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests + self.switch_details.refresh() + for switch in self.need: + msg = f"REMOVE: {self.class_name}.handle_merged_state: switch: {switch}" + self.log_msg(msg) + self.switch_details.ip_address = switch.get("ip_address") + device = {} + device["serial_number"] = self.switch_details.serial_number + self.have.ip_address = self.switch_details.ip_address + device["policy_name"] = switch.get("policy") + device["ip_address"] = self.switch_details.ip_address + if switch.get("stage") is not False: + stage_devices.append(device["serial_number"]) + if switch.get("validate") is not False: + validate_devices.append(device["serial_number"]) + if ( + switch.get("upgrade").get("nxos") is not False or + switch.get("upgrade").get("epld") is not False): + upgrade_devices.append(switch) + + msg = f"REMOVE: {self.class_name}.handle_merged_state: stage_devices: {stage_devices}" + self.log_msg(msg) + self._stage_images(stage_devices) + + self.log_msg( + f"REMOVE: {self.class_name}.handle_merged_state: validate_devices: {validate_devices}" + ) + self._validate_images(validate_devices) + + self.log_msg( + f"REMOVE: {self.class_name}.handle_merged_state: upgrade_devices: {upgrade_devices}" + ) + self._verify_install_options(upgrade_devices) + self._upgrade_images(upgrade_devices) + + def handle_deleted_state(self): + """ + Delete the image policy from the switch(es) + + Caller: main() + """ + msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + msg += f"Entered with self.need {self.need}" + self.log_msg(msg) + detach_policy_devices = {} + # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests + self.switch_details.refresh() + self.image_policies.refresh() + for switch in self.need: + self.switch_details.ip_address = switch.get("ip_address") + self.image_policies.policy_name = switch.get("policy") + # if self.image_policies.name is None: + # continue + if self.image_policies.name not in detach_policy_devices: + detach_policy_devices[self.image_policies.policy_name] = [] + detach_policy_devices[self.image_policies.policy_name].append( + self.switch_details.serial_number + ) + msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + msg += f"detach_policy_devices: {detach_policy_devices}" + self.log_msg(msg) + + if len(detach_policy_devices) == 0: + self.result = dict(changed=False, diff=[], response=[]) + return + instance = NdfcImagePolicyAction(self.module) + for policy_name in detach_policy_devices: + msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + msg += f"detach policy_name: {policy_name}" + msg += f" from devices: {detach_policy_devices[policy_name]}" + self.log_msg(msg) + instance.policy_name = policy_name + instance.action = "detach" + instance.serial_numbers = detach_policy_devices[policy_name] + instance.commit() + + def handle_query_state(self): + """ + Return the ISSU state of the switch(es) listed in the playbook + + Caller: main() + """ + instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + instance.refresh() + msg = f"REMOVE: {self.class_name}.handle_query_state: " + msg += f"Entered. self.need {self.need}" + self.log_msg(msg) + query_devices = [] + for switch in self.need: + instance.ip_address = switch.get("ip_address") + if instance.filtered_data is None: + continue + query_devices.append(instance.filtered_data) + msg = f"REMOVE: {self.class_name}.handle_query_state: " + msg += f"query_policies: {query_devices}" + self.log_msg(msg) + self.result["response"] = query_devices + self.result["diff"] = [] + self.result["changed"] = False + + + def _failure(self, resp): + """ + Caller: self.attach_policies() + + This came from dcnm_inventory.py, but doesn't seem to be correct + for the case where resp["DATA"] does not exist? + + If resp["DATA"] does not exist, the contents of the + if block don't seem to actually do anything: + - data will be None + - Hence, data.get("stackTrace") will also be None + - Hence, data.update() and res.update() are never executed + + So, the only two lines that will actually ever be executed are + the happy path: + + res = copy.deepcopy(resp) + self.module.fail_json(msg=res) + """ + res = copy.deepcopy(resp) + + if not resp.get("DATA"): + data = copy.deepcopy(resp.get("DATA")) + if data.get("stackTrace"): + data.update( + {"stackTrace": "Stack trace is hidden, use '-vvvvv' to print it"} + ) + res.update({"DATA": data}) + + self.module.fail_json(msg=res) + + +class NdfcSwitchDetails(NdfcAnsibleImageUpgradeCommon): + """ + Retrieve switch details from NDFC and provide property accessors + for the switch attributes. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchDetails(module) + instance.refresh() + instance.ip_address = 10.1.1.1 + fabric_name = instance.fabric_name + serial_number = instance.serial_number + etc... + + Switch details are retrieved by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["ip_address"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh switch_details with current switch details from NDFC + """ + path = self.endpoints.switches_info.get("path") + verb = self.endpoints.switches_info.get("verb") + self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") + self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response {self.ndfc_response}" + self.log_msg(msg) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_result {self.ndfc_result}" + self.log_msg(msg) + + if self.ndfc_response["RETURN_CODE"] != 200: + msg = "Unable to retrieve switch information from NDFC. " + msg += f"Got response {self.ndfc_response}" + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA") + self.properties["ndfc_data"] = {} + for switch in data: + self.properties["ndfc_data"][switch["ipAddress"]] = switch + + msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_data {self.ndfc_data}" + self.log_msg(msg) + + def _get(self, item): + if self.ip_address is None: + msg = f"{self.class_name}: set instance.ip_address " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + return self.make_boolean( + self.make_none( + self.properties["ndfc_data"][self.ip_address].get(item) + ) + ) + + @property + def ip_address(self): + """ + Set the ip_address of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("ip_address") + + @ip_address.setter + def ip_address(self, value): + self.properties["ip_address"] = value + + @property + def fabric_name(self): + """ + Return the fabricName of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("fabricName") + + @property + def hostname(self): + """ + Return the hostName of the switch with ip_address, if it exists. + Return None otherwise + + NOTES: + 1. This is None for 12.1.2e + 2. Better to use logical_name which is populated in both 12.1.2e and 12.1.3b + """ + return self._get("hostName") + + @property + def logical_name(self): + """ + Return the logicalName of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("logicalName") + + @property + def model(self): + """ + Return the model of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("model") + + @property + def ndfc_data(self): + """ + Return parsed data from the GET request. + Return None otherwise + + NOTE: Keyed on ip_address + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the GET request. + Return None otherwise + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result of the GET request. + Return None otherwise + """ + return self.properties["ndfc_result"] + + @property + def platform(self): + """ + Return the platform of the switch with ip_address, if it exists. + Return None otherwise + + NOTE: This is derived from "model" and is not in the NDFC response + """ + model = self._get("model") + if model is None: + return None + return model.split("-")[0] + + @property + def role(self): + """ + Return the switchRole of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("switchRole") + + @property + def serial_number(self): + """ + Return the serialNumber of the switch with ip_address, if it exists. + Return None otherwise + """ + return self._get("serialNumber") + + +class NdfcImageInstallOptions(NdfcAnsibleImageUpgradeCommon): + """ + Retrieve install-options details for ONE switch from NDFC and + provide property accessors for the policy attributes. + + Caveats: + - This retrieves for a SINGLE switch only. + - Set serial_number and policy_name and call refresh() for + each switch separately. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImageInstallOptions(module) + # Mandatory + instance.policy_name = "NR3F" + instance.serial_number = "FDO211218GC" + # Optional + instance.epld = True + instance.package_install = True + instance.issu = True + # Retrieve install-options details from NDFC + instance.refresh() + if instance.device_name is None: + print("Cannot retrieve policy/serial_number combination from NDFC") + exit(1) + status = instance.status + platform = instance.platform + etc... + + install-options are retrieved by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options + Request body: + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + }, + { + "serialNumber": "FDO211218GC", + "policyName": "NR3F" + } + ], + "issu": true, + "epld": false, + "packageInstall": false + } + Response body: + NOTES: + 1. epldModules will be null if epld is false in the request body. + This class converts this to None (python NoneType) in this case. + + { + "compatibilityStatusList": [ + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "platform": "N9K/N3K", + "version": "10.2.5", + "osType": "64bit", + "status": "Skipped", + "installOption": "NA", + "compDisp": "Compatibility status skipped.", + "versionCheck": "Compatibility status skipped.", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "timestamp": "NA" + } + ], + "epldModules": { + "moduleList": [ + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "IO FPGA", + "oldVersion": "0x15", + "newVersion": "0x15" + }, + { + "deviceName": "cvd-1312-leaf", + "ipAddress": "172.22.150.103", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "MI FPGA", + "oldVersion": "0x4", + "newVersion": "0x04" + } + ], + "bException": false, + "exceptionReason": null + }, + "installPacakges": null, + "errMessage": "" + } + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["epld"] = False + self.properties["issu"] = True + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["package_install"] = False + self.properties["policy_name"] = None + self.properties["serial_number"] = None + self.properties["epld_modules"] = None + + def refresh(self): + """ + Refresh self.data with current install-options from NDFC + """ + if self.policy_name is None: + msg = f"{self.class_name}.refresh: " + msg += "instance.policy_name must be set before " + msg += "calling refresh()" + self.module.fail_json(msg) + if self.serial_number is None: + msg = f"{self.class_name}.refresh: " + msg += f"instance.serial_number must be set before " + msg += f"calling refresh()" + self.module.fail_json(msg) + + path = self.endpoints.install_options.get("path") + verb = self.endpoints.install_options.get("verb") + self._build_payload() + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"payload: {self.payload}" + self.log_msg(msg) + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"ndfc_response: {self.ndfc_response}" + self.log_msg(msg) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + # TODO:2 should error message contain full response or just DATA.error? + if self.ndfc_result["success"] is False: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retrieving install-options from NDFC. " + msg += f"NDFC response: {self.ndfc_response}" + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.data = self.properties["ndfc_data"] + if self.data.get("compatibilityStatusList") is None: + self.compatibility_status = {} + else: + self.compatibility_status = self.data.get("compatibilityStatusList")[0] + + def _build_payload(self): + """ + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + } + ], + "issu": true, + "epld": false, + "packageInstall": false + } + """ + self.payload = {} + self.payload["devices"] = [] + devices = {} + devices["serialNumber"] = self.serial_number + devices["policyName"] = self.policy_name + self.payload["devices"].append(devices) + self.payload["issu"] = self.issu + self.payload["epld"] = self.epld + self.payload["packageInstall"] = self.package_install + + def _get(self, item): + return self.data.get(item) + + # Mandatory properties + @property + def policy_name(self): + """ + Set the policy_name of the policy to query. + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def serial_number(self): + """ + Set the serial_number of the device to query. + """ + return self.properties.get("serial_number") + + @serial_number.setter + def serial_number(self, value): + self.properties["serial_number"] = value + + # Optional properties + @property + def issu(self): + """ + Enable (True) or disable (False) issu compatibility check. + Valid values: + True - Enable issu compatibility check + False - Disable issu compatibility check + Default: True + """ + return self.properties.get("issu") + + @issu.setter + def issu(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.issu.setter: " + msg += "issu must be a boolean value" + self.module.fail_json(msg) + self.properties["issu"] = value + + @property + def epld(self): + """ + Enable (True) or disable (False) epld compatibility check. + + Valid values: + True - Enable epld compatibility check + False - Disable epld compatibility check + Default: False + """ + return self.properties.get("epld") + + @epld.setter + def epld(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.epld.setter: " + msg += "epld must be a boolean value" + self.module.fail_json(msg) + self.properties["epld"] = value + + @property + def package_install(self): + """ + Enable (True) or disable (False) package_install compatibility check. + Valid values: + True - Enable package_install compatibility check + False - Disable package_install compatibility check + Default: False + """ + return self.properties.get("package_install") + + @package_install.setter + def package_install(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.package_install.setter: " + msg += "package_install must be a boolean value" + self.module.fail_json(msg) + self.properties["package_install"] = value + + # Retrievable properties + @property + def comp_disp(self): + """ + Return the compDisp (CLI output from show install all status) + of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("compDisp") + + @property + def device_name(self): + """ + Return the deviceName of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("deviceName") + + @property + def epld_modules(self): + """ + Return the epldModules of the install-options response, + if it exists. + Return None otherwise + + epldModules will be "null" if self.epld is False, so + make_none() will convert to NoneType in this case. + """ + return self.make_none(self._get("epldModules")) + + @property + def err_message(self): + """ + Return the errMessage of the install-options response, + if it exists. + Return None otherwise + + epldModules will be "null" if self.epld is False, so + make_none() will convert to NoneType in this case. + """ + return self._get("errMessage") + + @property + def install_option(self): + """ + Return the installOption of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("installOption") + + @property + def install_packages(self): + """ + Return the installPackages of the install-options response, + if it exists. + Return None otherwise + + NOTE: yes, installPacakges is misspelled in the response in the + following versions (at least): + 12.1.2e + 12.1.3b + """ + return self.make_none(self._get("installPacakges")) + + @property + def ip_address(self): + """ + Return the ipAddress of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("ipAddress") + + @property + def ndfc_data(self): + """ + Return the raw data from the NDFC response. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_response(self): + """ + Return the response from NDFC of the query. + """ + return self.properties.get("ndfc_response") + + @property + def ndfc_result(self): + """ + Return the result from NDFC of the query. + """ + return self.properties.get("ndfc_result") + + @property + def os_type(self): + """ + Return the osType of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("osType") + + @property + def platform(self): + """ + Return the platform of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("platform") + + @property + def pre_issu_link(self): + """ + Return the preIssuLink of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("preIssuLink") + + @property + def raw_data(self): + """ + Return the raw data of the install-options response, if it exists. + """ + return self.data + + @property + def raw_response(self): + """ + Return the raw response, if it exists. + """ + return self.response + + @property + def rep_status(self): + """ + Return the repStatus of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("repStatus") + + @property + def status(self): + """ + Return the status of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("status") + + @property + def timestamp(self): + """ + Return the timestamp of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("timestamp") + + @property + def version(self): + """ + Return the version of the install-options response, + if it exists. + Return None otherwise + """ + return self.compatibility_status.get("version") + + @property + def version_check(self): + """ + Return the versionCheck (version check CLI output) + of the install-options response, if it exists. + Return None otherwise + """ + return self.compatibility_status.get("versionCheck") + + +# ============================================================================== +class NdfcImagePolicyAction(NdfcAnsibleImageUpgradeCommon): + """ + Perform image policy actions on NDFC on one or more switches. + + Support for the following actions: + - attach + - detach + - query + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImagePolicyAction(module) + instance.policy_name = "NR3F" + instance.action = "attach" # or detach, or query + instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] + instance.commit() + # for query only + query_result = instance.query_result + + Endpoints: + For action == attach: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy + For action == detach: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy + For action == query: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + self.image_policies = NdfcImagePolicies(self.module) + self.switch_issu_details = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.valid_actions = {"attach", "detach", "query"} + + def _init_properties(self): + self.properties = {} + self.properties["action"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["policy_name"] = None + self.properties["query_result"] = None + self.properties["serial_numbers"] = None + + def build_attach_payload(self): + """ + build the payload to send in the POST request + to attach policies to devices + + caller _attach_policy() + """ + self.payloads = [] + # TODO:2 this results in more calls than necessary to NDFC. Need a better way to handle this. + self.switch_issu_details.refresh() + for serial_number in self.serial_numbers: + self.switch_issu_details.serial_number = serial_number + payload = {} + payload["policyName"] = self.policy_name + payload["hostName"] = self.switch_issu_details.device_name + payload["ipAddr"] = self.switch_issu_details.ip_address + payload["platform"] = self.switch_issu_details.platform + payload["serialNumber"] = self.switch_issu_details.serial_number + for item in payload: + if payload[item] is None: + msg = f"Unable to determine {item} for switch " + msg += f"{self.switch_issu_details.ip_address}, " + msg += f"{self.switch_issu_details.serial_number}, " + msg += f"{self.switch_issu_details.device_name}. " + msg += "Please verify that the switch is managed by NDFC." + self.module.fail_json(msg) + self.payloads.append(payload) + + def validate_request(self): + """ + validations prior to commit() should be added here. + """ + self.log_msg(f"REMOVE: {self.class_name}.validate_request: Entered") + if self.action is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.action must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + if self.policy_name is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.policy_name must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + self.log_msg(f"REMOVE: {self.class_name}.validate_request: action {self.action}") + + if self.action == "query": + return + + self.log_msg(f"REMOVE: {self.class_name}.validate_request: serial_numbers {self.serial_numbers}") + + if self.serial_numbers is None: + msg = f"{self.class_name}.validate_request: " + msg += "instance.serial_numbers must be set before " + msg += "calling commit()" + self.module.fail_json(msg) + + + # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it + self.image_policies.refresh() + self.switch_issu_details.refresh() + # Fail if the image policy does not support the switch platform + self.image_policies.policy_name = self.policy_name + for serial_number in self.serial_numbers: + self.switch_issu_details.serial_number = serial_number + if self.switch_issu_details.platform not in self.image_policies.platform: + msg = f"policy {self.policy_name} does not support platform " + msg += f"{self.switch_issu_details.platform}. {self.policy_name} " + msg += "supports the following platform(s): " + msg += f"{self.image_policies.platform}" + self.module.fail_json(msg) + + def commit(self): + self.validate_request() + if self.action == "attach": + self._attach_policy() + elif self.action == "detach": + self._detach_policy() + elif self.action == "query": + self._query_policy() + else: + msg = f"{self.class_name}.commit: " + msg += f"Unknown action {self.action}." + self.module.fail_json(msg) + + def _attach_policy(self): + """ + Attach policy_name to the switch(es) associated with serial_numbers + + NOTES: + 1. This method creates a list of responses and results which + are accessible via properties ndfc_response and ndfc_result, + respectively. + """ + self.build_attach_payload() + path = self.endpoints.policy_attach.get("path") + verb = self.endpoints.policy_attach.get("verb") + responses = [] + results = [] + for payload in self.payloads: + response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) + result = self._handle_response(response, verb) + if not result["success"]: + msg = f"{self.class_name}._attach_policy: " + msg += f"Bad result when attaching policy {self.policy_name} " + msg += f"to switch {payload['ipAddr']}." + self.module.fail_json(msg) + responses.append(response) + results.append(result) + self.properties["ndfc_response"] = responses + self.properties["ndfc_result"] = results + + def _detach_policy(self): + """ + Detach policy_name from the switch(es) associated with serial_numbers + verb: DELETE + endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy + query_params: ?serialNumber=FDO211218GC,FDO21120U5D + """ + path = self.endpoints.policy_detach.get("path") + verb = self.endpoints.policy_detach.get("verb") + query_params = ",".join(self.serial_numbers) + path += f"?serialNumber={query_params}" + response = dcnm_send(self.module, verb, path) + result = self._handle_response(response, verb) + if not result["success"]: + self._failure(response) + self.properties["ndfc_response"] = response + self.properties["ndfc_result"] = result + + def _query_policy(self): + """ + Query the image policy + verb: GET + endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ + """ + path = self.endpoints.policy_info.get("path") + verb = self.endpoints.policy_info.get("verb") + path = path.replace("__POLICY_NAME__", self.policy_name) + response = dcnm_send(self.module, verb, path) + result = self._handle_response(response, verb) + if not result["success"]: + self._failure(response) + self.properties["query_result"] = response.get("DATA") + self.properties["ndfc_response"] = response + self.properties["ndfc_result"] = result + + @property + def query_result(self): + """ + Return the value of properties["query_result"]. + """ + return self.properties.get("query_result") + + @property + def action(self): + """ + Set the action to take. Either "attach" or "detach". + + Must be set prior to calling instance.commit() + """ + return self.properties.get("action") + + @action.setter + def action(self, value): + if value not in self.valid_actions: + msg = f"{self.class_name}: instance.action must be " + msg += f"one of {','.join(sorted(self.valid_actions))}" + self.module.fail_json(msg) + self.properties["action"] = value + + @property + def ndfc_response(self): + """ + Return the raw response from NDFC after calling commit(). + + In the case of attach, this is a list of responses. + """ + return self.properties.get("ndfc_response") + + @property + def ndfc_result(self): + """ + Return the raw result from NDFC after calling commit(). + + In the case of attach, this is a list of results. + """ + return self.properties.get("ndfc_result") + + @property + def policy_name(self): + """ + Set the name of the policy to attach, detach, query. + + Must be set prior to calling instance.commit() + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to/from which + policy_name will be attached or detached. + + Must be set prior to calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.class_name}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + +# ============================================================================== + + +class NdfcImagePolicies(NdfcAnsibleImageUpgradeCommon): + """ + Retrieve image policy details from NDFC and provide property accessors + for the policy attributes. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImagePolicies(module).refresh() + instance.policy_name = "NR3F" + if instance.name is None: + print("policy NR3F does not exist on NDFC") + exit(1) + policy_name = instance.name + platform = instance.platform + epd_image_name = instance.epld_image_name + etc... + + Policies are retrieved on instantiation of this class. + Policies can be refreshed by calling instance.refresh(). + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self.log_msg(f"{self.class_name}.__init__ entered") + self._init_properties() + # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it + #self.refresh() + + def _init_properties(self): + self.properties = {} + self.properties["policy_name"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + + def refresh(self): + """ + Refresh self.image_policies with current image policies from NDFC + """ + path = self.endpoints.policies_info.get("path") + verb = self.endpoints.policies_info.get("verb") + + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"result: {self.ndfc_result}" + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retriving image policy " + msg += "information from NDFC." + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA").get("lastOperDataObject") + if data is None: + msg = f"{self.class_name}.refresh: " + msg += "Bad response when retrieving image policy " + msg += "information from NDFC." + self.module.fail_json(msg) + if len(data) == 0: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no defined image policies." + self.module.fail_json(msg) + self.properties["ndfc_data"] = {} + for policy in data: + policy_name = policy.get("policyName") + if policy_name is None: + msg = f"{self.class_name}.refresh: " + msg += "Cannot parse NDFC policy information" + self.module.fail_json(msg) + self.properties["ndfc_data"][policy_name] = policy + + def _get(self, item): + if self.policy_name is None: + msg = f"{self.class_name}._get: " + msg += f"instance.policy_name must be set before " + msg += f"accessing property {item}." + self.module.fail_json(msg) + if self.properties['ndfc_data'].get(self.policy_name) is None: + msg = f"{self.class_name}._get: " + msg += f"policy_name {self.policy_name} is not defined in NDFC" + self.module.fail_json(msg) + return_item = self.make_boolean(self.properties["ndfc_data"][self.policy_name].get(item)) + return_item = self.make_none(return_item) + return return_item + + @property + def description(self): + """ + Return the policyDescr of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyDescr") + + @property + def epld_image_name(self): + """ + Return the epldImgName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("epldImgName") + + @property + def name(self): + """ + Return the name of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyName") + + @property + def ndfc_data(self): + """ + Return the parsed data from the NDFC response as a dictionary, + keyed on policy_name. + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the NDFC response. + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result from the NDFC response. + """ + return self.properties["ndfc_result"] + + @property + def policy_name(self): + """ + Set the name of the policy to query. + + This must be set prior to accessing any other properties + """ + return self.properties.get("policy_name") + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value + + @property + def policy_type(self): + """ + Return the policyType of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policyType") + + @property + def nxos_version(self): + """ + Return the nxosVersion of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("nxosVersion") + + @property + def package_name(self): + """ + Return the packageName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("packageName") + + @property + def platform(self): + """ + Return the platform of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("platform") + + @property + def platform_policies(self): + """ + Return the platformPolicies of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("platformPolicies") + + @property + def ref_count(self): + """ + Return the reference count of the policy matching self.policy_name, + if it exists. The reference count is the number of switches using + this policy. + Return None otherwise + """ + return self._get("ref_count") + + @property + def rpm_images(self): + """ + Return the rpmimages of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("rpmimages") + + @property + def image_name(self): + """ + Return the imageName of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("imageName") + + @property + def agnostic(self): + """ + Return the value of agnostic for the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("agnostic") + + +class NdfcSwitchIssuDetails(NdfcAnsibleImageUpgradeCommon): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes. + + Usage: See subclasses. + + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu + + Response body: + { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO211218GC", + "deviceName": "cvd-1312-leaf", + "fabric": "fff", + "version": "10.3(2)", + "policy": "NR3F", + "status": "In-Sync", + "reason": "Compliance", + "imageStaged": "Success", + "validated": "None", + "upgrade": "None", + "upgGroups": "None", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": null, + "vpcPeer": null, + "role": "leaf", + "lastUpgAction": "Never", + "model": "N9K-C93180YC-EX", + "ipAddress": "172.22.150.103", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 0, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 8430, + "platform": "N9K", + "vpc_role": null, + "ip_address": "172.22.150.103", + "peer": null, + "vdc_id": -1, + "sys_name": "cvd-1312-leaf", + "id": 3, + "group": "fff", + "fcoEEnabled": false, + "mds": false + }, + {etc...} + ] + + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["ndfc_response"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_data"] = None + # action_keys is used in subclasses to determine if any actions + # are in progress. + # Property actions_in_progress returns True if so, False otherwise + self.properties["action_keys"] = set() + self.properties["action_keys"].add("imageStaged") + self.properties["action_keys"].add("upgrade") + self.properties["action_keys"].add("validated") + + + def refresh(self) -> None: + """ + Refresh current issu details from NDFC + """ + path = self.endpoints.issu_info.get("path") + verb = self.endpoints.issu_info.get("verb") + + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + + msg = f"REMOVE: {self.class_name}.refresh: " + msg += f"result: {self.ndfc_result}" + # ndfc_result for 404 response + # {'found': False, 'success': True} + if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: + msg = f"{self.class_name}.refresh: " + msg += "Bad result when retriving switch " + msg += "information from NDFC" + self.module.fail_json(msg) + + data = self.ndfc_response.get("DATA").get("lastOperDataObject") + if data is None: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no switch ISSU information." + self.module.fail_json(msg) + if len(data) == 0: + msg = f"{self.class_name}.refresh: " + msg += "NDFC has no switch ISSU information." + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA", {}).get( + "lastOperDataObject", [] + ) + self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + + @property + def actions_in_progress(self): + """ + Return True if any actions are in progress + Return False otherwise + """ + for action_key in self.properties["action_keys"]: + if self._get(action_key) == "In-Progress": + return True + return False + + def _get(self, item): + """ + overridden in subclasses + """ + pass + + @property + def ndfc_data(self): + """ + Return the raw data retrieved from NDFC + """ + return self.properties["ndfc_data"] + + @property + def ndfc_response(self): + """ + Return the raw response from the GET request. + Return None otherwise + """ + return self.properties["ndfc_response"] + + @property + def ndfc_result(self): + """ + Return the raw result of the GET request. + Return None otherwise + """ + return self.properties["ndfc_result"] + + @property + def device_name(self): + """ + Return the deviceName of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + device name, e.g. "cvd-1312-leaf" + None + """ + return self._get("deviceName") + + @property + def eth_switch_id(self): + """ + Return the ethswitchid of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + integer + None + """ + return self._get("ethswitchid") + + @property + def fabric(self): + """ + Return the fabric of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + fabric name, e.g. "myfabric" + None + """ + return self._get("fabric") + + @property + def fcoe_enabled(self): + """ + Return whether FCOE is enabled on the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + boolean (true/false) + None + """ + return self.make_boolean(self._get("fcoEEnabled")) + + @property + def group(self): + """ + Return the group of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + group name, e.g. "mygroup" + None + """ + return self._get("group") + + @property + # id is a python keyword, so we can't use it as a property name + # so we use switch_id instead + def switch_id(self): + """ + Return the switch ID of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("id") + + @property + def image_staged(self): + """ + Return the imageStaged of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Success + Failed + None + """ + return self._get("imageStaged") + + @property + def image_staged_percent(self): + """ + Return the imageStagedPercent of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("imageStagedPercent") + + @property + def ip_address(self): + """ + Return the ipAddress of the switch, if it exists. + Return None otherwise + + Possible values: + switch IP address + None + """ + return self._get("ipAddress") + + @property + def issu_allowed(self): + """ + Return the issuAllowed value of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + "" + None + """ + return self._get("issuAllowed") + + @property + def last_upg_action(self): + """ + Return the last upgrade action performed on the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + Never + None + """ + return self._get("lastUpgAction") + + @property + def mds(self): + """ + Return whether the switch with ip_address is an MSD, if it exists. + Return None otherwise + + Possible values: + Boolean (True or False) + None + """ + return self.make_boolean(self._get("mds")) + + @property + def mode(self): + """ + Return the ISSU mode of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + "Normal" + None + """ + return self._get("mode") + + @property + def model(self): + """ + Return the model of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + model number e.g. "N9K-C93180YC-EX" + None + """ + return self._get("model") + + @property + def model_type(self): + """ + Return the model type of the switch with + ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("modelType") + + @property + def peer(self): + """ + Return the peer of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + ?? TODO:3 check this + None + """ + return self._get("peer") + + @property + def platform(self): + """ + Return the platform of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + platform, e.g. "N9K" + None + """ + return self._get("platform") + + @property + def policy(self): + """ + Return the policy attached to the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + policy name, e.g. "NR3F" + None + """ + return self._get("policy") + + @property + def reason(self): + """ + Return the reason (?) of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Compliance + Validate + Upgrade + None + """ + return self._get("reason") + + @property + def role(self): + """ + Return the role of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + switch role, e.g. "leaf" + None + """ + return self._get("role") + + @property + def serial_number(self): + """ + Return the serialNumber of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + switch serial number, e.g. "AB1234567CD" + None + """ + return self._get("serialNumber") + + @property + def status(self): + """ + Return the sync status of the switch with ip_address, if it exists. + Return None otherwise + + Details: The sync status is the status of the switch with respect + to the image policy. If the switch is in sync with the image policy, + the status is "In-Sync". If the switch is out of sync with the image + policy, the status is "Out-Of-Sync". + + Possible values: + "In-Sync" + "Out-Of-Sync" + None + """ + return self._get("status") + + @property + def status_percent(self): + """ + Return the upgrade (TODO:3 verify this) percentage completion + of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("statusPercent") + + @property + def sys_name(self): + """ + Return the system name of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + system name, e.g. "cvd-1312-leaf" + None + """ + return self._get("sys_name") + + @property + def system_mode(self): + """ + Return the system mode of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + "Maintenance" (TODO:3 verify this) + "Normal" + None + """ + return self._get("systemMode") + + @property + def upgrade(self): + """ + Return the upgrade status of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + Success + In-Progress + None + """ + return self._get("upgrade") + + @property + def upg_groups(self): + """ + Return the upgGroups (upgrade groups) of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + upgrade group to which the switch belongs e.g. "LEAFS" + None + """ + return self._get("upgGroups") + + @property + def upgrade_percent(self): + """ + Return the upgrade percent complete of the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("upgradePercent") + + @property + def validated(self): + """ + Return the validation status of the switch with ip_address, + if it exists. + Return None otherwise + + Possible values: + Failed + Success + None + """ + return self._get("validated") + + @property + def validated_percent(self): + """ + Return the validation percent complete of the switch + with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer in range 0-100 + None + """ + return self._get("validatedPercent") + + @property + def vdc_id(self): + """ + Return the vdcId of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer + None + """ + return self._get("vdcId") + + @property + def vdc_id2(self): + """ + Return the vdc_id of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + Integer (negative values are valid) + None + """ + return self._get("vdc_id") + + @property + def version(self): + """ + Return the version of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + version, e.g. "10.3(2)" + None + """ + return self._get("version") + + @property + def vpc_peer(self): + """ + Return the vpcPeer of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + vpc peer e.g.: 10.1.1.1 + None + """ + return self._get("vpcPeer") + + @property + def vpc_role(self): + """ + Return the vpcRole of the switch with ip_address, if it exists. + Return None otherwise + + Possible values: + vpc role e.g.: + "primary" + "secondary" + "none" -> This will be translated to None + "none established" (TODO:3 verify this) + "primary, operational secondary" (TODO:3 verify this) + None + """ + return self._get("vpcRole") + + +class NdfcSwitchIssuDetailsByIpAddress(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by ip address. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsByIpAddress(module) + instance.refresh() + instance.ip_address = 10.1.1.1 + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + serial_number = instance.serial_number + etc... + + See NdfcSwitchIssuDetails for more details. + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["ip_address"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh ip_address current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + msg = f"{self.class_name}.refresh: " + msg += f"switch {switch}" + self.data_subclass[switch["ipAddress"]] = switch + + def _get(self, item): + if self.ip_address is None: + msg = f"{self.class_name}: set instance.ip_address " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.ip_address) is None: + msg = f"{self.class_name}: {self.ip_address} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.ip_address].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.ip_address. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.ip_address) + + @property + def ip_address(self): + """ + Set the ip_address of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("ip_address") + + @ip_address.setter + def ip_address(self, value): + self.properties["ip_address"] = value + + +class NdfcSwitchIssuDetailsBySerialNumber(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by serial_number. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsBySerialNumber(module) + instance.refresh() + instance.serial_number = "FDO211218GC" + instance.refresh() + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + ip_address = instance.ip_address + etc... + + See NdfcSwitchIssuDetails for more details. + + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["serial_number"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh serial_number current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + self.data_subclass[switch["serialNumber"]] = switch + + def _get(self, item): + if self.serial_number is None: + msg = f"{self.class_name}: set instance.serial_number " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.serial_number) is None: + msg = f"{self.class_name}: {self.serial_number} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.serial_number].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.serial_number. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.serial_number) + + @property + def serial_number(self): + """ + Set the serial_number of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("serial_number") + + @serial_number.setter + def serial_number(self, value): + self.properties["serial_number"] = value + + +class NdfcSwitchIssuDetailsByDeviceName(NdfcSwitchIssuDetails): + """ + Retrieve switch issu details from NDFC and provide property accessors + for the switch attributes retrieved by device_name. + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcSwitchIssuDetailsByDeviceName(module) + instance.refresh() + instance.device_name = "leaf_1" + image_staged = instance.image_staged + image_upgraded = instance.image_upgraded + ip_address = instance.ip_address + etc... + + See NdfcSwitchIssuDetails for more details. + + """ + + def __init__(self, module): + super().__init__(module) + self._init_properties() + + def _init_properties(self): + super()._init_properties() + self.properties["device_name"] = None + + def refresh(self): + """ + Caller: __init__() + + Refresh device_name current issu details from NDFC + """ + super().refresh() + self.data_subclass = {} + for switch in self.ndfc_data: + self.data_subclass[switch["deviceName"]] = switch + + def _get(self, item): + if self.device_name is None: + msg = f"{self.class_name}: set instance.device_name " + msg += f"before accessing property {item}." + self.module.fail_json(msg) + if self.data_subclass.get(self.device_name) is None: + msg = f"{self.class_name}: {self.device_name} is not " + msg += f"defined in NDFC." + self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.device_name].get(item)) + + @property + def filtered_data(self): + """ + Return a dictionary of the switch matching self.device_name. + Return None of the switch does not exist in NDFC. + """ + return self.data_subclass.get(self.device_name) + + @property + def device_name(self): + """ + Set the device_name of the switch to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("device_name") + + @device_name.setter + def device_name(self, value): + self.properties["device_name"] = value + + +class NdfcImageStage(NdfcAnsibleImageUpgradeCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image + + Verb: POST + + Usage (where module is an instance of AnsibleModule): + + stage = NdfcImageStage(module) + stage.serial_numbers = ["FDO211218HH", "FDO211218GC"] + stage.commit() + data = stage.data + + Request body (12.1.2e) (yes, serialNum is misspelled): + { + "sereialNum": [ + "FDO211218HH", + "FDO211218GC" + ] + } + Request body (12.1.3b): + { + "serialNumbers": [ + "FDO211218HH", + "FDO211218GC" + ] + } + + Response: + Unfortunately, the response does not contain consistent data. + Would be better if all responses contained serial numbers as keys so that + we could verify against a set() of serial numbers. Sigh. It is what it is. + { + 'RETURN_CODE': 200, + 'METHOD': 'POST', + 'REQUEST_PATH': 'https: //172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image', + 'MESSAGE': 'OK', + 'DATA': [ + { + 'key': 'success', + 'value': '' + }, + { + 'key': 'success', + 'value': '' + } + ] + } + + Response when there are no files to stage: + [ + { + "key": "FDO211218GC", + "value": "No files to stage" + }, + { + "key": "FDO211218HH", + "value": "No files to stage" + } + ] + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + self.serial_numbers_done = set() + self.ndfc_version = None + self.path = None + self.verb = None + self.payload = None + self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + + def _init_properties(self): + self.properties = {} + self.properties["serial_numbers"] = None + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + + def _populate_ndfc_version(self): + """ + Populate self.ndfc_version with the NDFC version. + + Notes: + 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + imports resulting in RecursionError + """ + instance = NdfcVersion(self.module) + instance.refresh() + self.ndfc_version = instance.version + + def prune_serial_numbers(self): + """ + If the image is already staged on a switch, remove that switch's + serial number from the list of serial numbers to stage. + """ + serial_numbers = copy.copy(self.serial_numbers) + for serial_number in serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.image_staged == "Success": + msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " + msg += "image already staged for " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}." + self.log_msg(msg) + self.serial_numbers.remove(serial_number) + + def validate_serial_numbers(self): + """ + Fail if the image_staged state for any serial_number + is Failed. + """ + for serial_number in self.serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.image_staged == "Failed": + msg = "Image staging is failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += f"Check the switch connectivity to NDFC " + msg += "and try again." + self.module.fail_json(msg) + else: + self.log_msg(f"Image staging is not failing for the following switch: {self.issu_detail.serial_number}") + + def commit(self): + """ + Commit the image staging request to NDFC and wait + for the images to be staged. + """ + if self.serial_numbers is None: + msg = f"{self.class_name}.commit() call instance.serial_numbers " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.serial_numbers) == 0: + msg = f"REMOVE: {self.class_name}.commit() no serial numbers to stage." + self.log_msg(msg) + return + self.prune_serial_numbers() + self.validate_serial_numbers() + self._wait_for_current_actions_to_complete() + + self.path = self.endpoints.image_stage.get("path") + self.verb = self.endpoints.image_stage.get("verb") + + self.log_msg(f"REMOVE: {self.class_name}.commit() path: {self.path}") + self.log_msg(f"REMOVE: {self.class_name}.commit() verb: {self.verb}") + self.payload = {} + self._populate_ndfc_version() + if self.ndfc_version == "12.1.2e": + # Yes, NDFC 12.1.2e wants serialNum to be misspelled + self.payload["sereialNum"] = self.serial_numbers + else: + self.payload["serialNumbers"] = self.serial_numbers + self.properties["ndfc_response"] = dcnm_send( + self.module, self.verb, self.path, data=json.dumps(self.payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, self.verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_stage_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not stage an image if there are any actions in progress. + Wait for all actions to complete before staging image. + Actions include image staging, image upgrade, and image validation. + """ + self.serial_numbers_done = set() + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + timeout = self.check_timeout + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} actions in progress: " + msg += f"{self.issu_detail.actions_in_progress}, " + msg += f"{timeout} seconds remaining." + self.log_msg(msg) + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"Timed out waiting for actions to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + def _wait_for_image_stage_to_complete(self): + """ + # Wait for image stage to complete + """ + # We're promoting serial_numbers_done to a class-level attribute + # so that it can be used in unit test asserts. + self.serial_numbers_done = set() + timeout = self.check_timeout + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" + self.log_msg(msg) + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + staged_percent = self.issu_detail.image_staged_percent + staged_status = self.issu_detail.image_staged + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Seconds remaining {timeout}: " + msg += f"{device_name}, {serial_number}, {ip_address} " + msg += f"image staged percent: {staged_percent}" + self.log_msg(msg) + + if staged_status == "Failed": + msg = f"Seconds remaining {timeout}: stage image failed " + msg += f"for {device_name}, {serial_number}, {ip_address}. " + msg += f"image staged percent: {staged_percent}" + self.module.fail_json(msg) + + if staged_status == "Success": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Seconds remaining {timeout}: stage image complete for " + msg += f"for {device_name}, {serial_number}, {ip_address}. " + msg += f"image staged percent: {staged_percent}" + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + + # if staged_status == None: + # msg = f"REMOVE: {self.class_name}." + # msg += "_wait_for_image_stage_to_complete: " + # msg += f"Seconds remaining {timeout}: stage image " + # msg += "not started for " + # msg += f"{device_name}, {serial_number}, {ip_address}. " + # msg += f"image staged percent: {staged_percent}" + # self.log_msg(msg) + + # if staged_status == "In Progress": + # msg = f"REMOVE: {self.class_name}." + # msg += "_wait_for_image_stage_to_complete: " + # msg += f"Seconds remaining {timeout}: stage image " + # msg += f"{staged_status} for " + # msg += f"{device_name}, {serial_number}, {ip_address}. " + # msg += f"image staged percent: {staged_percent}" + # self.log_msg(msg) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_image_stage_to_complete: " + msg += f"Timed out waiting for image stage to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to stage. + + This must be set before calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.__class__.__name__}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + + @property + def ndfc_data(self): + """ + Return the result of the image staging request + for serial_numbers. + + instance.serial_numbers must be set first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def check_interval(self): + """ + Return the stage check interval in seconds + """ + return self.properties.get("check_interval") + + @check_interval.setter + def check_interval(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_interval must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_interval"] = value + + @property + def check_timeout(self): + """ + Return the stage check timeout in seconds + """ + return self.properties.get("check_timeout") + + @check_timeout.setter + def check_timeout(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_timeout must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_timeout"] = value + +# ============================================================================== +class NdfcImageValidate(NdfcAnsibleImageUpgradeCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image + + Verb: POST + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcImageValidate(module) + instance.serial_numbers = ["FDO211218HH", "FDO211218GC"] + # non_disruptive is optional + instance.non_disruptive = True + instance.commit() + data = instance.ndfc_data + + Request body: + { + "serialNum": ["FDO21120U5D"], + "nonDisruptive":"true" + } + + Response body when nonDisruptive is True: + [StageResponse [key=success, value=]] + + Response body when nonDisruptive is False: + [StageResponse [key=success, value=]] + + The response is not JSON, nor is it very useful. + Instead, we poll for validation status using + NdfcSwitchIssuDetailsBySerialNumber. + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + + def _init_properties(self): + self.properties = {} + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["non_disruptive"] = False + self.properties["serial_numbers"] = None + + def _populate_ndfc_version(self): + """ + Populate self.ndfc_version with the NDFC version. + + TODO:3 Remove if 12.1.3b works with no changes to request/response payloads. + + Notes: + 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + imports resulting in RecursionError + """ + instance = NdfcVersion(self.module) + instance.refresh() + self.ndfc_version = instance.version + + def prune_serial_numbers(self): + """ + If the image is already validated on a switch, remove that switch's + serial number from the list of serial numbers to validate. + """ + serial_numbers = copy.copy(self.serial_numbers) + for serial_number in serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.validated == "Success": + msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " + msg += "image already validated for " + msg += f"{self.issu_detail.serial_number}, " + msg += f"{self.issu_detail.ip_address}" + self.log_msg(msg) + self.serial_numbers.remove(self.issu_detail.serial_number) + + def validate_serial_numbers(self): + """ + Log a warning if the validated state for any serial_number + is Failed. + + TODO:1 Need a way to compare current image_policy with the image policy in the response + TODO:3 If validate == Failed, it may have been from the last operation. + TODO:3 We can't fail here based on this until we can verify the failure is happening for the current image_policy. + TODO:3 Change this to a log message and update the unit test if we can't verify the failure is happening for the current image_policy. + """ + for serial_number in self.serial_numbers: + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + if self.issu_detail.validated == "Failed": + msg = f"{self.class_name}.validate_serial_numbers: " + msg += "image validation is failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += "If this persists, check the switch connectivity to NDFC and " + msg += "try again." + #self.log_msg(msg) + self.module.fail_json(msg) + + def build_payload(self): + self.payload = {} + self.payload["serialNum"] = self.serial_numbers + self.payload["nonDisruptive"] = self.non_disruptive + + def commit(self): + """ + Commit the image validation request to NDFC and wait + for the images to be validated. + """ + if self.serial_numbers is None: + msg = f"{self.class_name}.commit() call instance.serial_numbers " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.serial_numbers) == 0: + msg = f"REMOVE: {self.class_name}.commit() no serial numbers " + msg += "to validate." + self.log_msg(msg) + return + self.prune_serial_numbers() + self.validate_serial_numbers() + self._wait_for_current_actions_to_complete() + path = self.endpoints.image_validate.get("path") + verb = self.endpoints.image_validate.get("verb") + self.build_payload() + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_validate_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not validate an image if there are any actions in progress. + Wait for all actions to complete before validating image. + Actions include image staging, image upgrade, and image validation. + """ + self.serial_numbers_done = set() + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + timeout = self.check_timeout + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} actions in progress: " + msg += f"{self.issu_detail.actions_in_progress}, " + msg += f"{timeout} seconds remaining." + self.log_msg(msg) + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{serial_number} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"Timed out waiting for actions to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + def _wait_for_image_validate_to_complete(self): + """ + Wait for image validation to complete + """ + # We're promiting serial_numbers_done to a class-level attribute + # so that it can be used in unit test asserts. + self.serial_numbers_done = set() + timeout = self.check_timeout + serial_numbers_todo = set(copy.copy(self.serial_numbers)) + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"seconds remaining: {timeout}, " + msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" + self.log_msg(msg) + for serial_number in self.serial_numbers: + if serial_number in self.serial_numbers_done: + continue + self.issu_detail.serial_number = serial_number + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + validated_percent = self.issu_detail.validated_percent + validated_status = self.issu_detail.validated + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"validated_percent: {validated_percent} " + msg += f"validated_state: {validated_status}" + self.log_msg(msg) + + if validated_status == "Failed": + msg = f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}. " + msg += "Check the switch e.g. show install log detail, " + msg += "show incompatibility-all nxos . Or " + msg += "check NDFC Operations > Image Management > " + msg += "Devices > View Details > Validate for " + msg += "more details." + self.module.fail_json(msg) + + if validated_status == "Success": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + self.serial_numbers_done.add(serial_number) + + if validated_status == None: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += "not started for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + + if validated_status == "In Progress": + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Seconds remaining {timeout}: validate image " + msg += f"{validated_status} for " + msg += f"{device_name}, {ip_address}, {serial_number}, " + msg += f"image validated percent: {validated_percent}" + self.log_msg(msg) + if self.serial_numbers_done != serial_numbers_todo: + msg = f"{self.class_name}." + msg += "_wait_for_image_validate_to_complete: " + msg += f"Timed out waiting for image validation to complete. " + msg += f"serial_numbers_done: " + msg += f"{','.join(sorted(self.serial_numbers_done))}, " + msg += f"serial_numbers_todo: " + msg += f"{','.join(sorted(serial_numbers_todo))}" + self.log_msg(msg) + self.module.fail_json(msg) + + @property + def serial_numbers(self): + """ + Set the serial numbers of the switches to stage. + + This must be set before calling instance.commit() + """ + return self.properties.get("serial_numbers") + + @serial_numbers.setter + def serial_numbers(self, value): + if not isinstance(value, list): + msg = f"{self.__class__.__name__}: instance.serial_numbers must " + msg += f"be a python list of switch serial numbers." + self.module.fail_json(msg) + self.properties["serial_numbers"] = value + + @property + def non_disruptive(self): + """ + Set the non_disruptive flag to True or False. + """ + return self.properties.get("non_disruptive") + + @non_disruptive.setter + def non_disruptive(self, value): + value = self.make_boolean(value) + if not isinstance(value, bool): + msg = f"{self.class_name}.non_disruptive: " + msg += "instance.non_disruptive must " + msg += f"be a boolean. Got {value}." + self.module.fail_json(msg) + self.properties["non_disruptive"] = value + + @property + def ndfc_data(self): + """ + Return the result of the image staging request + for serial_numbers. + + instance.serial_numbers must be set first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def check_interval(self): + """ + Return the validate check interval in seconds + """ + return self.properties.get("check_interval") + + @check_interval.setter + def check_interval(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_interval must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_interval"] = value + + @property + def check_timeout(self): + """ + Return the validate check timeout in seconds + """ + return self.properties.get("check_timeout") + + @check_timeout.setter + def check_timeout(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_timeout must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_timeout"] = value + + +# ============================================================================== +class NdfcImageUpgrade(NdfcAnsibleImageUpgradeCommon): + """ + Endpoint: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image + Verb: POST + + TODO:3 Discuss with Mike/Shangxin. NdfcImageUpgrade.epld_upgrade, etc + + Usage (where module is an instance of AnsibleModule): + + upgrade = NdfcImageUpgrade(module) + upgrade.devices = devices + upgrade.commit() + data = upgrade.data + + Where devices is a list of dict. Example structure: + + [ + { + 'policy': 'KR3F', + 'ip_address': '172.22.150.102', + 'policy_changed': False + 'stage': False, + 'validate': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'nxos': { + 'mode': 'non_disruptive' + 'bios_force': False + }, + 'epld': { + 'module': 'ALL', + 'golden': False + }, + 'reboot': { + 'config_reload': False, + 'write_erase': False + }, + 'package': { + 'install': False, + 'uninstall': False + } + }, + }, + etc... + ] + + Request body: + Yes, the keys below are misspelled in the request body: + pacakgeInstall + pacakgeUnInstall + + { + "devices": [ + { + "serialNumber": "FDO211218HH", + "policyName": "NR1F" + } + ], + "issuUpgrade": true, + "issuUpgradeOptions1": { + "nonDisruptive": true, + "forceNonDisruptive": false, + "disruptive": false + }, + "issuUpgradeOptions2": { + "biosForce": false + }, + "epldUpgrade": false, + "epldOptions": { + "moduleNumber": "ALL", + "golden": false + }, + "reboot": false, + "rebootOptions": { + "configReload": "false", + "writeErase": "false" + }, + "pacakgeInstall": false, + "pacakgeUnInstall": false + } + Response bodies: + Responses are text, not JSON, and are returned immediately. + They do not contain useful information. We need to poll NDFC + to determine when the upgrade is complete. Basically, we ignore + these responses in favor of the poll responses. + - If an action is in progress, text is returned: + "Action in progress for some of selected device(s). Please try again after completing current action." + - If an action is not in progress, text is returned: + "3" + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + # Maximum number of modules/linecards in a switch + self.max_module_number = 9 + + self._init_defaults() + self._init_properties() + self.issu_detail = NdfcSwitchIssuDetailsByIpAddress(self.module) + + def _init_defaults(self): + self.defaults = {} + self.defaults["reboot"] = False + self.defaults["stage"] = True + self.defaults["validate"] = True + self.defaults["upgrade"] = {} + self.defaults["upgrade"]["nxos"] = True + self.defaults["upgrade"]["epld"] = False + self.defaults["options"] = {} + self.defaults["options"]["nxos"] = {} + self.defaults["options"]["nxos"]["mode"] = "disruptive" + self.defaults["options"]["nxos"]["bios_force"] = False + self.defaults["options"]["epld"] = {} + self.defaults["options"]["epld"]["module"] = "ALL" + self.defaults["options"]["epld"]["golden"] = False + self.defaults["options"]["reboot"] = {} + self.defaults["options"]["reboot"]["config_reload"] = False + self.defaults["options"]["reboot"]["write_erase"] = False + self.defaults["options"]["package"] = {} + self.defaults["options"]["package"]["install"] = False + self.defaults["options"]["package"]["uninstall"] = False + + def _init_properties(self): + # self.ip_addresses is used in: + # self._wait_for_current_actions_to_complete() + # self._wait_for_image_upgrade_to_complete() + self.ip_addresses = set() + # TODO:1 Review these properties since we are no longer + # calling this class per-switch given the payload structure + # is not amenable to that. + self.properties = {} + self.properties["bios_force"] = False + self.properties["check_interval"] = 10 # seconds + self.properties["check_timeout"] = 1800 # seconds + self.properties["config_reload"] = False + self.properties["devices"] = None + self.properties["disruptive"] = True + self.properties["epld_golden"] = False + self.properties["epld_module"] = "ALL" + self.properties["epld_upgrade"] = False + self.properties["force_non_disruptive"] = False + self.properties["ndfc_data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + self.properties["non_disruptive"] = False + self.properties["package_install"] = False + self.properties["package_uninstall"] = False + self.properties["reboot"] = False + self.properties["write_erase"] = False + + self.valid_epld_module = set() + self.valid_epld_module.add("ALL") + for module in range(1, self.max_module_number + 1): + self.valid_epld_module.add(str(module)) + + self.valid_nxos_mode = set() + self.valid_nxos_mode.add("disruptive") + self.valid_nxos_mode.add("non_disruptive") + self.valid_nxos_mode.add("force_non_disruptive") + + + # def prune_devices(self): + # """ + # If the image is already upgraded on a device, remove that device + # from self.devices. self.devices dict has already been validated, + # so no further error checking is needed here. + + # TODO:1 This prunes devices only based on the image upgrade state. + # TODO:1 It does not check other image states and EPLD states. + # """ + # # issu = NdfcSwitchIssuDetailsBySerialNumber(self.module) + # pruned_devices = set() + # instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + # instance.refresh() + # for device in self.devices: + # msg = f"REMOVE: {self.class_name}.prune_devices() device: {device}" + # self.log_msg(msg) + # instance.ip_address = device.get("ip_address") + # instance.refresh() + # if instance.upgrade == "Success": + # msg = f"REMOVE: {self.class_name}.prune_devices: " + # msg = "image already upgraded for " + # msg += f"{instance.device_name}, " + # msg += f"{instance.serial_number}, " + # msg += f"{instance.ip_address}" + # self.log_msg(msg) + # pruned_devices.add(instance.ip_address) + # self.devices = [ + # device + # for device in self.devices + # if device.get("ip_address") not in pruned_devices + # ] + + def validate_devices(self): + """ + Fail if the upgrade state for any device is Failed. + """ + for device in self.devices: + self.issu_detail.ip_address = device.get("ip_address") + self.issu_detail.refresh() + if self.issu_detail.upgrade == "Failed": + msg = f"{self.class_name}.validate_devices: Image upgrade is " + msg += "failing for the following switch: " + msg += f"{self.issu_detail.device_name}, " + msg += f"{self.issu_detail.ip_address}, " + msg += f"{self.issu_detail.serial_number}. " + msg += "Please check the switch " + msg += "to determine the cause and try again." + self.module.fail_json(msg) + # used in self._wait_for_current_actions_to_complete() + self.ip_addresses.add(self.issu_detail.ip_address) + + def _merge_defaults_to_switch_config(self, config): + if config.get("stage") is None: + config["stage"] = self.defaults["stage"] + if config.get("reboot") is None: + config["reboot"] = self.defaults["reboot"] + if config.get("validate") is None: + config["validate"] = self.defaults["validate"] + if config.get("upgrade") is None: + config["upgrade"] = self.defaults["upgrade"] + if config.get("upgrade").get("nxos") is None: + config["upgrade"]["nxos"] = self.defaults["upgrade"]["nxos"] + if config.get("upgrade").get("epld") is None: + config["upgrade"]["epld"] = self.defaults["upgrade"]["epld"] + if config.get("options") is None: + config["options"] = self.defaults["options"] + if config["options"].get("nxos") is None: + config["options"]["nxos"] = self.defaults["options"]["nxos"] + if config["options"]["nxos"].get("mode") is None: + config["options"]["nxos"]["mode"] = self.defaults["options"]["nxos"]["mode"] + if config["options"]["nxos"].get("bios_force") is None: + config["options"]["nxos"]["bios_force"] = self.defaults["options"]["nxos"]["bios_force"] + if config["options"].get("epld") is None: + config["options"]["epld"] = self.defaults["options"]["epld"] + if config["options"]["epld"].get("module") is None: + config["options"]["epld"]["module"] = self.defaults["options"]["epld"]["module"] + if config["options"]["epld"].get("golden") is None: + config["options"]["epld"]["golden"] = self.defaults["options"]["epld"]["golden"] + if config["options"].get("reboot") is None: + config["options"]["reboot"] = self.defaults["options"]["reboot"] + if config["options"]["reboot"].get("config_reload") is None: + config["options"]["reboot"]["config_reload"] = self.defaults["options"]["reboot"]["config_reload"] + if config["options"]["reboot"].get("write_erase") is None: + config["options"]["reboot"]["write_erase"] = self.defaults["options"]["reboot"]["write_erase"] + if config["options"].get("package") is None: + config["options"]["package"] = self.defaults["options"]["package"] + if config["options"]["package"].get("install") is None: + config["options"]["package"]["install"] = self.defaults["options"]["package"]["install"] + if config["options"]["package"].get("uninstall") is None: + config["options"]["package"]["uninstall"] = self.defaults["options"]["package"]["uninstall"] + return config + + def build_payload(self, device): + """ + Build the request payload to upgrade the switches. + """ + msg = f"REMOVE: {self.class_name}.build_payload() PRE_DEFAULTS: " + msg += f"device: {device}" + self.log_msg(msg) + device = self._merge_defaults_to_switch_config(device) + msg = f"REMOVE: {self.class_name}.build_payload() POST_DEFAULTS: " + msg += f"device: {device}" + self.log_msg(msg) + + + # devices_to_upgrade must currently be a single device + devices_to_upgrade = [] + self.issu_detail.ip_address = device.get("ip_address") + self.issu_detail.refresh() + payload_device = {} + payload_device["serialNumber"] = self.issu_detail.serial_number + payload_device["policyName"] = device.get("policy") + devices_to_upgrade.append(payload_device) + + self.payload = {} + self.payload["devices"] = devices_to_upgrade + self.payload["issuUpgrade"] = device.get("upgrade").get("nxos") + + # nxos_mode: The choices for nxos_mode are mutually-exclusive. + # If one is set to True, the others must be False. + # nonDisruptive corresponds to NDFC Allow Non-Disruptive GUI option + self.payload["issuUpgradeOptions1"] = {} + self.payload["issuUpgradeOptions1"]["nonDisruptive"] = False + self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = False + self.payload["issuUpgradeOptions1"]["disruptive"] = False + + nxos_mode = device.get("options").get("nxos").get("mode") + if nxos_mode not in self.valid_nxos_mode: + msg = f"{self.class_name}.build_payload() options.nxos.mode must " + msg += f"be one of {self.valid_nxos_mode}. Got {nxos_mode}." + self.module.fail_json(msg) + if nxos_mode == "non_disruptive": + self.payload["issuUpgradeOptions1"]["nonDisruptive"] = True + if nxos_mode == "disruptive": + self.payload["issuUpgradeOptions1"]["disruptive"] = True + if nxos_mode == "force_non_disruptive": + self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True + + # biosForce corresponds to NDFC BIOS Force GUI option + bios_force = device.get("options").get("nxos").get("bios_force") + if not isinstance(bios_force, bool): + msg = f"{self.class_name}.build_payload() options.nxos.bios_force " + msg += f"must be a boolean. Got {bios_force}." + self.module.fail_json(msg) + self.payload["issuUpgradeOptions2"] = {} + self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force + + # EPLD + epld_module = device.get("options").get("epld").get("module") + epld_golden = device.get("options").get("epld").get("golden") + if epld_module not in self.valid_epld_module: + msg = f"{self.class_name}.build_payload() options.epld.module must " + msg += f"be one of {self.valid_epld_module}. Got {epld_module}." + self.module.fail_json(msg) + if not isinstance(epld_golden, bool): + msg = f"{self.class_name}.build_payload() options.epld.golden " + msg += f"must be a boolean. Got {epld_golden}." + self.module.fail_json(msg) + self.payload["epldUpgrade"] = device.get("upgrade").get("epld") + self.payload["epldOptions"] = {} + self.payload["epldOptions"]["moduleNumber"] = epld_module + self.payload["epldOptions"]["golden"] = epld_golden + + # Reboot + reboot = device.get("reboot") + if not isinstance(reboot, bool): + msg = f"{self.class_name}.build_payload() reboot must " + msg += f"be a boolean. Got {reboot}." + self.module.fail_json(msg) + self.payload["reboot"] = reboot + + # Reboot options + config_reload = device.get("options").get("reboot").get("config_reload") + write_erase = device.get("options").get("reboot").get("write_erase") + if not isinstance(config_reload, bool): + msg = f"{self.class_name}.build_payload() options.reboot.config_reload " + msg += f"must be a boolean. Got {config_reload}." + self.module.fail_json(msg) + if not isinstance(write_erase, bool): + msg = f"{self.class_name}.build_payload() options.reboot.write_erase " + msg += f"must be a boolean. Got {write_erase}." + self.module.fail_json(msg) + self.payload["rebootOptions"] = {} + self.payload["rebootOptions"]["configReload"] = config_reload + self.payload["rebootOptions"]["writeErase"] = write_erase + + # Packages + package_install = device.get("options").get("package").get("install") + package_uninstall = device.get("options").get("package").get("uninstall") + if not isinstance(package_install, bool): + msg = f"{self.class_name}.build_payload() options.package.install " + msg += f"must be a boolean. Got {package_install}." + self.module.fail_json(msg) + if not isinstance(package_uninstall, bool): + msg = f"{self.class_name}.build_payload() options.package.uninstall " + msg += f"must be a boolean. Got {package_uninstall}." + self.module.fail_json(msg) + self.payload["pacakgeInstall"] = package_install + self.payload["pacakgeUnInstall"] = package_uninstall + + def commit(self): + """ + Commit the image upgrade request to NDFC and wait + for the images to be upgraded. + """ + if self.devices is None: + msg = f"{self.class_name}.commit() call instance.devices " + msg += "before calling commit()." + self.module.fail_json(msg) + if len(self.devices) == 0: + msg = f"REMOVE: {self.class_name}.commit() no devices to upgrade." + self.log_msg(msg) + return + #self.prune_devices() + self.validate_devices() + self._wait_for_current_actions_to_complete() + path = self.endpoints.image_upgrade.get("path") + verb = self.endpoints.image_upgrade.get("verb") + for device in self.devices: + self.build_payload(device) + self.log_msg(f"REMOVE: {self.class_name}.commit() upgrade payload: {self.payload}") + self.properties["ndfc_response"] = dcnm_send( + self.module, verb, path, data=json.dumps(self.payload) + ) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.log_msg( + f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + ) + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") + if not self.ndfc_result["success"]: + msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " + msg += f"NDFC response was: {self.ndfc_response}" + self.module.fail_json(msg) + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self._wait_for_image_upgrade_to_complete() + + def _wait_for_current_actions_to_complete(self): + """ + NDFC will not upgrade an image if there are any actions in progress. + Wait for all actions to complete before upgrading image. + Actions include image staging, image upgrade, and image validation. + """ + ipv4_todo = copy.copy(self.ip_addresses) + timeout = self.check_timeout + while len(ipv4_todo) > 0 and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + for ipv4 in self.ip_addresses: + if ipv4 not in ipv4_todo: + continue + self.issu_detail.ip_address = ipv4 + self.issu_detail.refresh() + if self.issu_detail.actions_in_progress is False: + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{ipv4} no actions in progress. " + msg += f"OK to proceed. {timeout} seconds remaining." + self.log_msg(msg) + ipv4_todo.remove(ipv4) + continue + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_current_actions_to_complete: " + msg += f"{ipv4} actions in progress. " + msg += f"Waiting. {timeout} seconds remaining." + self.log_msg(msg) + + def _wait_for_image_upgrade_to_complete(self): + """ + Wait for image upgrade to complete + """ + ipv4_done = set() + timeout = self.check_timeout + ipv4_todo = set(copy.copy(self.ip_addresses)) + while ipv4_done != ipv4_todo and timeout > 0: + sleep(self.check_interval) + timeout -= self.check_interval + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"seconds remaining {timeout}, " + msg += f"ipv4_todo: {sorted(list(ipv4_todo))}" + self.log_msg(msg) + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"seconds remaining {timeout}, " + msg += f"ipv4_done: {sorted(list(ipv4_done))}" + self.log_msg(msg) + for ipv4 in self.ip_addresses: + if ipv4 in ipv4_done: + continue + self.issu_detail.ip_address = ipv4 + self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address + device_name = self.issu_detail.device_name + upgrade_percent = self.issu_detail.upgrade_percent + upgrade_status = self.issu_detail.upgrade + serial_number = self.issu_detail.serial_number + + if upgrade_status == "Failed": + msg = f"{self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"Seconds remaining {timeout}: upgrade image " + msg += f"{upgrade_status} for " + msg += f"{device_name}, {serial_number}, {ip_address}" + self.module.fail_json(msg) + + if upgrade_status == "Success": + ipv4_done.add(ipv4) + status = "succeeded" + if upgrade_status == None: + status = "not started" + if upgrade_status == "In-Progress": + status = "in progress" + + msg = f"REMOVE: {self.class_name}." + msg += "_wait_for_image_upgrade_to_complete: " + msg += f"Seconds remaining {timeout}, " + msg += f"Percent complete {upgrade_percent}, " + msg += f"Status {status}, " + msg += f"{device_name}, {serial_number}, {ip_address}" + self.log_msg(msg) + + if ipv4_done != ipv4_todo: + msg = f"{self.class_name}._wait_for_image_upgrade_to_complete(): " + msg += "The following device(s) did not complete upgrade: " + msg += f"{ipv4_todo.difference(ipv4_done)}. " + msg += "Try increasing issu timeout in the playbook, or check " + msg += "the device(s) to determine the cause " + msg += "(e.g. show install all status)." + self.module.fail_json(msg) + + # setter properties + @property + def bios_force(self): + """ + Set the bios_force flag to True or False. + + Default: False + """ + return self.properties.get("bios_force") + + @bios_force.setter + def bios_force(self, value): + name = "bios_force" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def config_reload(self): + """ + Set the config_reload flag to True or False. + + Default: False + """ + return self.properties.get("config_reload") + + @config_reload.setter + def config_reload(self, value): + name = "config_reload" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def devices(self): + """ + Set the devices to upgrade. + + list() of dict() with the following structure: + { + "serial_number": "FDO211218HH", + "policy_name": "NR1F" + } + + Must be set before calling instance.commit() + """ + return self.properties.get("devices") + + @devices.setter + def devices(self, value): + name = "devices" + if not isinstance(value, list): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a python list of dict." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def disruptive(self): + """ + Set the disruptive flag to True or False. + + Default: False + """ + return self.properties.get("disruptive") + + @disruptive.setter + def disruptive(self, value): + name = "disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_golden(self): + """ + Set the epld_golden flag to True or False. + + Default: False + """ + return self.properties.get("epld_golden") + + @epld_golden.setter + def epld_golden(self, value): + name = "epld_golden" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_upgrade(self): + """ + Set the epld_upgrade flag to True or False. + + Default: False + """ + return self.properties.get("epld_upgrade") + + @epld_upgrade.setter + def epld_upgrade(self, value): + name = "epld_upgrade" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def epld_module(self): + """ + Set the epld_module to upgrade. + + Ignored if epld_upgrade is set to False + Valid values: integer or "ALL" + Default: "ALL" + """ + return self.properties.get("epld_module") + + @epld_module.setter + def epld_module(self, value): + name = "epld_module" + try: + value = value.upper() + except AttributeError: + pass + if not isinstance(value, int) and value != "ALL": + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be an integer or 'ALL'" + self.module.fail_json(msg) + self.properties[name] = value + + @property + def force_non_disruptive(self): + """ + Set the force_non_disruptive flag to True or False. + + Default: False + """ + return self.properties.get("force_non_disruptive") + + @force_non_disruptive.setter + def force_non_disruptive(self, value): + name = "force_non_disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def non_disruptive(self): + """ + Set the non_disruptive flag to True or False. + + Default: True + """ + return self.properties.get("non_disruptive") + + @non_disruptive.setter + def non_disruptive(self, value): + name = "non_disruptive" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def package_install(self): + """ + Set the package_install flag to True or False. + + Default: False + """ + return self.properties.get("package_install") + + @package_install.setter + def package_install(self, value): + name = "package_install" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def package_uninstall(self): + """ + Set the package_uninstall flag to True or False. + + Default: False + """ + return self.properties.get("package_uninstall") + + @package_uninstall.setter + def package_uninstall(self, value): + name = "package_uninstall" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def reboot(self): + """ + Set the reboot flag to True or False. + + Default: False + """ + return self.properties.get("reboot") + + @reboot.setter + def reboot(self, value): + name = "reboot" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + @property + def write_erase(self): + """ + Set the write_erase flag to True or False. + + Default: False + """ + return self.properties.get("write_erase") + + @write_erase.setter + def write_erase(self, value): + name = "write_erase" + if not isinstance(value, bool): + msg = f"{self.class_name}.{name}.setter: " + msg += f"instance.{name} must be a boolean." + self.module.fail_json(msg) + self.properties[name] = value + + + # getter properties + @property + def check_interval(self): + """ + Return the image upgrade check interval in seconds + """ + return self.properties.get("check_interval") + + @property + def check_timeout(self): + """ + Return the image upgrade check timeout in seconds + """ + return self.properties.get("check_timeout") + + @property + def ndfc_data(self): + """ + Return the data retrieved from NDFC for the image upgrade request. + + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the POST result from NDFC + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the POST response from NDFC + instance.devices must be set first. + instance.commit() must be called first. + """ + return self.properties.get("ndfc_response") + + @property + def serial_numbers(self): + """ + Return a list of serial numbers from self.devices + """ + return [device.get("serial_number") for device in self.devices] + + +class NdfcVersion(NdfcAnsibleImageUpgradeCommon): + """ + Return image version information from NDFC + + NOTES: + 1. considered using dcnm_version_supported() but it does not return + minor release info, which is needed due to key changes between + 12.1.2e and 12.1.3b. For example, see NdfcImageStage().commit() + + Endpoint: + /appcenter/cisco/ndfc/api/v1/fm/about/version + + Usage (where module is an instance of AnsibleModule): + + instance = NdfcVersion(module) + instance.refresh() + if instance.version == "12.1.2e": + do 12.1.2e stuff + else: + do other stuff + + Response: + { + "version": "12.1.2e", + "mode": "LAN", + "isMediaController": false, + "dev": false, + "isHaEnabled": false, + "install": "EASYFABRIC", + "uuid": "f49e6088-ad4f-4406-bef6-2419de914ff1", + "is_upgrade_inprogress": false + } + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = self.__class__.__name__ + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["data"] = None + self.properties["ndfc_result"] = None + self.properties["ndfc_response"] = None + + def refresh(self): + """ + Refresh self.ndfc_data with current version info from NDFC + """ + path = self.endpoints.ndfc_version.get("path") + verb = self.endpoints.ndfc_version.get("verb") + self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) + self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_response: {self.ndfc_response}" + self.log_msg(msg) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_result: {self.ndfc_result}" + self.log_msg(msg) + + if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: + msg = f"{self.class_name}.refresh() failed: {self.ndfc_result}" + self.module.fail_json(msg) + + self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + if self.ndfc_data is None: + msg = f"{self.class_name}.refresh() failed: NDFC response " + msg += "does not contain DATA key. NDFC response: " + msg += f"{self.ndfc_response}" + self.module.fail_json(msg) + + msg = f"REMOVE: {self.class_name}.refresh() ndfc_data: {self.ndfc_data}" + self.log_msg(msg) + + def _get(self, item): + return self.make_boolean(self.make_none(self.ndfc_data.get(item))) + + @property + def dev(self): + """ + Return True if NDFC is a development release. + Return False if NDFC is not a development release. + Return None otherwise + + Possible values: + True + False + None + """ + return self._get("dev") + + @property + def install(self): + """ + Return the value of install, if it exists. + Return None otherwise + + Possible values: + EASYFABRIC + (probably other values) + None + """ + return self._get("install") + + @property + def is_ha_enabled(self): + """ + Return True if NDFC is a media controller. + Return False if NDFC is not a media controller. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("isHaEnabled")) + + @property + def is_media_controller(self): + """ + Return True if NDFC is a media controller. + Return False if NDFC is not a media controller. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("isMediaController")) + + @property + def is_upgrade_inprogress(self): + """ + Return True if an NDFC upgrade is in progress. + Return False if an NDFC upgrade is not in progress. + Return None otherwise + + Possible values: + True + False + None + """ + return self.make_boolean(self._get("is_upgrade_inprogress")) + + @property + def ndfc_data(self): + """ + Return the data retrieved from the request + """ + return self.properties.get("ndfc_data") + + @property + def ndfc_result(self): + """ + Return the GET result from NDFC + """ + return self.properties.get("ndfc_result") + + @property + def ndfc_response(self): + """ + Return the GET response from NDFC + """ + return self.properties.get("ndfc_response") + + @property + def mode(self): + """ + Return the NDFC mode, if it exists. + Return None otherwise + + Possible values: + LAN + None + """ + return self._get("mode") + + @property + def uuid(self): + """ + Return the value of uuid, if it exists. + Return None otherwise + + Possible values: + uuid e.g. "f49e6088-ad4f-4406-bef6-2419de914df1" + None + """ + return self._get("uuid") + + @property + def version(self): + """ + Return the NDFC version, if it exists. + Return None otherwise + + Possible values: + version, e.g. "12.1.2e" + None + """ + return self._get("version") + + @property + def version_major(self): + """ + Return the NDFC major version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 12 + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[0] + + @property + def version_minor(self): + """ + Return the NDFC minor version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 1 + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[1] + + @property + def version_patch(self): + """ + Return the NDFC minor version, if it exists. + Return None otherwise + + We are assuming semantic versioning based on: + https://semver.org + + Possible values: + if version is 12.1.2e, return 2e + None + """ + if self.version is None: + return None + return (self._get("version").split("."))[2] + + +def main(): + """main entry point for module execution""" + + element_spec = dict( + config=dict(required=True, type="dict"), + state=dict(default="merged", choices=["merged", "deleted", "query"]), + ) + + module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) + dcnm_module = NdfcAnsibleImageUpgrade(module) + dcnm_module.validate_input() + dcnm_module.get_have() + dcnm_module.get_want() + + if module.params["state"] == "merged": + dcnm_module.get_need_merged() + elif module.params["state"] == "deleted": + dcnm_module.get_need_deleted() + elif module.params["state"] == "query": + dcnm_module.get_need_query() + + if module.params["state"] == "query": + dcnm_module.result["changed"] = False + if module.params["state"] in ["merged", "deleted"]: + if dcnm_module.need: + dcnm_module.result["changed"] = True + else: + module.exit_json(**dcnm_module.result) + + if module.check_mode: + dcnm_module.result["changed"] = False + module.exit_json(**dcnm_module.result) + + if dcnm_module.need: + if module.params["state"] == "merged": + dcnm_module.handle_merged_state() + elif module.params["state"] == "deleted": + dcnm_module.handle_deleted_state() + elif module.params["state"] == "query": + dcnm_module.handle_query_state() + + module.exit_json(**dcnm_module.result) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py index c83d2f467..9e0b6f273 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py @@ -3,8 +3,10 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( - NdfcAnsibleImageUpgradeCommon, NdfcEndpoints) +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# NdfcAnsibleImageUpgradeCommon, NdfcEndpoints) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints from .fixture import load_fixture @@ -23,8 +25,8 @@ def fail_json(msg) -> dict: @pytest.fixture def module(): - return NdfcAnsibleImageUpgradeCommon(MockAnsibleModule) - + # return NdfcAnsibleImageUpgradeCommon(MockAnsibleModule) + return NdfcCommon(MockAnsibleModule) def responses_ndfc_ansible_image_upgrade_common(key: str) -> Dict[str, str]: response_file = f"dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon" @@ -42,8 +44,9 @@ def test_init_(module) -> None: assert module.params == {} assert module.debug == True assert module.fd == None - assert module.logfile == "/tmp/dcnm_image_upgrade.log" - assert isinstance(module.endpoints, NdfcEndpoints) + # assert module.logfile == "/tmp/dcnm_image_upgrade.log" + assert module.logfile == "/tmp/ndfc.log" + # assert isinstance(module.endpoints, NdfcEndpoints) @pytest.mark.parametrize( diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py index 42a6fef67..d8f6d3a99 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py @@ -1,5 +1,7 @@ -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ - NdfcEndpoints +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ +# NdfcEndpoints + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints """ ndfc_version: 12 diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py index 39c83365b..4c9e60de9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py @@ -3,9 +3,11 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( - NdfcImageInstallOptions -) +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# NdfcImageInstallOptions +# ) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import NdfcImageInstallOptions + from .fixture import load_fixture """ @@ -15,8 +17,8 @@ class_name = "NdfcImageInstallOptions" response_file = f"dcnm_image_upgrade_responses_{class_name}" -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" - +#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options.dcnm_send" class MockAnsibleModule: params = {} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py index 5fdefc5aa..76e0921ca 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py @@ -3,9 +3,11 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( - NdfcImagePolicies -) +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# NdfcImagePolicies +# ) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import NdfcImagePolicies + from .fixture import load_fixture """ @@ -13,8 +15,8 @@ description: Verify functionality of class NdfcImagePolicies """ -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" - +#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies.dcnm_send" class MockAnsibleModule: params = {} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py index da4d4beb2..12d1fd108 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py @@ -9,9 +9,13 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( - NdfcImagePolicies, NdfcImagePolicyAction, - NdfcSwitchIssuDetailsBySerialNumber) +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# NdfcImagePolicies, NdfcImagePolicyAction, +# NdfcSwitchIssuDetailsBySerialNumber) + +# from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import NdfcImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import NdfcImagePolicyAction +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber from .fixture import load_fixture @@ -21,10 +25,10 @@ def does_not_raise(): yield -dcnm_send_patch = ( - "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -) - +# dcnm_send_patch = ( +# "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +# ) +dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details.dcnm_send" def response_data_issu_details(key: str) -> Dict[str, str]: response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py index 99e837245..f68566659 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py @@ -10,9 +10,13 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( - NdfcEndpoints, NdfcImageStage, NdfcSwitchIssuDetailsBySerialNumber, - NdfcVersion) +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# NdfcEndpoints, NdfcImageStage, NdfcSwitchIssuDetailsBySerialNumber, +# NdfcVersion) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import NdfcImageStage +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber +#from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import NdfcVersion from .fixture import load_fixture @@ -22,10 +26,13 @@ def does_not_raise(): yield -dcnm_send_patch = ( - "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -) +# dcnm_send_patch = ( +# "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +# ) +dcnm_send_issu_details = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details.dcnm_send" +dcnm_send_ndfc_version = "ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version.dcnm_send" +dcnm_send_image_stage = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage.dcnm_send" def response_data_issu_details(key: str) -> Dict[str, str]: response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" @@ -123,7 +130,7 @@ def test_populate_ndfc_version(monkeypatch, module, key, expected) -> None: def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: return response_data_ndfc_version(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_ndfc_version, mock_dcnm_send) module._populate_ndfc_version() assert module.ndfc_version == expected @@ -152,7 +159,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_prune_serial_numbers" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -189,7 +196,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_validate_serial_numbers" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send) module.issu_detail = mock_issu_details module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] @@ -226,11 +233,21 @@ def test_commit_serial_numbers( 2. fail_json is not called when serial_numbers is set """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_ndfc_version(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcVersion_get_return_code_200" + return response_data_ndfc_version(key) + + def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_validate_serial_numbers" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_validate_serial_numbers" + return response_data_issu_details(key) + + monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_ndfc_version, mock_dcnm_send_ndfc_version) if serial_numbers_is_set: module.serial_numbers = ["FDO21120U5D"] @@ -254,11 +271,21 @@ def test_commit_path_verb(monkeypatch, module) -> None: 1. both self.path and self.verb should be set, per above """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_ndfc_version(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcVersion_get_return_code_200" + return response_data_ndfc_version(key) + + def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_validate_serial_numbers" + return response_data_issu_details(key) + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_validate_serial_numbers" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_ndfc_version, mock_dcnm_send_ndfc_version) module.serial_numbers = ["FDO21120U5D"] module.commit() @@ -301,11 +328,16 @@ def mock_ndfc_version(*args, **kwargs) -> None: ndfc_version_patch += "dcnm_image_upgrade.NdfcImageStage._populate_ndfc_version" monkeypatch.setattr(ndfc_version_patch, mock_ndfc_version) - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + key = "NdfcImageStage_test_validate_serial_numbers" + return response_data_issu_details(key) + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_commit_payload_serial_number_key_name" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.serial_numbers = ["FDO21120U5D"] module.commit() @@ -330,12 +362,11 @@ def test_wait_for_image_stage_to_complete( 3. module.serial_numbers_done should contain all serial numbers module.serial_numbers 4. The module should return without calling fail_json. """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_wait_for_image_stage_to_complete" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -368,12 +399,11 @@ def test_wait_for_image_stage_to_complete_stage_failed( 3. module.serial_numbers_done contains FDO21120U5D, imageStaged is "Success" 4. Call fail_json on serial number FDO2112189M, imageStaged is "Failed" """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_wait_for_image_stage_to_complete_fail_json" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -409,11 +439,11 @@ def test_wait_for_image_stage_to_complete_timout( 4. The function should call fail_json due to timeout """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_wait_for_image_stage_to_complete_timeout" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -462,7 +492,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_wait_for_current_actions_to_complete" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -498,7 +528,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: key = "NdfcImageStage_test_wait_for_current_actions_to_complete_timeout" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send) module.issu_detail = mock_issu_details module.serial_numbers = [ From c033e8194b01405caba3c296fb11c59def701d83 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 1 Nov 2023 11:26:33 -1000 Subject: [PATCH 016/300] Remove product references --- .../module_utils/common/controller_version.py | 93 ++- .../{endpoints.py => api_endpoints.py} | 6 +- .../module_utils/image_mgmt/image_policies.py | 75 +-- .../image_mgmt/image_policy_action.py | 54 +- .../module_utils/image_mgmt/image_stage.py | 66 +- .../module_utils/image_mgmt/image_upgrade.py | 52 +- .../image_upgrade_common.py} | 4 +- .../module_utils/image_mgmt/image_validate.py | 63 +- .../image_mgmt/install_options.py | 42 +- .../module_utils/image_mgmt/switch_details.py | 50 +- .../image_mgmt/switch_issu_details.py | 70 +-- plugins/modules/dcnm_image_upgrade.py | 127 ++-- plugins/modules/dcnm_image_upgrade_orig.py | 482 +++++++-------- ...loads.json => image_upgrade_payloads.json} | 4 +- ...on => image_upgrade_playbook_configs.json} | 0 ...nses.json => image_upgrade_responses.json} | 0 ..._upgrade_responses_ControllerVersion.json} | 54 +- ...pgrade_responses_ImageInstallOptions.json} | 0 ...mage_upgrade_responses_ImagePolicies.json} | 2 +- ...upgrade_responses_ImageUpgradeCommon.json} | 0 ...mage_upgrade_responses_SwitchDetails.json} | 6 +- ..._upgrade_responses_SwitchIssuDetails.json} | 36 +- ...st_dcnm_image_upgrade_NdfcImagePolicies.py | 213 ------- .../test_dcnm_image_upgrade_NdfcVersion.py | 570 ----------------- ....py => test_image_upgrade_ApiEndpoints.py} | 48 +- .../test_image_upgrade_ControllerVersion.py | 574 ++++++++++++++++++ ...test_image_upgrade_ImageInstallOptions.py} | 51 +- .../test_image_upgrade_ImagePolicies.py | 219 +++++++ ...> test_image_upgrade_ImagePolicyAction.py} | 62 +- ...ge.py => test_image_upgrade_ImageStage.py} | 204 +++---- ....py => test_image_upgrade_ImageUpgrade.py} | 115 ++-- ... test_image_upgrade_ImageUpgradeCommon.py} | 30 +- ...py => test_image_upgrade_ImageValidate.py} | 85 +-- ...py => test_image_upgrade_SwitchDetails.py} | 133 ++-- ..._upgrade_SwitchIssuDetailsByDeviceName.py} | 107 ++-- ...e_upgrade_SwitchIssuDetailsByIpAddress.py} | 108 ++-- ...pgrade_SwitchIssuDetailsBySerialNumber.py} | 110 ++-- 37 files changed, 1985 insertions(+), 1930 deletions(-) rename plugins/module_utils/image_mgmt/{endpoints.py => api_endpoints.py} (97%) rename plugins/module_utils/{common/ndfc_common.py => image_mgmt/image_upgrade_common.py} (98%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_payloads.json => image_upgrade_payloads.json} (85%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_playbook_configs.json => image_upgrade_playbook_configs.json} (100%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_responses.json => image_upgrade_responses.json} (100%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_responses_NdfcVersion.json => image_upgrade_responses_ControllerVersion.json} (91%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_responses_NdfcImageInstallOptions.json => image_upgrade_responses_ImageInstallOptions.json} (100%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_responses_NdfcImagePolicies.json => image_upgrade_responses_ImagePolicies.json} (97%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon.json => image_upgrade_responses_ImageUpgradeCommon.json} (100%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_responses_NdfcSwitchDetails.json => image_upgrade_responses_SwitchDetails.json} (98%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/{dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json => image_upgrade_responses_SwitchIssuDetails.json} (98%) delete mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py delete mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcVersion.py rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcEndpoints.py => test_image_upgrade_ApiEndpoints.py} (86%) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcImageInstallOptions.py => test_image_upgrade_ImageInstallOptions.py} (75%) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcImagePolicyAction.py => test_image_upgrade_ImagePolicyAction.py} (74%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcImageStage.py => test_image_upgrade_ImageStage.py} (68%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcImageUpgrade.py => test_image_upgrade_ImageUpgrade.py} (68%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py => test_image_upgrade_ImageUpgradeCommon.py} (90%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcImageValidate.py => test_image_upgrade_ImageValidate.py} (75%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcSwitchDetails.py => test_image_upgrade_SwitchDetails.py} (50%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByDeviceName.py => test_image_upgrade_SwitchIssuDetailsByDeviceName.py} (57%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByIpAddress.py => test_image_upgrade_SwitchIssuDetailsByIpAddress.py} (58%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_dcnm_image_upgrade_NdfcSwitchIssuDetailsBySerialNumber.py => test_image_upgrade_SwitchIssuDetailsBySerialNumber.py} (57%) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 548ceb4b9..bde9aaf50 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -2,25 +2,24 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import (dcnm_send) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -class NdfcVersion(NdfcCommon): +class ControllerVersion(ImageUpgradeCommon): """ - Return image version information from NDFC + Return image version information from the Controller NOTES: 1. considered using dcnm_version_supported() but it does not return minor release info, which is needed due to key changes between - 12.1.2e and 12.1.3b. For example, see NdfcImageStage().commit() + 12.1.2e and 12.1.3b. For example, see ImageStage().commit() Endpoint: /appcenter/cisco/ndfc/api/v1/fm/about/version Usage (where module is an instance of AnsibleModule): - instance = NdfcVersion(module) + instance = ControllerVersion(module) instance.refresh() if instance.version == "12.1.2e": do 12.1.2e stuff @@ -43,52 +42,52 @@ class NdfcVersion(NdfcCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): self.properties = {} self.properties["data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None + self.properties["result"] = None + self.properties["response"] = None def refresh(self): """ - Refresh self.ndfc_data with current version info from NDFC + Refresh self.response_data with current version info from the Controller """ - path = self.endpoints.ndfc_version.get("path") - verb = self.endpoints.ndfc_version.get("verb") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + path = self.endpoints.controller_version.get("path") + verb = self.endpoints.controller_version.get("verb") + self.properties["response"] = dcnm_send(self.module, verb, path) + self.properties["result"] = self._handle_response(self.response, verb) - msg = f"REMOVE: {self.class_name}.refresh() ndfc_response: {self.ndfc_response}" + msg = f"REMOVE: {self.class_name}.refresh() response: {self.response}" self.log_msg(msg) - msg = f"REMOVE: {self.class_name}.refresh() ndfc_result: {self.ndfc_result}" + msg = f"REMOVE: {self.class_name}.refresh() result: {self.result}" self.log_msg(msg) - if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: - msg = f"{self.class_name}.refresh() failed: {self.ndfc_result}" + if self.result["success"] == False or self.result["found"] == False: + msg = f"{self.class_name}.refresh() failed: {self.result}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - if self.ndfc_data is None: - msg = f"{self.class_name}.refresh() failed: NDFC response " - msg += "does not contain DATA key. NDFC response: " - msg += f"{self.ndfc_response}" + self.properties["response_data"] = self.response.get("DATA") + if self.response_data is None: + msg = f"{self.class_name}.refresh() failed: response " + msg += "does not contain DATA key. Controller response: " + msg += f"{self.response}" self.module.fail_json(msg) - msg = f"REMOVE: {self.class_name}.refresh() ndfc_data: {self.ndfc_data}" + msg = f"REMOVE: {self.class_name}.refresh() response_data: {self.response_data}" self.log_msg(msg) def _get(self, item): - return self.make_boolean(self.make_none(self.ndfc_data.get(item))) + return self.make_boolean(self.make_none(self.response_data.get(item))) @property def dev(self): """ - Return True if NDFC is a development release. - Return False if NDFC is not a development release. + Return True if the Controller is running a development release. + Return False if the Controller is not running a development release. Return None otherwise Possible values: @@ -114,8 +113,8 @@ def install(self): @property def is_ha_enabled(self): """ - Return True if NDFC is a media controller. - Return False if NDFC is not a media controller. + Return True if Controller is high-availability enabled. + Return False if Controller is not high-availability enabled. Return None otherwise Possible values: @@ -128,8 +127,8 @@ def is_ha_enabled(self): @property def is_media_controller(self): """ - Return True if NDFC is a media controller. - Return False if NDFC is not a media controller. + Return True if Controller is a media controller. + Return False if Controller is not a media controller. Return None otherwise Possible values: @@ -142,8 +141,8 @@ def is_media_controller(self): @property def is_upgrade_inprogress(self): """ - Return True if an NDFC upgrade is in progress. - Return False if an NDFC upgrade is not in progress. + Return True if a Controller upgrade is in progress. + Return False if a Controller upgrade is not in progress. Return None otherwise Possible values: @@ -154,30 +153,30 @@ def is_upgrade_inprogress(self): return self.make_boolean(self._get("is_upgrade_inprogress")) @property - def ndfc_data(self): + def response_data(self): """ Return the data retrieved from the request """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_result(self): + def result(self): """ - Return the GET result from NDFC + Return the GET result from the Controller """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property - def ndfc_response(self): + def response(self): """ - Return the GET response from NDFC + Return the GET response from the Controller """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property def mode(self): """ - Return the NDFC mode, if it exists. + Return the controller mode, if it exists. Return None otherwise Possible values: @@ -201,7 +200,7 @@ def uuid(self): @property def version(self): """ - Return the NDFC version, if it exists. + Return the controller version, if it exists. Return None otherwise Possible values: @@ -213,7 +212,7 @@ def version(self): @property def version_major(self): """ - Return the NDFC major version, if it exists. + Return the controller major version, if it exists. Return None otherwise We are assuming semantic versioning based on: @@ -230,7 +229,7 @@ def version_major(self): @property def version_minor(self): """ - Return the NDFC minor version, if it exists. + Return the controller minor version, if it exists. Return None otherwise We are assuming semantic versioning based on: @@ -247,7 +246,7 @@ def version_minor(self): @property def version_patch(self): """ - Return the NDFC minor version, if it exists. + Return the controller minor version, if it exists. Return None otherwise We are assuming semantic versioning based on: diff --git a/plugins/module_utils/image_mgmt/endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py similarity index 97% rename from plugins/module_utils/image_mgmt/endpoints.py rename to plugins/module_utils/image_mgmt/api_endpoints.py index b1c66fea3..43700a093 100644 --- a/plugins/module_utils/image_mgmt/endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -1,6 +1,6 @@ -class NdfcEndpoints: +class ApiEndpoints: """ - Endpoints for NDFC image management API calls + Endpoints for image management API calls """ def __init__(self): self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" @@ -73,7 +73,7 @@ def issu_info(self): return endpoint @property - def ndfc_version(self): + def controller_version(self): path = f"{self.endpoint_feature_manager}/about/version" endpoint = {} endpoint["path"] = path diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 7e8354560..4c386de7f 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -1,20 +1,20 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -class NdfcImagePolicies(NdfcCommon): +class ImagePolicies(ImageUpgradeCommon): """ - Retrieve image policy details from NDFC and provide property accessors - for the policy attributes. + Retrieve image policy details from the controller and provide + property accessors for the policy attributes. Usage (where module is an instance of AnsibleModule): - instance = NdfcImagePolicies(module).refresh() + instance = ImagePolicies(module).refresh() instance.policy_name = "NR3F" if instance.name is None: - print("policy NR3F does not exist on NDFC") + print("policy NR3F does not exist on the controller") exit(1) policy_name = instance.name platform = instance.platform @@ -31,7 +31,7 @@ class NdfcImagePolicies(NdfcCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self.log_msg(f"{self.class_name}.__init__ entered") self._init_properties() # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it @@ -40,49 +40,49 @@ def __init__(self, module): def _init_properties(self): self.properties = {} self.properties["policy_name"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None + self.properties["response_data"] = None + self.properties["response"] = None + self.properties["result"] = None def refresh(self): """ - Refresh self.image_policies with current image policies from NDFC + Refresh self.image_policies with current image policies from the controller """ path = self.endpoints.policies_info.get("path") verb = self.endpoints.policies_info.get("verb") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + self.properties["response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + self.properties["result"] = self._handle_response(self.response, verb) + self.log_msg(f"{self.class_name}.refresh: result: {self.result}") msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"result: {self.ndfc_result}" - if not self.ndfc_result["success"]: + msg += f"result: {self.result}" + if not self.result["success"]: msg = f"{self.class_name}.refresh: " msg += "Bad result when retriving image policy " - msg += "information from NDFC." + msg += "information from the controller." self.module.fail_json(msg) - data = self.ndfc_response.get("DATA").get("lastOperDataObject") + data = self.response.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.refresh: " msg += "Bad response when retrieving image policy " - msg += "information from NDFC." + msg += "information from the controller." self.module.fail_json(msg) if len(data) == 0: msg = f"{self.class_name}.refresh: " - msg += "NDFC has no defined image policies." + msg += "the controller has no defined image policies." self.module.fail_json(msg) - self.properties["ndfc_data"] = {} + self.properties["response_data"] = {} for policy in data: policy_name = policy.get("policyName") if policy_name is None: msg = f"{self.class_name}.refresh: " - msg += "Cannot parse NDFC policy information" + msg += "Cannot parse policy information from the controller." self.module.fail_json(msg) - self.properties["ndfc_data"][policy_name] = policy + self.properties["response_data"][policy_name] = policy def _get(self, item): if self.policy_name is None: @@ -90,11 +90,12 @@ def _get(self, item): msg += f"instance.policy_name must be set before " msg += f"accessing property {item}." self.module.fail_json(msg) - if self.properties['ndfc_data'].get(self.policy_name) is None: + if self.properties['response_data'].get(self.policy_name) is None: msg = f"{self.class_name}._get: " - msg += f"policy_name {self.policy_name} is not defined in NDFC" + msg += f"policy_name {self.policy_name} is not defined on " + msg += "the controller." self.module.fail_json(msg) - return_item = self.make_boolean(self.properties["ndfc_data"][self.policy_name].get(item)) + return_item = self.make_boolean(self.properties["response_data"][self.policy_name].get(item)) return_item = self.make_none(return_item) return return_item @@ -126,26 +127,26 @@ def name(self): return self._get("policyName") @property - def ndfc_data(self): + def response_data(self): """ - Return the parsed data from the NDFC response as a dictionary, + Return the parsed data from the response as a dictionary, keyed on policy_name. """ - return self.properties["ndfc_data"] + return self.properties["response_data"] @property - def ndfc_response(self): + def response(self): """ - Return the raw response from the NDFC response. + Return the raw response from the controller. """ - return self.properties["ndfc_response"] + return self.properties["response"] @property - def ndfc_result(self): + def result(self): """ - Return the raw result from the NDFC response. + Return the raw result. """ - return self.properties["ndfc_result"] + return self.properties["result"] @property def policy_name(self): diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index b0b2de731..3646428c8 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -2,15 +2,15 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import NdfcImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber -class NdfcImagePolicyAction(NdfcCommon): +class ImagePolicyAction(ImageUpgradeCommon): """ - Perform image policy actions on NDFC on one or more switches. + Perform image policy actions on the controller for one or more switches. Support for the following actions: - attach @@ -19,7 +19,7 @@ class NdfcImagePolicyAction(NdfcCommon): Usage (where module is an instance of AnsibleModule): - instance = NdfcImagePolicyAction(module) + instance = ImagePolicyAction(module) instance.policy_name = "NR3F" instance.action = "attach" # or detach, or query instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] @@ -39,17 +39,17 @@ class NdfcImagePolicyAction(NdfcCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self._init_properties() - self.image_policies = NdfcImagePolicies(self.module) - self.switch_issu_details = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.image_policies = ImagePolicies(self.module) + self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.module) self.valid_actions = {"attach", "detach", "query"} def _init_properties(self): self.properties = {} self.properties["action"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None + self.properties["response"] = None + self.properties["result"] = None self.properties["policy_name"] = None self.properties["query_result"] = None self.properties["serial_numbers"] = None @@ -146,7 +146,7 @@ def _attach_policy(self): NOTES: 1. This method creates a list of responses and results which - are accessible via properties ndfc_response and ndfc_result, + are accessible via properties response and result, respectively. """ self.build_attach_payload() @@ -164,8 +164,8 @@ def _attach_policy(self): self.module.fail_json(msg) responses.append(response) results.append(result) - self.properties["ndfc_response"] = responses - self.properties["ndfc_result"] = results + self.properties["response"] = responses + self.properties["result"] = results def _detach_policy(self): """ @@ -182,8 +182,8 @@ def _detach_policy(self): result = self._handle_response(response, verb) if not result["success"]: self._failure(response) - self.properties["ndfc_response"] = response - self.properties["ndfc_result"] = result + self.properties["response"] = response + self.properties["result"] = result def _query_policy(self): """ @@ -199,8 +199,8 @@ def _query_policy(self): if not result["success"]: self._failure(response) self.properties["query_result"] = response.get("DATA") - self.properties["ndfc_response"] = response - self.properties["ndfc_result"] = result + self.properties["response"] = response + self.properties["result"] = result @property def query_result(self): @@ -227,22 +227,26 @@ def action(self, value): self.properties["action"] = value @property - def ndfc_response(self): + def response(self): """ - Return the raw response from NDFC after calling commit(). + Return the raw response from the controller. + + Assumes that commit() has been called. In the case of attach, this is a list of responses. """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property - def ndfc_result(self): + def result(self): """ - Return the raw result from NDFC after calling commit(). + Return the raw result. + + Assumes that commit() has been called. In the case of attach, this is a list of results. """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property def policy_name(self): diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 40a99bbe7..28aef8bd6 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -4,12 +4,12 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber -from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import NdfcVersion +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import ControllerVersion -class NdfcImageStage(NdfcCommon): +class ImageStage(ImageUpgradeCommon): """ Endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image @@ -18,7 +18,7 @@ class NdfcImageStage(NdfcCommon): Usage (where module is an instance of AnsibleModule): - stage = NdfcImageStage(module) + stage = ImageStage(module) stage.serial_numbers = ["FDO211218HH", "FDO211218GC"] stage.commit() data = stage.data @@ -75,35 +75,35 @@ class NdfcImageStage(NdfcCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self._init_properties() self.serial_numbers_done = set() - self.ndfc_version = None + self.controller_version = None self.path = None self.verb = None self.payload = None - self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): self.properties = {} self.properties["serial_numbers"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None + self.properties["response_data"] = None + self.properties["result"] = None + self.properties["response"] = None self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds - def _populate_ndfc_version(self): + def _populate_controller_version(self): """ - Populate self.ndfc_version with the NDFC version. + Populate self.controller_version with the NDFC version. Notes: - 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + 1. This cannot go into ImageUpgradeCommon() due to circular imports resulting in RecursionError """ - instance = NdfcVersion(self.module) + instance = ControllerVersion(self.module) instance.refresh() - self.ndfc_version = instance.version + self.controller_version = instance.version def prune_serial_numbers(self): """ @@ -165,25 +165,25 @@ def commit(self): self.log_msg(f"REMOVE: {self.class_name}.commit() path: {self.path}") self.log_msg(f"REMOVE: {self.class_name}.commit() verb: {self.verb}") self.payload = {} - self._populate_ndfc_version() - if self.ndfc_version == "12.1.2e": + self._populate_controller_version() + if self.controller_version == "12.1.2e": # Yes, NDFC 12.1.2e wants serialNum to be misspelled self.payload["sereialNum"] = self.serial_numbers else: self.payload["serialNumbers"] = self.serial_numbers - self.properties["ndfc_response"] = dcnm_send( + self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, self.verb) + self.properties["result"] = self._handle_response(self.response, self.verb) self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + f"REMOVE: {self.class_name}.commit() response: {self.response}" ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + if not self.result["success"]: + msg = f"{self.class_name}.commit() failed: {self.result}. " + msg += f"NDFC response was: {self.response}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_stage_to_complete() def _wait_for_current_actions_to_complete(self): @@ -327,28 +327,28 @@ def serial_numbers(self, value): self.properties["serial_numbers"] = value @property - def ndfc_data(self): + def response_data(self): """ Return the result of the image staging request for serial_numbers. instance.serial_numbers must be set first. """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_result(self): + def result(self): """ Return the POST result from NDFC """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property - def ndfc_response(self): + def response(self): """ Return the POST response from NDFC """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property def check_interval(self): diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index bc17686bb..b3e82b5b1 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -5,11 +5,11 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsByIpAddress +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsByIpAddress -class NdfcImageUpgrade(NdfcCommon): +class ImageUpgrade(ImageUpgradeCommon): """ Endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image @@ -17,7 +17,7 @@ class NdfcImageUpgrade(NdfcCommon): Usage (where module is an instance of AnsibleModule): - upgrade = NdfcImageUpgrade(module) + upgrade = ImageUpgrade(module) upgrade.devices = devices upgrade.commit() data = upgrade.data @@ -105,13 +105,13 @@ class NdfcImageUpgrade(NdfcCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() # Maximum number of modules/linecards in a switch self.max_module_number = 9 self._init_defaults() self._init_properties() - self.issu_detail = NdfcSwitchIssuDetailsByIpAddress(self.module) + self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) def _init_defaults(self): self.defaults = {} @@ -154,9 +154,9 @@ def _init_properties(self): self.properties["epld_module"] = "ALL" self.properties["epld_upgrade"] = False self.properties["force_non_disruptive"] = False - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None + self.properties["response_data"] = None + self.properties["result"] = None + self.properties["response"] = None self.properties["non_disruptive"] = False self.properties["package_install"] = False self.properties["package_uninstall"] = False @@ -183,9 +183,9 @@ def _init_properties(self): # TODO:1 This prunes devices only based on the image upgrade state. # TODO:1 It does not check other image states and EPLD states. # """ - # # issu = NdfcSwitchIssuDetailsBySerialNumber(self.module) + # # issu = SwitchIssuDetailsBySerialNumber(self.module) # pruned_devices = set() - # instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + # instance = SwitchIssuDetailsByIpAddress(self.module) # instance.refresh() # for device in self.devices: # msg = f"REMOVE: {self.class_name}.prune_devices() device: {device}" @@ -395,19 +395,19 @@ def commit(self): for device in self.devices: self.build_payload(device) self.log_msg(f"REMOVE: {self.class_name}.commit() upgrade payload: {self.payload}") - self.properties["ndfc_response"] = dcnm_send( + self.properties["response"] = dcnm_send( self.module, verb, path, data=json.dumps(self.payload) ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.properties["result"] = self._handle_response(self.response, verb) self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + f"REMOVE: {self.class_name}.commit() response: {self.response}" ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + if not self.result["success"]: + msg = f"{self.class_name}.commit() failed: {self.result}. " + msg += f"NDFC response was: {self.response}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_upgrade_to_complete() def _wait_for_current_actions_to_complete(self): @@ -768,32 +768,32 @@ def check_timeout(self): return self.properties.get("check_timeout") @property - def ndfc_data(self): + def response_data(self): """ Return the data retrieved from NDFC for the image upgrade request. instance.devices must be set first. instance.commit() must be called first. """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_result(self): + def result(self): """ Return the POST result from NDFC instance.devices must be set first. instance.commit() must be called first. """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property - def ndfc_response(self): + def response(self): """ Return the POST response from NDFC instance.devices must be set first. instance.commit() must be called first. """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property def serial_numbers(self): diff --git a/plugins/module_utils/common/ndfc_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py similarity index 98% rename from plugins/module_utils/common/ndfc_common.py rename to plugins/module_utils/image_mgmt/image_upgrade_common.py index 6d08e0e86..43c73b657 100644 --- a/plugins/module_utils/common/ndfc_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -1,10 +1,10 @@ -class NdfcCommon: +class ImageUpgradeCommon: """ Base class for the other NDFC classes Usage (where module is an instance of AnsibleModule): - class MyNdfcClass(NdfcCommon): + class MyClass(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) ... diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 9af13b65e..1386f4020 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -1,13 +1,14 @@ import copy +import json from time import sleep from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber -class NdfcImageValidate(NdfcCommon): +class ImageValidate(ImageUpgradeCommon): """ Endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image @@ -16,12 +17,12 @@ class NdfcImageValidate(NdfcCommon): Usage (where module is an instance of AnsibleModule): - instance = NdfcImageValidate(module) + instance = ImageValidate(module) instance.serial_numbers = ["FDO211218HH", "FDO211218GC"] # non_disruptive is optional instance.non_disruptive = True instance.commit() - data = instance.ndfc_data + data = instance.response_data Request body: { @@ -37,39 +38,39 @@ class NdfcImageValidate(NdfcCommon): The response is not JSON, nor is it very useful. Instead, we poll for validation status using - NdfcSwitchIssuDetailsBySerialNumber. + SwitchIssuDetailsBySerialNumber. """ def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self._init_properties() - self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): self.properties = {} self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None + self.properties["response_data"] = None + self.properties["result"] = None + self.properties["response"] = None self.properties["non_disruptive"] = False self.properties["serial_numbers"] = None - # def _populate_ndfc_version(self): + # def _populate_controller_version(self): # """ - # Populate self.ndfc_version with the NDFC version. + # Populate self.controller_version with the NDFC version. # TODO:3 Remove if 12.1.3b works with no changes to request/response payloads. # Notes: - # 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + # 1. This cannot go into ImageUpgradeCommon() due to circular # imports resulting in RecursionError # """ - # instance = NdfcVersion(self.module) + # instance = ControllerVersion(self.module) # instance.refresh() - # self.ndfc_version = instance.version + # self.controller_version = instance.version def prune_serial_numbers(self): """ @@ -137,19 +138,19 @@ def commit(self): path = self.endpoints.image_validate.get("path") verb = self.endpoints.image_validate.get("verb") self.build_payload() - self.properties["ndfc_response"] = dcnm_send( + self.properties["response"] = dcnm_send( self.module, verb, path, data=json.dumps(self.payload) ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.properties["result"] = self._handle_response(self.response, verb) self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + f"REMOVE: {self.class_name}.commit() response: {self.response}" ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + if not self.result["success"]: + msg = f"{self.class_name}.commit() failed: {self.result}. " + msg += f"NDFC response was: {self.response}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_validate_to_complete() def _wait_for_current_actions_to_complete(self): @@ -318,28 +319,28 @@ def non_disruptive(self, value): self.properties["non_disruptive"] = value @property - def ndfc_data(self): + def response_data(self): """ Return the result of the image staging request for serial_numbers. instance.serial_numbers must be set first. """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_result(self): + def result(self): """ Return the POST result from NDFC """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property - def ndfc_response(self): + def response(self): """ Return the POST response from NDFC """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property def check_interval(self): diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 898e81241..ade4afe39 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -3,10 +3,10 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -class NdfcImageInstallOptions(NdfcCommon): +class ImageInstallOptions(ImageUpgradeCommon): """ Retrieve install-options details for ONE switch from NDFC and provide property accessors for the policy attributes. @@ -18,7 +18,7 @@ class NdfcImageInstallOptions(NdfcCommon): Usage (where module is an instance of AnsibleModule): - instance = NdfcImageInstallOptions(module) + instance = ImageInstallOptions(module) # Mandatory instance.policy_name = "NR3F" instance.serial_number = "FDO211218GC" @@ -114,16 +114,16 @@ class NdfcImageInstallOptions(NdfcCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): self.properties = {} self.properties["epld"] = False self.properties["issu"] = True - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None + self.properties["response_data"] = None + self.properties["response"] = None + self.properties["result"] = None self.properties["package_install"] = False self.properties["policy_name"] = None self.properties["serial_number"] = None @@ -150,22 +150,22 @@ def refresh(self): msg = f"REMOVE: {self.class_name}.refresh: " msg += f"payload: {self.payload}" self.log_msg(msg) - self.properties["ndfc_response"] = dcnm_send( + self.properties["response"] = dcnm_send( self.module, verb, path, data=json.dumps(self.payload) ) msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"ndfc_response: {self.ndfc_response}" + msg += f"response: {self.response}" self.log_msg(msg) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.properties["result"] = self._handle_response(self.response, verb) # TODO:2 should error message contain full response or just DATA.error? - if self.ndfc_result["success"] is False: + if self.result["success"] is False: msg = f"{self.class_name}.refresh: " msg += "Bad result when retrieving install-options from NDFC. " - msg += f"NDFC response: {self.ndfc_response}" + msg += f"NDFC response: {self.response}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - self.data = self.properties["ndfc_data"] + self.properties["response_data"] = self.response.get("DATA") + self.data = self.properties["response_data"] if self.data.get("compatibilityStatusList") is None: self.compatibility_status = {} else: @@ -356,25 +356,25 @@ def ip_address(self): return self.compatibility_status.get("ipAddress") @property - def ndfc_data(self): + def response_data(self): """ Return the raw data from the NDFC response. """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_response(self): + def response(self): """ Return the response from NDFC of the query. """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property - def ndfc_result(self): + def result(self): """ Return the result from NDFC of the query. """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property def os_type(self): diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index 9d1a5c4a8..c7dae0135 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -1,17 +1,17 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -class NdfcSwitchDetails(NdfcCommon): +class SwitchDetails(ImageUpgradeCommon): """ Retrieve switch details from NDFC and provide property accessors for the switch attributes. Usage (where module is an instance of AnsibleModule): - instance = NdfcSwitchDetails(module) + instance = SwitchDetails(module) instance.refresh() instance.ip_address = 10.1.1.1 fabric_name = instance.fabric_name @@ -27,15 +27,15 @@ class NdfcSwitchDetails(NdfcCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): self.properties = {} self.properties["ip_address"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None + self.properties["response_data"] = None + self.properties["response"] = None + self.properties["result"] = None def refresh(self): """ @@ -47,24 +47,24 @@ def refresh(self): verb = self.endpoints.switches_info.get("verb") self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response {self.ndfc_response}" + self.properties["response"] = dcnm_send(self.module, verb, path) + msg = f"REMOVE: {self.class_name}.refresh: self.response {self.response}" self.log_msg(msg) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_result {self.ndfc_result}" + self.properties["result"] = self._handle_response(self.response, verb) + msg = f"REMOVE: {self.class_name}.refresh: self.result {self.result}" self.log_msg(msg) - if self.ndfc_response["RETURN_CODE"] != 200: + if self.response["RETURN_CODE"] != 200: msg = "Unable to retrieve switch information from NDFC. " - msg += f"Got response {self.ndfc_response}" + msg += f"Got response {self.response}" self.module.fail_json(msg) - data = self.ndfc_response.get("DATA") - self.properties["ndfc_data"] = {} + data = self.response.get("DATA") + self.properties["response_data"] = {} for switch in data: - self.properties["ndfc_data"][switch["ipAddress"]] = switch + self.properties["response_data"][switch["ipAddress"]] = switch - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_data {self.ndfc_data}" + msg = f"REMOVE: {self.class_name}.refresh: self.response_data {self.response_data}" self.log_msg(msg) def _get(self, item): @@ -74,7 +74,7 @@ def _get(self, item): self.module.fail_json(msg) return self.make_boolean( self.make_none( - self.properties["ndfc_data"][self.ip_address].get(item) + self.properties["response_data"][self.ip_address].get(item) ) ) @@ -128,30 +128,30 @@ def model(self): return self._get("model") @property - def ndfc_data(self): + def response_data(self): """ Return parsed data from the GET request. Return None otherwise NOTE: Keyed on ip_address """ - return self.properties["ndfc_data"] + return self.properties["response_data"] @property - def ndfc_response(self): + def response(self): """ Return the raw response from the GET request. Return None otherwise """ - return self.properties["ndfc_response"] + return self.properties["response"] @property - def ndfc_result(self): + def result(self): """ Return the raw result of the GET request. Return None otherwise """ - return self.properties["ndfc_result"] + return self.properties["result"] @property def platform(self): diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index f108c2e1d..b7b1277f8 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -1,9 +1,9 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import dcnm_send -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -class NdfcSwitchIssuDetails(NdfcCommon): +class SwitchIssuDetails(ImageUpgradeCommon): """ Retrieve switch issu details from NDFC and provide property accessors for the switch attributes. @@ -64,14 +64,14 @@ class NdfcSwitchIssuDetails(NdfcCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): self.properties = {} - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_data"] = None + self.properties["response"] = None + self.properties["result"] = None + self.properties["response_data"] = None # action_keys is used in subclasses to determine if any actions # are in progress. # Property actions_in_progress returns True if so, False otherwise @@ -88,23 +88,23 @@ def refresh(self) -> None: path = self.endpoints.issu_info.get("path") verb = self.endpoints.issu_info.get("verb") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + self.properties["response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + self.properties["result"] = self._handle_response(self.response, verb) + self.log_msg(f"{self.class_name}.refresh: result: {self.result}") msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"result: {self.ndfc_result}" - # ndfc_result for 404 response + msg += f"result: {self.result}" + # result for 404 response # {'found': False, 'success': True} - if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: + if self.result["success"] == False or self.result["found"] == False: msg = f"{self.class_name}.refresh: " msg += "Bad result when retriving switch " msg += "information from NDFC" self.module.fail_json(msg) - data = self.ndfc_response.get("DATA").get("lastOperDataObject") + data = self.response.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.refresh: " msg += "NDFC has no switch ISSU information." @@ -114,10 +114,10 @@ def refresh(self) -> None: msg += "NDFC has no switch ISSU information." self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA", {}).get( + self.properties["response_data"] = self.response.get("DATA", {}).get( "lastOperDataObject", [] ) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + self.log_msg(f"{self.class_name}.refresh: response: {self.response}") @property def actions_in_progress(self): @@ -137,27 +137,27 @@ def _get(self, item): pass @property - def ndfc_data(self): + def response_data(self): """ Return the raw data retrieved from NDFC """ - return self.properties["ndfc_data"] + return self.properties["response_data"] @property - def ndfc_response(self): + def response(self): """ Return the raw response from the GET request. Return None otherwise """ - return self.properties["ndfc_response"] + return self.properties["response"] @property - def ndfc_result(self): + def result(self): """ Return the raw result of the GET request. Return None otherwise """ - return self.properties["ndfc_result"] + return self.properties["result"] @property def device_name(self): @@ -613,14 +613,14 @@ def vpc_role(self): return self._get("vpcRole") -class NdfcSwitchIssuDetailsByIpAddress(NdfcSwitchIssuDetails): +class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): """ Retrieve switch issu details from NDFC and provide property accessors for the switch attributes retrieved by ip address. Usage (where module is an instance of AnsibleModule): - instance = NdfcSwitchIssuDetailsByIpAddress(module) + instance = SwitchIssuDetailsByIpAddress(module) instance.refresh() instance.ip_address = 10.1.1.1 image_staged = instance.image_staged @@ -628,7 +628,7 @@ class NdfcSwitchIssuDetailsByIpAddress(NdfcSwitchIssuDetails): serial_number = instance.serial_number etc... - See NdfcSwitchIssuDetails for more details. + See SwitchIssuDetails for more details. """ def __init__(self, module): @@ -647,7 +647,7 @@ def refresh(self): """ super().refresh() self.data_subclass = {} - for switch in self.ndfc_data: + for switch in self.response_data: msg = f"{self.class_name}.refresh: " msg += f"switch {switch}" self.data_subclass[switch["ipAddress"]] = switch @@ -685,14 +685,14 @@ def ip_address(self, value): self.properties["ip_address"] = value -class NdfcSwitchIssuDetailsBySerialNumber(NdfcSwitchIssuDetails): +class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): """ Retrieve switch issu details from NDFC and provide property accessors for the switch attributes retrieved by serial_number. Usage (where module is an instance of AnsibleModule): - instance = NdfcSwitchIssuDetailsBySerialNumber(module) + instance = SwitchIssuDetailsBySerialNumber(module) instance.refresh() instance.serial_number = "FDO211218GC" instance.refresh() @@ -701,7 +701,7 @@ class NdfcSwitchIssuDetailsBySerialNumber(NdfcSwitchIssuDetails): ip_address = instance.ip_address etc... - See NdfcSwitchIssuDetails for more details. + See SwitchIssuDetails for more details. """ @@ -721,7 +721,7 @@ def refresh(self): """ super().refresh() self.data_subclass = {} - for switch in self.ndfc_data: + for switch in self.response_data: self.data_subclass[switch["serialNumber"]] = switch def _get(self, item): @@ -757,14 +757,14 @@ def serial_number(self, value): self.properties["serial_number"] = value -class NdfcSwitchIssuDetailsByDeviceName(NdfcSwitchIssuDetails): +class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): """ Retrieve switch issu details from NDFC and provide property accessors for the switch attributes retrieved by device_name. Usage (where module is an instance of AnsibleModule): - instance = NdfcSwitchIssuDetailsByDeviceName(module) + instance = SwitchIssuDetailsByDeviceName(module) instance.refresh() instance.device_name = "leaf_1" image_staged = instance.image_staged @@ -772,7 +772,7 @@ class NdfcSwitchIssuDetailsByDeviceName(NdfcSwitchIssuDetails): ip_address = instance.ip_address etc... - See NdfcSwitchIssuDetails for more details. + See SwitchIssuDetails for more details. """ @@ -792,7 +792,7 @@ def refresh(self): """ super().refresh() self.data_subclass = {} - for switch in self.ndfc_data: + for switch in self.response_data: self.data_subclass[switch["deviceName"]] = switch def _get(self, item): diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 0f8ef02b9..38090a1eb 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -28,15 +28,16 @@ import json from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import NdfcImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import NdfcImageStage -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import NdfcImageValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import NdfcImageUpgrade -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import NdfcImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import NdfcSwitchDetails -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsByIpAddress +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import ImagePolicyAction +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import ImageStage +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import ImageUpgrade +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import ImageValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import ImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import SwitchDetails +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsByIpAddress from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, validate_list_of_dicts, @@ -56,7 +57,7 @@ options: state: description: - - The state of DCNM after module completion. + - The state of the feature or object after module completion. - I(merged) and I(query) are the only states supported. type: str choices: @@ -404,7 +405,7 @@ """ -class NdfcAnsibleImageUpgrade(NdfcCommon): +class ImageUpgradeTask(ImageUpgradeCommon): """ Ansible support for image policy attach, detach, and query. """ @@ -413,7 +414,7 @@ def __init__(self, module): super().__init__(module) self.params = self.module.params self.class_name = self.__class__.__name__ - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() self.log_msg(f"{self.class_name}.__init__") # populated in self._build_policy_attach_payload() self.payloads = [] @@ -457,10 +458,10 @@ def __init__(self, module): msg += f"got {switch.keys()}" self.module.fail_json(msg) - self.log_msg(f"{self.class_name}.__init__: instantiate NdfcSwitchDetails") - self.switch_details = NdfcSwitchDetails(self.module) - self.log_msg(f"{self.class_name}.__init__: instantiate NdfcImagePolicies") - self.image_policies = NdfcImagePolicies(self.module) + self.log_msg(f"{self.class_name}.__init__: instantiate SwitchDetails") + self.switch_details = SwitchDetails(self.module) + self.log_msg(f"{self.class_name}.__init__: instantiate ImagePolicies") + self.image_policies = ImagePolicies(self.module) def get_have(self): """ @@ -468,7 +469,7 @@ def get_have(self): Determine current switch ISSU state on NDFC """ - self.have = NdfcSwitchIssuDetailsByIpAddress(self.module) + self.have = SwitchIssuDetailsByIpAddress(self.module) self.have.refresh() def get_want(self): @@ -488,7 +489,7 @@ def _get_idempotent_want(self, want): """ Return an itempotent want item based on the have item contents. - The have item is obtained from an instance of NdfcSwitchIssuDetails + The have item is obtained from an instance of SwitchIssuDetails created in self.get_have(). want structure passed to this method: @@ -517,7 +518,7 @@ def _get_idempotent_want(self, want): The returned idempotent_want structure is identical to the above structure, except that the policy_changed key is added, and values are modified based on results from the have item, - and the information returned by NdfcImageInstallOptions. + and the information returned by ImageInstallOptions. Caller: self.get_need_merged() """ @@ -556,10 +557,10 @@ def _get_idempotent_want(self, want): # Get relevant install options from NDFC based on the # options in our want item - instance = NdfcImageInstallOptions(self.module) + instance = ImageInstallOptions(self.module) instance.policy_name = want["policy"] msg = f"REMOVE: {self.class_name}._get_idempotent_want() " - msg += f"calling NdfcImageInstallOptions.refresh() with " + msg += f"calling ImageInstallOptions.refresh() with " msg += f"serial_number {self.have.serial_number} " msg += f"ip_address {self.have.ip_address} " msg += f"device_name {self.have.device_name}" @@ -883,7 +884,7 @@ def _validate_switch_configs(self): NOTES: 1. Final application of missing default parameters is done in - NdfcImageUpgrade.commit() + ImageUpgrade.commit() Callers: - self.get_want @@ -981,7 +982,7 @@ def _stage_images(self, serial_numbers): Callers: - handle_merged_state """ - instance = NdfcImageStage(self.module) + instance = ImageStage(self.module) instance.serial_numbers = serial_numbers instance.commit() @@ -992,11 +993,11 @@ def _validate_images(self, serial_numbers): Callers: - handle_merged_state """ - instance = NdfcImageValidate(self.module) + instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers - # TODO:2 Discuss with Mike/Shangxin - NdfcImageValidate.non_disruptive + # TODO:2 Discuss with Mike/Shangxin - ImageValidate.non_disruptive # Should we add this option to the playbook? - # It's supported in NdfcImageValidate with default of False + # It's supported in ImageValidate with default of False # instance.non_disruptive = False instance.commit() @@ -1035,7 +1036,7 @@ def _verify_install_options(self, devices): """ if len(devices) == 0: return - install_options = NdfcImageInstallOptions(self.module) + install_options = ImageInstallOptions(self.module) # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests self.switch_details.refresh() for device in devices: @@ -1067,10 +1068,10 @@ def _upgrade_images(self, devices): - handle_merged_state """ self.log_msg(f"REMOVE: {self.class_name}._upgrade_images: devices: {devices}") - upgrade = NdfcImageUpgrade(self.module) + upgrade = ImageUpgrade(self.module) upgrade.devices = devices # TODO:2 Discuss with Mike/Shangxin. Upgrade option handling mutex options. - # I'm leaning toward doing this in NdfcImageUpgrade().validate_options() + # I'm leaning toward doing this in ImageUpgrade().validate_options() # which would cover the various scenarios and fail_json() on invalid # combinations. # For epld upgrade disrutive must be True and non_disruptive must be False @@ -1089,13 +1090,13 @@ def handle_merged_state(self): Caller: main() """ - # TODO:1 Replace these with NdfcImagePolicyAction + # TODO:1 Replace these with ImagePolicyAction # See commented code below self._build_policy_attach_payload() self._send_policy_attach_payload() # Use (or not) below for policy attach/detach - # instance = NdfcImagePolicyAction(self.module) + # instance = ImagePolicyAction(self.module) # instance.policy_name = "NR3F" # instance.action = "attach" # or detach # instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] @@ -1172,7 +1173,7 @@ def handle_deleted_state(self): if len(detach_policy_devices) == 0: self.result = dict(changed=False, diff=[], response=[]) return - instance = NdfcImagePolicyAction(self.module) + instance = ImagePolicyAction(self.module) for policy_name in detach_policy_devices: msg = f"REMOVE: {self.class_name}.handle_deleted_state: " msg += f"detach policy_name: {policy_name}" @@ -1189,7 +1190,7 @@ def handle_query_state(self): Caller: main() """ - instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() msg = f"REMOVE: {self.class_name}.handle_query_state: " msg += f"Entered. self.need {self.need}" @@ -1251,40 +1252,40 @@ def main(): state=dict(default="merged", choices=["merged", "deleted", "query"]), ) - module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) - dcnm_module = NdfcAnsibleImageUpgrade(module) - dcnm_module.validate_input() - dcnm_module.get_have() - dcnm_module.get_want() - - if module.params["state"] == "merged": - dcnm_module.get_need_merged() - elif module.params["state"] == "deleted": - dcnm_module.get_need_deleted() - elif module.params["state"] == "query": - dcnm_module.get_need_query() - - if module.params["state"] == "query": - dcnm_module.result["changed"] = False - if module.params["state"] in ["merged", "deleted"]: - if dcnm_module.need: - dcnm_module.result["changed"] = True + ansible_module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) + task_module = ImageUpgradeTask(ansible_module) + task_module.validate_input() + task_module.get_have() + task_module.get_want() + + if ansible_module.params["state"] == "merged": + task_module.get_need_merged() + elif ansible_module.params["state"] == "deleted": + task_module.get_need_deleted() + elif ansible_module.params["state"] == "query": + task_module.get_need_query() + + if ansible_module.params["state"] == "query": + task_module.result["changed"] = False + if ansible_module.params["state"] in ["merged", "deleted"]: + if task_module.need: + task_module.result["changed"] = True else: - module.exit_json(**dcnm_module.result) + ansible_module.exit_json(**task_module.result) - if module.check_mode: - dcnm_module.result["changed"] = False - module.exit_json(**dcnm_module.result) + if ansible_module.check_mode: + task_module.result["changed"] = False + ansible_module.exit_json(**task_module.result) - if dcnm_module.need: - if module.params["state"] == "merged": - dcnm_module.handle_merged_state() - elif module.params["state"] == "deleted": - dcnm_module.handle_deleted_state() - elif module.params["state"] == "query": - dcnm_module.handle_query_state() + if task_module.need: + if ansible_module.params["state"] == "merged": + task_module.handle_merged_state() + elif ansible_module.params["state"] == "deleted": + task_module.handle_deleted_state() + elif ansible_module.params["state"] == "query": + task_module.handle_query_state() - module.exit_json(**dcnm_module.result) + ansible_module.exit_json(**task_module.result) if __name__ == "__main__": diff --git a/plugins/modules/dcnm_image_upgrade_orig.py b/plugins/modules/dcnm_image_upgrade_orig.py index 347d5c3eb..b6d05b1d9 100644 --- a/plugins/modules/dcnm_image_upgrade_orig.py +++ b/plugins/modules/dcnm_image_upgrade_orig.py @@ -395,7 +395,7 @@ - ip_address: 192.168.1.2 """ -class NdfcEndpoints: +class ApiEndpoints: """ Endpoints for NDFC API calls """ @@ -470,7 +470,7 @@ def issu_info(self): return endpoint @property - def ndfc_version(self): + def controller_version(self): path = f"{self.endpoint_feature_manager}/about/version" endpoint = {} endpoint["path"] = path @@ -544,7 +544,7 @@ def switches_info(self): return endpoint -class NdfcAnsibleImageUpgradeCommon: +class ImageUpgradeCommon: """ Base class for the other classes in this file """ @@ -555,7 +555,7 @@ def __init__(self, module): self.debug = True self.fd = None self.logfile = "/tmp/dcnm_image_upgrade.log" - self.endpoints = NdfcEndpoints() + self.endpoints = ApiEndpoints() def _handle_response(self, response, verb): @@ -674,7 +674,7 @@ def make_none(self, value): return value -class NdfcAnsibleImageUpgrade(NdfcAnsibleImageUpgradeCommon): +class ImageUpgradeTask(ImageUpgradeCommon): """ Ansible support for image policy attach, detach, and query. """ @@ -725,10 +725,10 @@ def __init__(self, module): msg += f"got {switch.keys()}" self.module.fail_json(msg) - self.log_msg(f"{self.class_name}.__init__: instantiate NdfcSwitchDetails") - self.switch_details = NdfcSwitchDetails(self.module) - self.log_msg(f"{self.class_name}.__init__: instantiate NdfcImagePolicies") - self.image_policies = NdfcImagePolicies(self.module) + self.log_msg(f"{self.class_name}.__init__: instantiate SwitchDetails") + self.switch_details = SwitchDetails(self.module) + self.log_msg(f"{self.class_name}.__init__: instantiate ImagePolicies") + self.image_policies = ImagePolicies(self.module) def get_have(self): """ @@ -736,7 +736,7 @@ def get_have(self): Determine current switch ISSU state on NDFC """ - self.have = NdfcSwitchIssuDetailsByIpAddress(self.module) + self.have = SwitchIssuDetailsByIpAddress(self.module) self.have.refresh() def get_want(self): @@ -756,7 +756,7 @@ def _get_idempotent_want(self, want): """ Return an itempotent want item based on the have item contents. - The have item is obtained from an instance of NdfcSwitchIssuDetails + The have item is obtained from an instance of SwitchIssuDetails created in self.get_have(). want structure passed to this method: @@ -785,7 +785,7 @@ def _get_idempotent_want(self, want): The returned idempotent_want structure is identical to the above structure, except that the policy_changed key is added, and values are modified based on results from the have item, - and the information returned by NdfcImageInstallOptions. + and the information returned by ImageInstallOptions. Caller: self.get_need_merged() """ @@ -824,10 +824,10 @@ def _get_idempotent_want(self, want): # Get relevant install options from NDFC based on the # options in our want item - instance = NdfcImageInstallOptions(self.module) + instance = ImageInstallOptions(self.module) instance.policy_name = want["policy"] msg = f"REMOVE: {self.class_name}._get_idempotent_want() " - msg += f"calling NdfcImageInstallOptions.refresh() with " + msg += f"calling ImageInstallOptions.refresh() with " msg += f"serial_number {self.have.serial_number} " msg += f"ip_address {self.have.ip_address} " msg += f"device_name {self.have.device_name}" @@ -1151,7 +1151,7 @@ def _validate_switch_configs(self): NOTES: 1. Final application of missing default parameters is done in - NdfcImageUpgrade.commit() + ImageUpgrade.commit() Callers: - self.get_want @@ -1249,7 +1249,7 @@ def _stage_images(self, serial_numbers): Callers: - handle_merged_state """ - instance = NdfcImageStage(self.module) + instance = ImageStage(self.module) instance.serial_numbers = serial_numbers instance.commit() @@ -1260,11 +1260,11 @@ def _validate_images(self, serial_numbers): Callers: - handle_merged_state """ - instance = NdfcImageValidate(self.module) + instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers - # TODO:2 Discuss with Mike/Shangxin - NdfcImageValidate.non_disruptive + # TODO:2 Discuss with Mike/Shangxin - ImageValidate.non_disruptive # Should we add this option to the playbook? - # It's supported in NdfcImageValidate with default of False + # It's supported in ImageValidate with default of False # instance.non_disruptive = False instance.commit() @@ -1303,7 +1303,7 @@ def _verify_install_options(self, devices): """ if len(devices) == 0: return - install_options = NdfcImageInstallOptions(self.module) + install_options = ImageInstallOptions(self.module) # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests self.switch_details.refresh() for device in devices: @@ -1335,10 +1335,10 @@ def _upgrade_images(self, devices): - handle_merged_state """ self.log_msg(f"REMOVE: {self.class_name}._upgrade_images: devices: {devices}") - upgrade = NdfcImageUpgrade(self.module) + upgrade = ImageUpgrade(self.module) upgrade.devices = devices # TODO:2 Discuss with Mike/Shangxin. Upgrade option handling mutex options. - # I'm leaning toward doing this in NdfcImageUpgrade().validate_options() + # I'm leaning toward doing this in ImageUpgrade().validate_options() # which would cover the various scenarios and fail_json() on invalid # combinations. # For epld upgrade disrutive must be True and non_disruptive must be False @@ -1357,13 +1357,13 @@ def handle_merged_state(self): Caller: main() """ - # TODO:1 Replace these with NdfcImagePolicyAction + # TODO:1 Replace these with ImagePolicyAction # See commented code below self._build_policy_attach_payload() self._send_policy_attach_payload() # Use (or not) below for policy attach/detach - # instance = NdfcImagePolicyAction(self.module) + # instance = ImagePolicyAction(self.module) # instance.policy_name = "NR3F" # instance.action = "attach" # or detach # instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] @@ -1440,7 +1440,7 @@ def handle_deleted_state(self): if len(detach_policy_devices) == 0: self.result = dict(changed=False, diff=[], response=[]) return - instance = NdfcImagePolicyAction(self.module) + instance = ImagePolicyAction(self.module) for policy_name in detach_policy_devices: msg = f"REMOVE: {self.class_name}.handle_deleted_state: " msg += f"detach policy_name: {policy_name}" @@ -1457,7 +1457,7 @@ def handle_query_state(self): Caller: main() """ - instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() msg = f"REMOVE: {self.class_name}.handle_query_state: " msg += f"Entered. self.need {self.need}" @@ -1508,14 +1508,14 @@ def _failure(self, resp): self.module.fail_json(msg=res) -class NdfcSwitchDetails(NdfcAnsibleImageUpgradeCommon): +class SwitchDetails(ImageUpgradeCommon): """ Retrieve switch details from NDFC and provide property accessors for the switch attributes. Usage (where module is an instance of AnsibleModule): - instance = NdfcSwitchDetails(module) + instance = SwitchDetails(module) instance.refresh() instance.ip_address = 10.1.1.1 fabric_name = instance.fabric_name @@ -1536,9 +1536,9 @@ def __init__(self, module): def _init_properties(self): self.properties = {} self.properties["ip_address"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None + self.properties["response_data"] = None + self.properties["response"] = None + self.properties["result"] = None def refresh(self): """ @@ -1550,24 +1550,24 @@ def refresh(self): verb = self.endpoints.switches_info.get("verb") self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_response {self.ndfc_response}" + self.properties["response"] = dcnm_send(self.module, verb, path) + msg = f"REMOVE: {self.class_name}.refresh: self.response {self.response}" self.log_msg(msg) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_result {self.ndfc_result}" + self.properties["result"] = self._handle_response(self.response, verb) + msg = f"REMOVE: {self.class_name}.refresh: self.result {self.result}" self.log_msg(msg) - if self.ndfc_response["RETURN_CODE"] != 200: + if self.response["RETURN_CODE"] != 200: msg = "Unable to retrieve switch information from NDFC. " - msg += f"Got response {self.ndfc_response}" + msg += f"Got response {self.response}" self.module.fail_json(msg) - data = self.ndfc_response.get("DATA") - self.properties["ndfc_data"] = {} + data = self.response.get("DATA") + self.properties["response_data"] = {} for switch in data: - self.properties["ndfc_data"][switch["ipAddress"]] = switch + self.properties["response_data"][switch["ipAddress"]] = switch - msg = f"REMOVE: {self.class_name}.refresh: self.ndfc_data {self.ndfc_data}" + msg = f"REMOVE: {self.class_name}.refresh: self.response_data {self.response_data}" self.log_msg(msg) def _get(self, item): @@ -1577,7 +1577,7 @@ def _get(self, item): self.module.fail_json(msg) return self.make_boolean( self.make_none( - self.properties["ndfc_data"][self.ip_address].get(item) + self.properties["response_data"][self.ip_address].get(item) ) ) @@ -1631,30 +1631,30 @@ def model(self): return self._get("model") @property - def ndfc_data(self): + def response_data(self): """ Return parsed data from the GET request. Return None otherwise NOTE: Keyed on ip_address """ - return self.properties["ndfc_data"] + return self.properties["response_data"] @property - def ndfc_response(self): + def response(self): """ Return the raw response from the GET request. Return None otherwise """ - return self.properties["ndfc_response"] + return self.properties["response"] @property - def ndfc_result(self): + def result(self): """ Return the raw result of the GET request. Return None otherwise """ - return self.properties["ndfc_result"] + return self.properties["result"] @property def platform(self): @@ -1686,7 +1686,7 @@ def serial_number(self): return self._get("serialNumber") -class NdfcImageInstallOptions(NdfcAnsibleImageUpgradeCommon): +class ImageInstallOptions(ImageUpgradeCommon): """ Retrieve install-options details for ONE switch from NDFC and provide property accessors for the policy attributes. @@ -1698,7 +1698,7 @@ class NdfcImageInstallOptions(NdfcAnsibleImageUpgradeCommon): Usage (where module is an instance of AnsibleModule): - instance = NdfcImageInstallOptions(module) + instance = ImageInstallOptions(module) # Mandatory instance.policy_name = "NR3F" instance.serial_number = "FDO211218GC" @@ -1800,9 +1800,9 @@ def _init_properties(self): self.properties = {} self.properties["epld"] = False self.properties["issu"] = True - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None + self.properties["response_data"] = None + self.properties["response"] = None + self.properties["result"] = None self.properties["package_install"] = False self.properties["policy_name"] = None self.properties["serial_number"] = None @@ -1829,22 +1829,22 @@ def refresh(self): msg = f"REMOVE: {self.class_name}.refresh: " msg += f"payload: {self.payload}" self.log_msg(msg) - self.properties["ndfc_response"] = dcnm_send( + self.properties["response"] = dcnm_send( self.module, verb, path, data=json.dumps(self.payload) ) msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"ndfc_response: {self.ndfc_response}" + msg += f"response: {self.response}" self.log_msg(msg) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.properties["result"] = self._handle_response(self.response, verb) # TODO:2 should error message contain full response or just DATA.error? - if self.ndfc_result["success"] is False: + if self.result["success"] is False: msg = f"{self.class_name}.refresh: " msg += "Bad result when retrieving install-options from NDFC. " - msg += f"NDFC response: {self.ndfc_response}" + msg += f"NDFC response: {self.response}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - self.data = self.properties["ndfc_data"] + self.properties["response_data"] = self.response.get("DATA") + self.data = self.properties["response_data"] if self.data.get("compatibilityStatusList") is None: self.compatibility_status = {} else: @@ -2035,25 +2035,25 @@ def ip_address(self): return self.compatibility_status.get("ipAddress") @property - def ndfc_data(self): + def response_data(self): """ Return the raw data from the NDFC response. """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_response(self): + def response(self): """ Return the response from NDFC of the query. """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property - def ndfc_result(self): + def result(self): """ Return the result from NDFC of the query. """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property def os_type(self): @@ -2141,7 +2141,7 @@ def version_check(self): # ============================================================================== -class NdfcImagePolicyAction(NdfcAnsibleImageUpgradeCommon): +class ImagePolicyAction(ImageUpgradeCommon): """ Perform image policy actions on NDFC on one or more switches. @@ -2152,7 +2152,7 @@ class NdfcImagePolicyAction(NdfcAnsibleImageUpgradeCommon): Usage (where module is an instance of AnsibleModule): - instance = NdfcImagePolicyAction(module) + instance = ImagePolicyAction(module) instance.policy_name = "NR3F" instance.action = "attach" # or detach, or query instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] @@ -2173,15 +2173,15 @@ def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ self._init_properties() - self.image_policies = NdfcImagePolicies(self.module) - self.switch_issu_details = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.image_policies = ImagePolicies(self.module) + self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.module) self.valid_actions = {"attach", "detach", "query"} def _init_properties(self): self.properties = {} self.properties["action"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None + self.properties["response"] = None + self.properties["result"] = None self.properties["policy_name"] = None self.properties["query_result"] = None self.properties["serial_numbers"] = None @@ -2278,7 +2278,7 @@ def _attach_policy(self): NOTES: 1. This method creates a list of responses and results which - are accessible via properties ndfc_response and ndfc_result, + are accessible via properties response and result, respectively. """ self.build_attach_payload() @@ -2296,8 +2296,8 @@ def _attach_policy(self): self.module.fail_json(msg) responses.append(response) results.append(result) - self.properties["ndfc_response"] = responses - self.properties["ndfc_result"] = results + self.properties["response"] = responses + self.properties["result"] = results def _detach_policy(self): """ @@ -2314,8 +2314,8 @@ def _detach_policy(self): result = self._handle_response(response, verb) if not result["success"]: self._failure(response) - self.properties["ndfc_response"] = response - self.properties["ndfc_result"] = result + self.properties["response"] = response + self.properties["result"] = result def _query_policy(self): """ @@ -2331,8 +2331,8 @@ def _query_policy(self): if not result["success"]: self._failure(response) self.properties["query_result"] = response.get("DATA") - self.properties["ndfc_response"] = response - self.properties["ndfc_result"] = result + self.properties["response"] = response + self.properties["result"] = result @property def query_result(self): @@ -2359,22 +2359,22 @@ def action(self, value): self.properties["action"] = value @property - def ndfc_response(self): + def response(self): """ Return the raw response from NDFC after calling commit(). In the case of attach, this is a list of responses. """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property - def ndfc_result(self): + def result(self): """ Return the raw result from NDFC after calling commit(). In the case of attach, this is a list of results. """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property def policy_name(self): @@ -2410,14 +2410,14 @@ def serial_numbers(self, value): # ============================================================================== -class NdfcImagePolicies(NdfcAnsibleImageUpgradeCommon): +class ImagePolicies(ImageUpgradeCommon): """ Retrieve image policy details from NDFC and provide property accessors for the policy attributes. Usage (where module is an instance of AnsibleModule): - instance = NdfcImagePolicies(module).refresh() + instance = ImagePolicies(module).refresh() instance.policy_name = "NR3F" if instance.name is None: print("policy NR3F does not exist on NDFC") @@ -2445,9 +2445,9 @@ def __init__(self, module): def _init_properties(self): self.properties = {} self.properties["policy_name"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None + self.properties["response_data"] = None + self.properties["response"] = None + self.properties["result"] = None def refresh(self): """ @@ -2456,21 +2456,21 @@ def refresh(self): path = self.endpoints.policies_info.get("path") verb = self.endpoints.policies_info.get("verb") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + self.properties["response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + self.properties["result"] = self._handle_response(self.response, verb) + self.log_msg(f"{self.class_name}.refresh: result: {self.result}") msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"result: {self.ndfc_result}" - if not self.ndfc_result["success"]: + msg += f"result: {self.result}" + if not self.result["success"]: msg = f"{self.class_name}.refresh: " msg += "Bad result when retriving image policy " msg += "information from NDFC." self.module.fail_json(msg) - data = self.ndfc_response.get("DATA").get("lastOperDataObject") + data = self.response.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.refresh: " msg += "Bad response when retrieving image policy " @@ -2480,14 +2480,14 @@ def refresh(self): msg = f"{self.class_name}.refresh: " msg += "NDFC has no defined image policies." self.module.fail_json(msg) - self.properties["ndfc_data"] = {} + self.properties["response_data"] = {} for policy in data: policy_name = policy.get("policyName") if policy_name is None: msg = f"{self.class_name}.refresh: " msg += "Cannot parse NDFC policy information" self.module.fail_json(msg) - self.properties["ndfc_data"][policy_name] = policy + self.properties["response_data"][policy_name] = policy def _get(self, item): if self.policy_name is None: @@ -2495,11 +2495,11 @@ def _get(self, item): msg += f"instance.policy_name must be set before " msg += f"accessing property {item}." self.module.fail_json(msg) - if self.properties['ndfc_data'].get(self.policy_name) is None: + if self.properties['response_data'].get(self.policy_name) is None: msg = f"{self.class_name}._get: " msg += f"policy_name {self.policy_name} is not defined in NDFC" self.module.fail_json(msg) - return_item = self.make_boolean(self.properties["ndfc_data"][self.policy_name].get(item)) + return_item = self.make_boolean(self.properties["response_data"][self.policy_name].get(item)) return_item = self.make_none(return_item) return return_item @@ -2531,26 +2531,26 @@ def name(self): return self._get("policyName") @property - def ndfc_data(self): + def response_data(self): """ Return the parsed data from the NDFC response as a dictionary, keyed on policy_name. """ - return self.properties["ndfc_data"] + return self.properties["response_data"] @property - def ndfc_response(self): + def response(self): """ Return the raw response from the NDFC response. """ - return self.properties["ndfc_response"] + return self.properties["response"] @property - def ndfc_result(self): + def result(self): """ Return the raw result from the NDFC response. """ - return self.properties["ndfc_result"] + return self.properties["result"] @property def policy_name(self): @@ -2648,7 +2648,7 @@ def agnostic(self): return self._get("agnostic") -class NdfcSwitchIssuDetails(NdfcAnsibleImageUpgradeCommon): +class SwitchIssuDetails(ImageUpgradeCommon): """ Retrieve switch issu details from NDFC and provide property accessors for the switch attributes. @@ -2713,9 +2713,9 @@ def __init__(self, module): def _init_properties(self): self.properties = {} - self.properties["ndfc_response"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_data"] = None + self.properties["response"] = None + self.properties["result"] = None + self.properties["response_data"] = None # action_keys is used in subclasses to determine if any actions # are in progress. # Property actions_in_progress returns True if so, False otherwise @@ -2732,23 +2732,23 @@ def refresh(self) -> None: path = self.endpoints.issu_info.get("path") verb = self.endpoints.issu_info.get("verb") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + self.properties["response"] = dcnm_send(self.module, verb, path) + self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) - self.log_msg(f"{self.class_name}.refresh: ndfc_result: {self.ndfc_result}") + self.properties["result"] = self._handle_response(self.response, verb) + self.log_msg(f"{self.class_name}.refresh: result: {self.result}") msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"result: {self.ndfc_result}" - # ndfc_result for 404 response + msg += f"result: {self.result}" + # result for 404 response # {'found': False, 'success': True} - if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: + if self.result["success"] == False or self.result["found"] == False: msg = f"{self.class_name}.refresh: " msg += "Bad result when retriving switch " msg += "information from NDFC" self.module.fail_json(msg) - data = self.ndfc_response.get("DATA").get("lastOperDataObject") + data = self.response.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.refresh: " msg += "NDFC has no switch ISSU information." @@ -2758,10 +2758,10 @@ def refresh(self) -> None: msg += "NDFC has no switch ISSU information." self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA", {}).get( + self.properties["response_data"] = self.response.get("DATA", {}).get( "lastOperDataObject", [] ) - self.log_msg(f"{self.class_name}.refresh: ndfc_response: {self.ndfc_response}") + self.log_msg(f"{self.class_name}.refresh: response: {self.response}") @property def actions_in_progress(self): @@ -2781,27 +2781,27 @@ def _get(self, item): pass @property - def ndfc_data(self): + def response_data(self): """ Return the raw data retrieved from NDFC """ - return self.properties["ndfc_data"] + return self.properties["response_data"] @property - def ndfc_response(self): + def response(self): """ Return the raw response from the GET request. Return None otherwise """ - return self.properties["ndfc_response"] + return self.properties["response"] @property - def ndfc_result(self): + def result(self): """ Return the raw result of the GET request. Return None otherwise """ - return self.properties["ndfc_result"] + return self.properties["result"] @property def device_name(self): @@ -3257,14 +3257,14 @@ def vpc_role(self): return self._get("vpcRole") -class NdfcSwitchIssuDetailsByIpAddress(NdfcSwitchIssuDetails): +class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): """ Retrieve switch issu details from NDFC and provide property accessors for the switch attributes retrieved by ip address. Usage (where module is an instance of AnsibleModule): - instance = NdfcSwitchIssuDetailsByIpAddress(module) + instance = SwitchIssuDetailsByIpAddress(module) instance.refresh() instance.ip_address = 10.1.1.1 image_staged = instance.image_staged @@ -3272,7 +3272,7 @@ class NdfcSwitchIssuDetailsByIpAddress(NdfcSwitchIssuDetails): serial_number = instance.serial_number etc... - See NdfcSwitchIssuDetails for more details. + See SwitchIssuDetails for more details. """ def __init__(self, module): @@ -3291,7 +3291,7 @@ def refresh(self): """ super().refresh() self.data_subclass = {} - for switch in self.ndfc_data: + for switch in self.response_data: msg = f"{self.class_name}.refresh: " msg += f"switch {switch}" self.data_subclass[switch["ipAddress"]] = switch @@ -3329,14 +3329,14 @@ def ip_address(self, value): self.properties["ip_address"] = value -class NdfcSwitchIssuDetailsBySerialNumber(NdfcSwitchIssuDetails): +class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): """ Retrieve switch issu details from NDFC and provide property accessors for the switch attributes retrieved by serial_number. Usage (where module is an instance of AnsibleModule): - instance = NdfcSwitchIssuDetailsBySerialNumber(module) + instance = SwitchIssuDetailsBySerialNumber(module) instance.refresh() instance.serial_number = "FDO211218GC" instance.refresh() @@ -3345,7 +3345,7 @@ class NdfcSwitchIssuDetailsBySerialNumber(NdfcSwitchIssuDetails): ip_address = instance.ip_address etc... - See NdfcSwitchIssuDetails for more details. + See SwitchIssuDetails for more details. """ @@ -3365,7 +3365,7 @@ def refresh(self): """ super().refresh() self.data_subclass = {} - for switch in self.ndfc_data: + for switch in self.response_data: self.data_subclass[switch["serialNumber"]] = switch def _get(self, item): @@ -3401,14 +3401,14 @@ def serial_number(self, value): self.properties["serial_number"] = value -class NdfcSwitchIssuDetailsByDeviceName(NdfcSwitchIssuDetails): +class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): """ Retrieve switch issu details from NDFC and provide property accessors for the switch attributes retrieved by device_name. Usage (where module is an instance of AnsibleModule): - instance = NdfcSwitchIssuDetailsByDeviceName(module) + instance = SwitchIssuDetailsByDeviceName(module) instance.refresh() instance.device_name = "leaf_1" image_staged = instance.image_staged @@ -3416,7 +3416,7 @@ class NdfcSwitchIssuDetailsByDeviceName(NdfcSwitchIssuDetails): ip_address = instance.ip_address etc... - See NdfcSwitchIssuDetails for more details. + See SwitchIssuDetails for more details. """ @@ -3436,7 +3436,7 @@ def refresh(self): """ super().refresh() self.data_subclass = {} - for switch in self.ndfc_data: + for switch in self.response_data: self.data_subclass[switch["deviceName"]] = switch def _get(self, item): @@ -3472,7 +3472,7 @@ def device_name(self, value): self.properties["device_name"] = value -class NdfcImageStage(NdfcAnsibleImageUpgradeCommon): +class ImageStage(ImageUpgradeCommon): """ Endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image @@ -3481,7 +3481,7 @@ class NdfcImageStage(NdfcAnsibleImageUpgradeCommon): Usage (where module is an instance of AnsibleModule): - stage = NdfcImageStage(module) + stage = ImageStage(module) stage.serial_numbers = ["FDO211218HH", "FDO211218GC"] stage.commit() data = stage.data @@ -3540,32 +3540,32 @@ def __init__(self, module): self.class_name = self.__class__.__name__ self._init_properties() self.serial_numbers_done = set() - self.ndfc_version = None + self.controller_version = None self.path = None self.verb = None self.payload = None - self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): self.properties = {} self.properties["serial_numbers"] = None - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None + self.properties["response_data"] = None + self.properties["result"] = None + self.properties["response"] = None self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds - def _populate_ndfc_version(self): + def _populate_controller_version(self): """ - Populate self.ndfc_version with the NDFC version. + Populate self.controller_version with the NDFC version. Notes: - 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + 1. This cannot go into ImageUpgradeCommon() due to circular imports resulting in RecursionError """ - instance = NdfcVersion(self.module) + instance = ControllerVersion(self.module) instance.refresh() - self.ndfc_version = instance.version + self.controller_version = instance.version def prune_serial_numbers(self): """ @@ -3627,25 +3627,25 @@ def commit(self): self.log_msg(f"REMOVE: {self.class_name}.commit() path: {self.path}") self.log_msg(f"REMOVE: {self.class_name}.commit() verb: {self.verb}") self.payload = {} - self._populate_ndfc_version() - if self.ndfc_version == "12.1.2e": + self._populate_controller_version() + if self.controller_version == "12.1.2e": # Yes, NDFC 12.1.2e wants serialNum to be misspelled self.payload["sereialNum"] = self.serial_numbers else: self.payload["serialNumbers"] = self.serial_numbers - self.properties["ndfc_response"] = dcnm_send( + self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, self.verb) + self.properties["result"] = self._handle_response(self.response, self.verb) self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + f"REMOVE: {self.class_name}.commit() response: {self.response}" ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + if not self.result["success"]: + msg = f"{self.class_name}.commit() failed: {self.result}. " + msg += f"NDFC response was: {self.response}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_stage_to_complete() def _wait_for_current_actions_to_complete(self): @@ -3789,28 +3789,28 @@ def serial_numbers(self, value): self.properties["serial_numbers"] = value @property - def ndfc_data(self): + def response_data(self): """ Return the result of the image staging request for serial_numbers. instance.serial_numbers must be set first. """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_result(self): + def result(self): """ Return the POST result from NDFC """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property - def ndfc_response(self): + def response(self): """ Return the POST response from NDFC """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property def check_interval(self): @@ -3843,7 +3843,7 @@ def check_timeout(self, value): self.properties["check_timeout"] = value # ============================================================================== -class NdfcImageValidate(NdfcAnsibleImageUpgradeCommon): +class ImageValidate(ImageUpgradeCommon): """ Endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image @@ -3852,12 +3852,12 @@ class NdfcImageValidate(NdfcAnsibleImageUpgradeCommon): Usage (where module is an instance of AnsibleModule): - instance = NdfcImageValidate(module) + instance = ImageValidate(module) instance.serial_numbers = ["FDO211218HH", "FDO211218GC"] # non_disruptive is optional instance.non_disruptive = True instance.commit() - data = instance.ndfc_data + data = instance.response_data Request body: { @@ -3873,38 +3873,38 @@ class NdfcImageValidate(NdfcAnsibleImageUpgradeCommon): The response is not JSON, nor is it very useful. Instead, we poll for validation status using - NdfcSwitchIssuDetailsBySerialNumber. + SwitchIssuDetailsBySerialNumber. """ def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ self._init_properties() - self.issu_detail = NdfcSwitchIssuDetailsBySerialNumber(self.module) + self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): self.properties = {} self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None + self.properties["response_data"] = None + self.properties["result"] = None + self.properties["response"] = None self.properties["non_disruptive"] = False self.properties["serial_numbers"] = None - def _populate_ndfc_version(self): + def _populate_controller_version(self): """ - Populate self.ndfc_version with the NDFC version. + Populate self.controller_version with the NDFC version. TODO:3 Remove if 12.1.3b works with no changes to request/response payloads. Notes: - 1. This cannot go into NdfcAnsibleImageUpgradeCommon() due to circular + 1. This cannot go into ImageUpgradeCommon() due to circular imports resulting in RecursionError """ - instance = NdfcVersion(self.module) + instance = ControllerVersion(self.module) instance.refresh() - self.ndfc_version = instance.version + self.controller_version = instance.version def prune_serial_numbers(self): """ @@ -3972,19 +3972,19 @@ def commit(self): path = self.endpoints.image_validate.get("path") verb = self.endpoints.image_validate.get("verb") self.build_payload() - self.properties["ndfc_response"] = dcnm_send( + self.properties["response"] = dcnm_send( self.module, verb, path, data=json.dumps(self.payload) ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.properties["result"] = self._handle_response(self.response, verb) self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + f"REMOVE: {self.class_name}.commit() response: {self.response}" ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + if not self.result["success"]: + msg = f"{self.class_name}.commit() failed: {self.result}. " + msg += f"NDFC response was: {self.response}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_validate_to_complete() def _wait_for_current_actions_to_complete(self): @@ -4153,28 +4153,28 @@ def non_disruptive(self, value): self.properties["non_disruptive"] = value @property - def ndfc_data(self): + def response_data(self): """ Return the result of the image staging request for serial_numbers. instance.serial_numbers must be set first. """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_result(self): + def result(self): """ Return the POST result from NDFC """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property - def ndfc_response(self): + def response(self): """ Return the POST response from NDFC """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property def check_interval(self): @@ -4208,17 +4208,17 @@ def check_timeout(self, value): # ============================================================================== -class NdfcImageUpgrade(NdfcAnsibleImageUpgradeCommon): +class ImageUpgrade(ImageUpgradeCommon): """ Endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image Verb: POST - TODO:3 Discuss with Mike/Shangxin. NdfcImageUpgrade.epld_upgrade, etc + TODO:3 Discuss with Mike/Shangxin. ImageUpgrade.epld_upgrade, etc Usage (where module is an instance of AnsibleModule): - upgrade = NdfcImageUpgrade(module) + upgrade = ImageUpgrade(module) upgrade.devices = devices upgrade.commit() data = upgrade.data @@ -4311,7 +4311,7 @@ def __init__(self, module): self._init_defaults() self._init_properties() - self.issu_detail = NdfcSwitchIssuDetailsByIpAddress(self.module) + self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) def _init_defaults(self): self.defaults = {} @@ -4354,9 +4354,9 @@ def _init_properties(self): self.properties["epld_module"] = "ALL" self.properties["epld_upgrade"] = False self.properties["force_non_disruptive"] = False - self.properties["ndfc_data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None + self.properties["response_data"] = None + self.properties["result"] = None + self.properties["response"] = None self.properties["non_disruptive"] = False self.properties["package_install"] = False self.properties["package_uninstall"] = False @@ -4383,9 +4383,9 @@ def _init_properties(self): # TODO:1 This prunes devices only based on the image upgrade state. # TODO:1 It does not check other image states and EPLD states. # """ - # # issu = NdfcSwitchIssuDetailsBySerialNumber(self.module) + # # issu = SwitchIssuDetailsBySerialNumber(self.module) # pruned_devices = set() - # instance = NdfcSwitchIssuDetailsByIpAddress(self.module) + # instance = SwitchIssuDetailsByIpAddress(self.module) # instance.refresh() # for device in self.devices: # msg = f"REMOVE: {self.class_name}.prune_devices() device: {device}" @@ -4595,19 +4595,19 @@ def commit(self): for device in self.devices: self.build_payload(device) self.log_msg(f"REMOVE: {self.class_name}.commit() upgrade payload: {self.payload}") - self.properties["ndfc_response"] = dcnm_send( + self.properties["response"] = dcnm_send( self.module, verb, path, data=json.dumps(self.payload) ) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + self.properties["result"] = self._handle_response(self.response, verb) self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.ndfc_response}" + f"REMOVE: {self.class_name}.commit() response: {self.response}" ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.ndfc_result}") - if not self.ndfc_result["success"]: - msg = f"{self.class_name}.commit() failed: {self.ndfc_result}. " - msg += f"NDFC response was: {self.ndfc_response}" + self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + if not self.result["success"]: + msg = f"{self.class_name}.commit() failed: {self.result}. " + msg += f"NDFC response was: {self.response}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_upgrade_to_complete() def _wait_for_current_actions_to_complete(self): @@ -4968,32 +4968,32 @@ def check_timeout(self): return self.properties.get("check_timeout") @property - def ndfc_data(self): + def response_data(self): """ Return the data retrieved from NDFC for the image upgrade request. instance.devices must be set first. instance.commit() must be called first. """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_result(self): + def result(self): """ Return the POST result from NDFC instance.devices must be set first. instance.commit() must be called first. """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property - def ndfc_response(self): + def response(self): """ Return the POST response from NDFC instance.devices must be set first. instance.commit() must be called first. """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property def serial_numbers(self): @@ -5003,21 +5003,21 @@ def serial_numbers(self): return [device.get("serial_number") for device in self.devices] -class NdfcVersion(NdfcAnsibleImageUpgradeCommon): +class ControllerVersion(ImageUpgradeCommon): """ Return image version information from NDFC NOTES: 1. considered using dcnm_version_supported() but it does not return minor release info, which is needed due to key changes between - 12.1.2e and 12.1.3b. For example, see NdfcImageStage().commit() + 12.1.2e and 12.1.3b. For example, see ImageStage().commit() Endpoint: /appcenter/cisco/ndfc/api/v1/fm/about/version Usage (where module is an instance of AnsibleModule): - instance = NdfcVersion(module) + instance = ControllerVersion(module) instance.refresh() if instance.version == "12.1.2e": do 12.1.2e stuff @@ -5045,40 +5045,40 @@ def __init__(self, module): def _init_properties(self): self.properties = {} self.properties["data"] = None - self.properties["ndfc_result"] = None - self.properties["ndfc_response"] = None + self.properties["result"] = None + self.properties["response"] = None def refresh(self): """ - Refresh self.ndfc_data with current version info from NDFC + Refresh self.response_data with current version info from NDFC """ - path = self.endpoints.ndfc_version.get("path") - verb = self.endpoints.ndfc_version.get("verb") - self.properties["ndfc_response"] = dcnm_send(self.module, verb, path) - self.properties["ndfc_result"] = self._handle_response(self.ndfc_response, verb) + path = self.endpoints.controller_version.get("path") + verb = self.endpoints.controller_version.get("verb") + self.properties["response"] = dcnm_send(self.module, verb, path) + self.properties["result"] = self._handle_response(self.response, verb) - msg = f"REMOVE: {self.class_name}.refresh() ndfc_response: {self.ndfc_response}" + msg = f"REMOVE: {self.class_name}.refresh() response: {self.response}" self.log_msg(msg) - msg = f"REMOVE: {self.class_name}.refresh() ndfc_result: {self.ndfc_result}" + msg = f"REMOVE: {self.class_name}.refresh() result: {self.result}" self.log_msg(msg) - if self.ndfc_result["success"] == False or self.ndfc_result["found"] == False: - msg = f"{self.class_name}.refresh() failed: {self.ndfc_result}" + if self.result["success"] == False or self.result["found"] == False: + msg = f"{self.class_name}.refresh() failed: {self.result}" self.module.fail_json(msg) - self.properties["ndfc_data"] = self.ndfc_response.get("DATA") - if self.ndfc_data is None: + self.properties["response_data"] = self.response.get("DATA") + if self.response_data is None: msg = f"{self.class_name}.refresh() failed: NDFC response " msg += "does not contain DATA key. NDFC response: " - msg += f"{self.ndfc_response}" + msg += f"{self.response}" self.module.fail_json(msg) - msg = f"REMOVE: {self.class_name}.refresh() ndfc_data: {self.ndfc_data}" + msg = f"REMOVE: {self.class_name}.refresh() response_data: {self.response_data}" self.log_msg(msg) def _get(self, item): - return self.make_boolean(self.make_none(self.ndfc_data.get(item))) + return self.make_boolean(self.make_none(self.response_data.get(item))) @property def dev(self): @@ -5150,25 +5150,25 @@ def is_upgrade_inprogress(self): return self.make_boolean(self._get("is_upgrade_inprogress")) @property - def ndfc_data(self): + def response_data(self): """ Return the data retrieved from the request """ - return self.properties.get("ndfc_data") + return self.properties.get("response_data") @property - def ndfc_result(self): + def result(self): """ Return the GET result from NDFC """ - return self.properties.get("ndfc_result") + return self.properties.get("result") @property - def ndfc_response(self): + def response(self): """ Return the GET response from NDFC """ - return self.properties.get("ndfc_response") + return self.properties.get("response") @property def mode(self): @@ -5267,7 +5267,7 @@ def main(): ) module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) - dcnm_module = NdfcAnsibleImageUpgrade(module) + dcnm_module = ImageUpgradeTask(module) dcnm_module.validate_input() dcnm_module.get_have() dcnm_module.get_want() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_payloads.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads.json similarity index 85% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_payloads.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads.json index 5e8601d43..7ef829dae 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_payloads.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads.json @@ -1,5 +1,5 @@ { - "ndfc_image_install_options": { + "image_install_options": { "devices": [ { "serialNumber": "FDO21120U5D", @@ -10,7 +10,7 @@ "epld": "false", "packageInstall": "false" }, - "ndfc_image_install_options_epld_true": { + "image_install_options_epld_true": { "devices": [ { "serialNumber": "FDO21120U5D", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json similarity index 100% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_playbook_configs.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses.json similarity index 100% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json similarity index 91% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json index 5b1970e84..b1bc22c1a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcVersion.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json @@ -1,5 +1,5 @@ { - "NdfcVersion_get_return_code_200": { + "ControllerVersion_get_return_code_200": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -15,21 +15,21 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_get_return_code_404": { + "ControllerVersion_get_return_code_404": { "RETURN_CODE": 404, "METHOD": "GET", "REQUEST_PATH": "https://foo/noop", "MESSAGE": "Not Found", "DATA": {} }, - "NdfcVersion_get_return_code_500": { + "ControllerVersion_get_return_code_500": { "RETURN_CODE": 500, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", "MESSAGE": "Internal Server Error", "DATA": {} }, - "NdfcVersion_dev_false": { + "ControllerVersion_dev_false": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -45,7 +45,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_dev_true": { + "ControllerVersion_dev_true": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -61,7 +61,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_dev_none": { + "ControllerVersion_dev_none": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -76,7 +76,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_install_EASYFABRIC": { + "ControllerVersion_install_EASYFABRIC": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -92,7 +92,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_install_none": { + "ControllerVersion_install_none": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -107,7 +107,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_is_ha_enabled_false": { + "ControllerVersion_is_ha_enabled_false": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -123,7 +123,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_is_ha_enabled_true": { + "ControllerVersion_is_ha_enabled_true": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -139,7 +139,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_is_ha_enabled_none": { + "ControllerVersion_is_ha_enabled_none": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -154,7 +154,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_is_upgrade_inprogress_false": { + "ControllerVersion_is_upgrade_inprogress_false": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -170,7 +170,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_is_upgrade_inprogress_true": { + "ControllerVersion_is_upgrade_inprogress_true": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -186,7 +186,7 @@ "is_upgrade_inprogress": "true" } }, - "NdfcVersion_is_upgrade_inprogress_none": { + "ControllerVersion_is_upgrade_inprogress_none": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -201,7 +201,7 @@ "uuid": "" } }, - "NdfcVersion_is_media_controller_false": { + "ControllerVersion_is_media_controller_false": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -217,7 +217,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_is_media_controller_true": { + "ControllerVersion_is_media_controller_true": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -233,7 +233,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_is_media_controller_none": { + "ControllerVersion_is_media_controller_none": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -248,7 +248,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_DATA_present": { + "ControllerVersion_DATA_present": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -264,13 +264,13 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_DATA_not_present": { + "ControllerVersion_DATA_not_present": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", "MESSAGE": "OK" }, - "NdfcVersion_mode_LAN": { + "ControllerVersion_mode_LAN": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -286,7 +286,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_mode_none": { + "ControllerVersion_mode_none": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -300,7 +300,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_uuid_UUID": { + "ControllerVersion_uuid_UUID": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -316,7 +316,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_uuid_none": { + "ControllerVersion_uuid_none": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -330,7 +330,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_version_12.1.3b": { + "ControllerVersion_version_12.1.3b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -346,7 +346,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcVersion_version_none": { + "ControllerVersion_version_none": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -360,7 +360,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcImageStage_12_1_2e": { + "ImageStage_12_1_2e": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -376,7 +376,7 @@ "is_upgrade_inprogress": "false" } }, - "NdfcImageStage_12_1_3b": { + "ImageStage_12_1_3b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json similarity index 100% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImageInstallOptions.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json similarity index 97% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImagePolicies.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json index 4478a8300..399d77222 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json @@ -46,7 +46,7 @@ "MESSAGE": "OK", "DATA": {} }, - "policymgnt_policies_get_return_code_200_ndfc_has_no_defined_image_policies": { + "policymgnt_policies_get_return_code_200_controller_has_no_defined_image_policies": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json similarity index 100% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json similarity index 98% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchDetails.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json index 84cd03d90..0a9af9d5e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json @@ -1,5 +1,5 @@ { - "NdfcSwitchDetails_get_return_code_200": { + "SwitchDetails_get_return_code_200": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", @@ -235,14 +235,14 @@ } ] }, - "NdfcSwitchDetails_get_return_code_404": { + "SwitchDetails_get_return_code_404": { "RETURN_CODE": 404, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitchess", "MESSAGE": "Not Found", "DATA": {} }, - "NdfcSwitchDetails_get_return_code_500": { + "SwitchDetails_get_return_code_500": { "RETURN_CODE": 500, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json similarity index 98% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json rename to tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 7e2f5ad73..3531636a1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/dcnm_image_upgrade_responses_NdfcSwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -57,7 +57,7 @@ "MESSAGE": "OK", "DATA": {} }, - "packagemgnt_issu_get_return_code_200_ndfc_switch_issu_info_length_0": { + "packagemgnt_issu_get_return_code_200_switch_issu_info_length_0": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -755,7 +755,7 @@ "message": "" } }, - "NdfcImageStage_test_prune_serial_numbers": { + "ImageStage_test_prune_serial_numbers": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -962,7 +962,7 @@ "message": "" } }, - "NdfcImageStage_test_validate_serial_numbers": { + "ImageStage_test_validate_serial_numbers": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1052,7 +1052,7 @@ "message": "" } }, - "NdfcImageStage_test_wait_for_image_stage_to_complete": { + "ImageStage_test_wait_for_image_stage_to_complete": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1142,7 +1142,7 @@ "message": "" } }, - "NdfcImageStage_test_wait_for_image_stage_to_complete_fail_json": { + "ImageStage_test_wait_for_image_stage_to_complete_fail_json": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1232,7 +1232,7 @@ "message": "" } }, - "NdfcImageStage_test_wait_for_image_stage_to_complete_timeout": { + "ImageStage_test_wait_for_image_stage_to_complete_timeout": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1322,7 +1322,7 @@ "message": "" } }, - "NdfcImageStage_test_wait_for_current_actions_to_complete": { + "ImageStage_test_wait_for_current_actions_to_complete": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1412,7 +1412,7 @@ "message": "" } }, - "NdfcImageStage_test_wait_for_current_actions_to_complete_timeout": { + "ImageStage_test_wait_for_current_actions_to_complete_timeout": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1502,7 +1502,7 @@ "message": "" } }, - "NdfcImageStage_test_commit_payload_serial_number_key_name": { + "ImageStage_test_commit_payload_serial_number_key_name": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1592,7 +1592,7 @@ "message": "" } }, - "NdfcImageValidate_test_prune_serial_numbers": { + "ImageValidate_test_prune_serial_numbers": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1799,7 +1799,7 @@ "message": "" } }, - "NdfcImageValidate_test_validate_serial_numbers": { + "ImageValidate_test_validate_serial_numbers": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1889,7 +1889,7 @@ "message": "" } }, - "NdfcImageValidate_test_wait_for_image_validate_to_complete": { + "ImageValidate_test_wait_for_image_validate_to_complete": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1979,7 +1979,7 @@ "message": "" } }, - "NdfcImageValidate_test_wait_for_image_validate_to_complete_fail_json": { + "ImageValidate_test_wait_for_image_validate_to_complete_fail_json": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2069,7 +2069,7 @@ "message": "" } }, - "NdfcImageValidate_test_wait_for_image_validate_to_complete_timeout": { + "ImageValidate_test_wait_for_image_validate_to_complete_timeout": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2159,7 +2159,7 @@ "message": "" } }, - "NdfcImageValidate_test_wait_for_current_actions_to_complete": { + "ImageValidate_test_wait_for_current_actions_to_complete": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2249,7 +2249,7 @@ "message": "" } }, - "NdfcImageValidate_test_wait_for_current_actions_to_complete_timeout": { + "ImageValidate_test_wait_for_current_actions_to_complete_timeout": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2339,7 +2339,7 @@ "message": "" } }, - "NdfcImagePolicyAction_test_build_attach_payload": { + "ImagePolicyAction_test_build_attach_payload": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2546,7 +2546,7 @@ "message": "" } }, - "NdfcImagePolicyAction_test_build_attach_payload_fail_json": { + "ImagePolicyAction_test_build_attach_payload_fail_json": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py deleted file mode 100644 index 76e0921ca..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicies.py +++ /dev/null @@ -1,213 +0,0 @@ -from typing import Any, Dict - -import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# NdfcImagePolicies -# ) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import NdfcImagePolicies - -from .fixture import load_fixture - -""" -ndfc_version: 12 -description: Verify functionality of class NdfcImagePolicies -""" - -#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies.dcnm_send" -class MockAnsibleModule: - params = {} - - def fail_json(msg) -> AnsibleFailJson: - raise AnsibleFailJson(msg) - - -def response_data_ndfc_image_stage(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcImagePolicies" - response = load_fixture(response_file).get(key) - print(f"response_data_ndfc_image_stage: {key} : {response}") - return response - - -@pytest.fixture -def module(): - return NdfcImagePolicies(MockAnsibleModule) - - -def test_init_properties(module) -> None: - """ - Properties are initialized to None - """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("policy_name") == None - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None - - -def test_refresh_return_code_200(monkeypatch, module) -> None: - """ - Properties are initialized based on 200 response from endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policies - """ - key = "policymgnt_policies_get_return_code_200" - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") - return response_data_ndfc_image_stage(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - module.policy_name = "KR5M" - assert isinstance(module.ndfc_response, dict) - assert module.agnostic == False - assert module.description == "10.2.(5) with EPLD" - assert module.epld_image_name == "n9000-epld.10.2.5.M.img" - assert module.image_name == "nxos64-cs.10.2.5.M.bin" - assert module.nxos_version == "10.2.5_nxos64-cs_64bit" - assert module.package_name == None - assert module.platform == "N9K/N3K" - assert module.platform_policies == None - assert module.policy_name == "KR5M" - assert module.policy_type == "PLATFORM" - assert module.ref_count == 10 - assert module.rpm_images == None - - -def test_ndfc_result_return_code_200(monkeypatch, module) -> None: - """ - ndfc_result contains expected key/values on 200 response from endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policies - """ - key = "policymgnt_policies_get_return_code_200" - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") - return response_data_ndfc_image_stage(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert isinstance(module.ndfc_result, dict) - assert module.ndfc_result.get("found") == True - assert module.ndfc_result.get("success") == True - - -def test_ndfc_result_return_code_404(monkeypatch, module) -> None: - """ - fail_json is called on 404 response from malformed endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess - """ - key = "policymgnt_policies_get_return_code_404" - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") - return response_data_ndfc_image_stage(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - error_message = "NdfcImagePolicies.refresh: Bad response when retrieving " - error_message += "image policy information from NDFC." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() - - -def test_ndfc_result_return_code_200_empty_data(monkeypatch, module) -> None: - """ - fail_json is called on 200 response with empty DATA key. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess - """ - key = "policymgnt_policies_get_return_code_200_empty_DATA" - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") - return response_data_ndfc_image_stage(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - error_message = "NdfcImagePolicies.refresh: Bad response when retrieving " - error_message += "image policy information from NDFC." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() - - -def test_ndfc_result_return_code_200_ndfc_has_no_defined_image_policies( - monkeypatch, module -) -> None: - """ - fail_json is called on 200 response with DATA.lastOperDataObject length 0. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess - """ - key = "policymgnt_policies_get_return_code_200" - key += "_ndfc_has_no_defined_image_policies" - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") - return response_data_ndfc_image_stage(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - error_message = "NdfcImagePolicies.refresh: " - error_message += "NDFC has no defined image policies." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() - - -def test_policy_name_not_found(monkeypatch, module) -> None: - """ - fail_json() is called if response does not contain policy_name. - i.e. image policy with name FOO has not yet been created on NDFC. - """ - key = "policymgnt_policies_get_return_code_200" - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") - return response_data_ndfc_image_stage(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - module.policy_name = "FOO" - error_message = "NdfcImagePolicies._get: " - error_message += "policy_name FOO is not defined in NDFC" - with pytest.raises(AnsibleFailJson, match=error_message): - module.policy_type == "PLATFORM" - - -def test_get_with_policy_name_None(module) -> None: - """ - fail_json is called when _get() is called prior to setting policy_name. - """ - error_message = "NdfcImagePolicies._get: instance.policy_name must be " - error_message += "set before accessing property imageName." - with pytest.raises(AnsibleFailJson, match=error_message): - module._get("imageName") - - -def test_ndfc_result_return_code_200_policy_name_missing_in_response( - monkeypatch, module -) -> None: - """ - fail_json is called on 200 response with missing policyName key. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess - - NOTE: This is to cover a check in NdfcImagePolicies.refresh() for a scenario that should never happen. - TODO: Consider removing this check, and this testcase. - """ - key = "policymgnt_policies_get_return_code_200" - key += "_policyName_missing_in_response" - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data_ndfc_image_stage(key)}") - return response_data_ndfc_image_stage(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - error_message = "NdfcImagePolicies.refresh: " - error_message += "Cannot parse NDFC policy information" - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcVersion.py deleted file mode 100644 index ae15399df..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcVersion.py +++ /dev/null @@ -1,570 +0,0 @@ -""" -ndfc_version: 12 -description: Verify functionality of NdfcVersion -""" - -from typing import Any, Dict - -import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import NdfcVersion -from .fixture import load_fixture - -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" - -def response_data_ndfc_version(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcVersion" - response = load_fixture(response_file).get(key) - print(f"response_data_ndfc_version: {key} : {response}") - return response - - -class MockAnsibleModule: - params = {} - - def fail_json(msg) -> AnsibleFailJson: - raise AnsibleFailJson(msg) - - -@pytest.fixture -def module(): - return NdfcVersion(MockAnsibleModule) - - -@pytest.fixture -def mock_ndfc_version() -> NdfcVersion: - return NdfcVersion(MockAnsibleModule) - - -def test_init_properties(module) -> None: - """ - Properties are initialized to expected values - """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None - - -@pytest.mark.parametrize( - "key, expected", - [ - ("NdfcVersion_dev_false", False), - ("NdfcVersion_dev_true", True), - ("NdfcVersion_dev_none", None), - ], -) -def test_dev(monkeypatch, module, key, expected) -> None: - """ - Function description: - - NdfcVersion.dev returns: - - True if NDFC is a development version - - False if NDFC is not a development version - - None otherwise - - Expectations: - - 1. NdfcVersion.dev returns above values given corresponding responses - - Expected results: - - 1. NdfcVersion_dev_false == False - 2. NdfcVersion_dev_true == True - 3. NdfcVersion_dev_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.dev == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("NdfcVersion_install_EASYFABRIC", "EASYFABRIC"), - ("NdfcVersion_install_none", None), - ], -) -def test_install(monkeypatch, module, key, expected) -> None: - """ - Description: - - NdfcVersion.install returns: - - Value of NDFC response "install" key, if present - - None, if NDFC response "install" key is missing - - Expectations: - - 1. NdfcVersion.install returns above values given - corresponding responses - - Expected results: - - 1. NdfcVersion_install_EASYFABRIC == "EASYFABRIC" - 2. NdfcVersion_install_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.install == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("NdfcVersion_is_ha_enabled_true", True), - ("NdfcVersion_is_ha_enabled_false", False), - ("NdfcVersion_is_ha_enabled_none", None), - ], -) -def test_is_ha_enabled(monkeypatch, module, key, expected) -> None: - """ - Function description: - - NdfcVersion.is_ha_enabled returns: - - True, if NDFC response "isHaEnabled" key == "true" - - False, if NDFC response "isHaEnabled" key == "false" - - None, if NDFC response "isHaEnabled" key is missing - - Expectations: - - 1. install returns above values given corresponding responses - - Expected results: - - 1. NdfcVersion_is_ha_enabled_true == True - 2. NdfcVersion_is_ha_enabled_false == False - 3. NdfcVersion_is_ha_enabled_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.is_ha_enabled == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("NdfcVersion_is_media_controller_true", True), - ("NdfcVersion_is_media_controller_false", False), - ("NdfcVersion_is_media_controller_none", None), - ], -) -def test_is_media_controller(monkeypatch, module, key, expected) -> None: - """ - Function description: - - NdfcVersion.is_media_controller returns: - - True, if NDFC response "isMediaController" key == "true" - - False, if NDFC response "isMediaController" key == "false" - - None, if NDFC response "isMediaController" key is missing - - Expectations: - - 1. NdfcVersion.is_media_controller returns above values - given corresponding responses - - Expected results: - - 1. NdfcVersion_is_media_controller_true == True - 2. NdfcVersion_is_media_controller_false == False - 3. NdfcVersion_is_media_controller_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.is_media_controller == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("NdfcVersion_is_upgrade_inprogress_true", True), - ("NdfcVersion_is_upgrade_inprogress_false", False), - ("NdfcVersion_is_upgrade_inprogress_none", None), - ], -) -def test_is_upgrade_inprogress(monkeypatch, module, key, expected) -> None: - """ - Function description: - - NdfcVersion.is_ha_enabled returns: - - True, if NDFC response "is_upgrade_inprogress" key == "true" - - False, if NDFC response "is_upgrade_inprogress" key == "false" - - None, if NDFC response "is_upgrade_inprogress" key is missing - - Expectations: - - 1. NdfcVersion.is_upgrade_inprogress returns above values - given corresponding responses - - Expected results: - - 1. NdfcVersion_is_upgrade_inprogress_true == True - 2. NdfcVersion_is_upgrade_inprogress_false == False - 3. NdfcVersion_is_upgrade_inprogress_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.is_upgrade_inprogress == expected - - -def test_ndfc_data_present(monkeypatch, module) -> None: - """ - Function description: - - NdfcVersion.ndfc_data returns the "DATA" key in the - NDFC response, which is a dictionary of key-value pairs. - If the "DATA" key is absent, fail_json is called. - - Expectations: - - 1. NdfcVersion.ndfc_data will return a dictionary of key-value - pairs - - Expected results: - - 1. NdfcVersion_DATA_present, NdfcVersion.ndfc_data == type(dict) - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcVersion_DATA_present" - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert isinstance(module.ndfc_data, dict) - - -def test_ndfc_data_not_present(monkeypatch, module) -> None: - """ - Function description: - - See: test_ndfc_data_present - - Expectations: - - 1. fail_json is called if the "DATA" key is absent - - Expected results: - - 1. fail_json is called - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcVersion_DATA_not_present" - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - with pytest.raises(AnsibleFailJson): - module.refresh() - - -def test_ndfc_result_200(monkeypatch, module) -> None: - """ - Function description: - - NdfcVersion.ndfc_result returns the result of its superclass - method NdfcAnsibleImageUpgradeCommon._handle_response() - - Expectations: - - 1. For a 200 response with "message" key == "OK", - NdfcVersion.ndfc_result == {'found': True, 'success': True} - - Expected results: - - 1. NdfcVersion_result_200 == {'found': True, 'success': True} - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcVersion_get_return_code_200" - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.ndfc_result == {"found": True, "success": True} - - -def test_ndfc_result_404(monkeypatch, module) -> None: - """ - Function description: - - See: test_ndfc_result_200 - - Expectations: - - 1. For a 404 response with "message" key == "Not Found", - NdfcVersion.ndfc_result == {'found': False, 'success': True} - and NdfcVersion.refresh() calls fail_json - - Expected results: - - 1. fail_json is called - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcVersion_get_return_code_404" - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - with pytest.raises(AnsibleFailJson): - module.refresh() - - -def test_ndfc_result_500(monkeypatch, module) -> None: - """ - Function description: - - See: test_ndfc_result_200 - - Expectations: - - 1. For a 500 response with any "message" key value, - NdfcVersion.ndfc_result == {'found': False, 'success': False} - and NdfcVersion.refresh() calls fail_json - - Expected results: - - 1. fail_json is called - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcVersion_get_return_code_500" - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - with pytest.raises(AnsibleFailJson): - module.refresh() - - -@pytest.mark.parametrize( - "key, expected", [("NdfcVersion_mode_LAN", "LAN"), ("NdfcVersion_mode_none", None)] -) -def test_mode(monkeypatch, module, key, expected) -> None: - """ - Function description: - - NdfcVersion.mode returns: - - If NDFC response "mode" key is present, its value - - If NDFC response "mode" key is not present, None - - Expectations: - - 1. NdfcVersion.mode returns above values - given corresponding responses - - Expected results: - - 1. NdfcVersion_mode_LAN == "LAN" - 2. NdfcVersion_mode_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.mode == expected - - -@pytest.mark.parametrize( - "key, expected", - [("NdfcVersion_uuid_UUID", "foo-uuid"), ("NdfcVersion_uuid_none", None)], -) -def test_uuid(monkeypatch, module, key, expected) -> None: - """ - Function description: - - NdfcVersion.uuid returns: - - If NDFC response "uuid" key is present, its value - - If NDFC response "uuid" key is not present, None - - Expectations: - - 1. NdfcVersion.uuid returns above values - given corresponding responses - - Expected results: - - 1. NdfcVersion_uuid_UUID == "foo-uuid" - 2. NdfcVersion_uuid_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.uuid == expected - - -@pytest.mark.parametrize( - "key, expected", - [("NdfcVersion_version_12.1.3b", "12.1.3b"), ("NdfcVersion_version_none", None)], -) -def test_version(monkeypatch, module, key, expected) -> None: - """ - Function description: - - NdfcVersion.version returns: - - If NDFC response "version" key is present, its value - - If NDFC response "version" key is not present, None - - Expectations: - - 1. NdfcVersion.version returns above values - given corresponding responses - - Expected results: - - 1. NdfcVersion_version_12.1.3b == "12.1.3b" - 2. NdfcVersion_version_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.version == expected - - -@pytest.mark.parametrize( - "key, expected", - [("NdfcVersion_version_12.1.3b", "12"), ("NdfcVersion_version_none", None)], -) -def test_version_major(monkeypatch, module, key, expected) -> None: - """ - Function description: - - version_major returns the major version of NDFC - It derives this from the "version" key in the NDFC response - by splitting the string on "." and returning the first element - - NdfcVersion.version_major returns: - - If NDFC response "version" key is present, the major version - - If NDFC response "version" key is not present, None - - Expectations: - - 1. NdfcVersion.version_major returns above values - given corresponding responses - - Expected results: - - 1. NdfcVersion_version_12.1.3b == "12" - 2. NdfcVersion_version_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.version_major == expected - - -@pytest.mark.parametrize( - "key, expected", - [("NdfcVersion_version_12.1.3b", "1"), ("NdfcVersion_version_none", None)], -) -def test_version_minor(monkeypatch, module, key, expected) -> None: - """ - Function description: - - version_minor returns the minor version of NDFC - It derives this from the "version" key in the NDFC response - by splitting the string on "." and returning the second element - - NdfcVersion.version_minor returns: - - If NDFC response "version" key is present, the minor version - - If NDFC response "version" key is not present, None - - Expectations: - - 1. NdfcVersion.version_minor returns above values - given corresponding responses - - Expected results: - - 1. NdfcVersion_version_12.1.3b == "1" - 2. NdfcVersion_version_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.version_minor == expected - - -@pytest.mark.parametrize( - "key, expected", - [("NdfcVersion_version_12.1.3b", "3b"), ("NdfcVersion_version_none", None)], -) -def test_version_patch(monkeypatch, module, key, expected) -> None: - """ - Function description: - - version_patch returns the patch version of NDFC - It derives this from the "version" key in the NDFC response - by splitting the string on "." and returning the third element - - NdfcVersion.version_patch returns: - - If NDFC response "version" key is present, the patch version - - If NDFC response "version" key is not present, None - - Expectations: - - 1. NdfcVersion.version_patch returns above values - given corresponding responses - - Expected results: - - 1. NdfcVersion_version_12.1.3b == "3b" - 2. NdfcVersion_version_none == None - """ - - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) - - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - - module.refresh() - assert module.version_patch == expected diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py similarity index 86% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py index d8f6d3a99..aec3a01c5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py @@ -1,11 +1,11 @@ # from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ -# NdfcEndpoints +# ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints """ -ndfc_version: 12 -description: Verify that class NdfcEndpoints returns the correct endpoints +controller_version: 12 +description: Verify that class ApiEndpoints returns the correct API endpoints """ @@ -13,7 +13,7 @@ def test_dcnm_image_upgrade_endpoints_init() -> None: """ Endpoints.__init__ """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() endpoints.__init__() assert endpoints.endpoint_api_v1 == "/appcenter/cisco/ndfc/api/v1" assert endpoints.endpoint_feature_manager == "/appcenter/cisco/ndfc/api/v1/fm" @@ -44,7 +44,7 @@ def test_dcnm_image_upgrade_endpoints_bootflash_info() -> None: """ Endpoints.bootflash_info """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.bootflash_info.get("verb") == "GET" assert ( endpoints.bootflash_info.get("path") @@ -56,7 +56,7 @@ def test_dcnm_image_upgrade_endpoints_install_options() -> None: """ Endpoints.install_options """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.install_options.get("verb") == "POST" assert ( endpoints.install_options.get("path") @@ -68,7 +68,7 @@ def test_dcnm_image_upgrade_endpoints_image_stage() -> None: """ Endpoints.image_stage """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.image_stage.get("verb") == "POST" assert ( endpoints.image_stage.get("path") @@ -80,7 +80,7 @@ def test_dcnm_image_upgrade_endpoints_image_upgrade() -> None: """ Endpoints.image_upgrade """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.image_upgrade.get("verb") == "POST" assert ( endpoints.image_upgrade.get("path") @@ -92,7 +92,7 @@ def test_dcnm_image_upgrade_endpoints_image_validate() -> None: """ Endpoints.image_validate """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.image_validate.get("verb") == "POST" assert ( endpoints.image_validate.get("path") @@ -104,7 +104,7 @@ def test_dcnm_image_upgrade_endpoints_issu_info() -> None: """ Endpoints.issu_info """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.issu_info.get("verb") == "GET" assert ( endpoints.issu_info.get("path") @@ -112,14 +112,14 @@ def test_dcnm_image_upgrade_endpoints_issu_info() -> None: ) -def test_dcnm_image_upgrade_endpoints_ndfc_version() -> None: +def test_dcnm_image_upgrade_endpoints_controller_version() -> None: """ - Endpoints.ndfc_version + Endpoints.controller_version """ - endpoints = NdfcEndpoints() - assert endpoints.ndfc_version.get("verb") == "GET" + endpoints = ApiEndpoints() + assert endpoints.controller_version.get("verb") == "GET" assert ( - endpoints.ndfc_version.get("path") + endpoints.controller_version.get("path") == "/appcenter/cisco/ndfc/api/v1/fm/about/version" ) @@ -128,7 +128,7 @@ def test_dcnm_image_upgrade_endpoints_policies_attached_info() -> None: """ Endpoints.policies_attached_info """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.policies_attached_info.get("verb") == "GET" assert ( endpoints.policies_attached_info.get("path") @@ -140,7 +140,7 @@ def test_dcnm_image_upgrade_endpoints_policies_info() -> None: """ Endpoints.policies_info """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.policies_info.get("verb") == "GET" assert ( endpoints.policies_info.get("path") @@ -152,7 +152,7 @@ def test_dcnm_image_upgrade_endpoints_policy_attach() -> None: """ Endpoints.policy_attach """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.policy_attach.get("verb") == "POST" assert ( endpoints.policy_attach.get("path") @@ -164,7 +164,7 @@ def test_dcnm_image_upgrade_endpoints_policy_create() -> None: """ Endpoints.policy_create """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.policy_create.get("verb") == "POST" assert ( endpoints.policy_create.get("path") @@ -176,7 +176,7 @@ def test_dcnm_image_upgrade_endpoints_policy_detach() -> None: """ Endpoints.policy_detach """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.policy_detach.get("verb") == "DELETE" assert ( endpoints.policy_detach.get("path") @@ -188,7 +188,7 @@ def test_dcnm_image_upgrade_endpoints_policy_info() -> None: """ Endpoints.policy_info """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.policy_info.get("verb") == "GET" assert ( endpoints.policy_info.get("path") @@ -200,7 +200,7 @@ def test_dcnm_image_upgrade_endpoints_stage_info() -> None: """ Endpoints.stage_info """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.stage_info.get("verb") == "GET" assert ( endpoints.stage_info.get("path") @@ -212,7 +212,7 @@ def test_dcnm_image_upgrade_endpoints_switches_info() -> None: """ Endpoints.switches_info """ - endpoints = NdfcEndpoints() + endpoints = ApiEndpoints() assert endpoints.switches_info.get("verb") == "GET" assert ( endpoints.switches_info.get("path") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py new file mode 100644 index 000000000..18dcde11e --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py @@ -0,0 +1,574 @@ +""" +controller_version: 12 +description: Verify functionality of ControllerVersion +""" + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import ControllerVersion + +from .fixture import load_fixture + +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_common = patch_module_utils + "common." + +dcnm_send_version = patch_common + "controller_version.dcnm_send" + +def responses_controller_version(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ControllerVersion" + response = load_fixture(response_file).get(key) + print(f"responses_controller_version: {key} : {response}") + return response + + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +@pytest.fixture +def module(): + return ControllerVersion(MockAnsibleModule) + + +@pytest.fixture +def mock_controller_version() -> ControllerVersion: + return ControllerVersion(MockAnsibleModule) + + +def test_init_properties(module) -> None: + """ + Properties are initialized to expected values + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None + + +@pytest.mark.parametrize( + "key, expected", + [ + ("ControllerVersion_dev_false", False), + ("ControllerVersion_dev_true", True), + ("ControllerVersion_dev_none", None), + ], +) +def test_dev(monkeypatch, module, key, expected) -> None: + """ + Function description: + + ControllerVersion.dev returns: + - True if NDFC is a development version + - False if NDFC is not a development version + - None otherwise + + Expectations: + + 1. ControllerVersion.dev returns above values given corresponding responses + + Expected results: + + 1. ControllerVersion_dev_false == False + 2. ControllerVersion_dev_true == True + 3. ControllerVersion_dev_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.dev == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("ControllerVersion_install_EASYFABRIC", "EASYFABRIC"), + ("ControllerVersion_install_none", None), + ], +) +def test_install(monkeypatch, module, key, expected) -> None: + """ + Description: + + ControllerVersion.install returns: + - Value of NDFC response "install" key, if present + - None, if NDFC response "install" key is missing + + Expectations: + + 1. ControllerVersion.install returns above values given + corresponding responses + + Expected results: + + 1. ControllerVersion_install_EASYFABRIC == "EASYFABRIC" + 2. ControllerVersion_install_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.install == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("ControllerVersion_is_ha_enabled_true", True), + ("ControllerVersion_is_ha_enabled_false", False), + ("ControllerVersion_is_ha_enabled_none", None), + ], +) +def test_is_ha_enabled(monkeypatch, module, key, expected) -> None: + """ + Function description: + + ControllerVersion.is_ha_enabled returns: + - True, if NDFC response "isHaEnabled" key == "true" + - False, if NDFC response "isHaEnabled" key == "false" + - None, if NDFC response "isHaEnabled" key is missing + + Expectations: + + 1. install returns above values given corresponding responses + + Expected results: + + 1. ControllerVersion_is_ha_enabled_true == True + 2. ControllerVersion_is_ha_enabled_false == False + 3. ControllerVersion_is_ha_enabled_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.is_ha_enabled == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("ControllerVersion_is_media_controller_true", True), + ("ControllerVersion_is_media_controller_false", False), + ("ControllerVersion_is_media_controller_none", None), + ], +) +def test_is_media_controller(monkeypatch, module, key, expected) -> None: + """ + Function description: + + ControllerVersion.is_media_controller returns: + - True, if NDFC response "isMediaController" key == "true" + - False, if NDFC response "isMediaController" key == "false" + - None, if NDFC response "isMediaController" key is missing + + Expectations: + + 1. ControllerVersion.is_media_controller returns above values + given corresponding responses + + Expected results: + + 1. ControllerVersion_is_media_controller_true == True + 2. ControllerVersion_is_media_controller_false == False + 3. ControllerVersion_is_media_controller_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.is_media_controller == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("ControllerVersion_is_upgrade_inprogress_true", True), + ("ControllerVersion_is_upgrade_inprogress_false", False), + ("ControllerVersion_is_upgrade_inprogress_none", None), + ], +) +def test_is_upgrade_inprogress(monkeypatch, module, key, expected) -> None: + """ + Function description: + + ControllerVersion.is_ha_enabled returns: + - True, if NDFC response "is_upgrade_inprogress" key == "true" + - False, if NDFC response "is_upgrade_inprogress" key == "false" + - None, if NDFC response "is_upgrade_inprogress" key is missing + + Expectations: + + 1. ControllerVersion.is_upgrade_inprogress returns above values + given corresponding responses + + Expected results: + + 1. ControllerVersion_is_upgrade_inprogress_true == True + 2. ControllerVersion_is_upgrade_inprogress_false == False + 3. ControllerVersion_is_upgrade_inprogress_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.is_upgrade_inprogress == expected + + +def test_response_data_present(monkeypatch, module) -> None: + """ + Function description: + + ControllerVersion.response_data returns the "DATA" key in the + NDFC response, which is a dictionary of key-value pairs. + If the "DATA" key is absent, fail_json is called. + + Expectations: + + 1. ControllerVersion.response_data will return a dictionary of key-value + pairs + + Expected results: + + 1. ControllerVersion_DATA_present, ControllerVersion.response_data == type(dict) + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + key = "ControllerVersion_DATA_present" + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert isinstance(module.response_data, dict) + + +def test_response_data_not_present(monkeypatch, module) -> None: + """ + Function description: + + See: test_response_data_present + + Expectations: + + 1. fail_json is called if the "DATA" key is absent + + Expected results: + + 1. fail_json is called + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + key = "ControllerVersion_DATA_not_present" + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + with pytest.raises(AnsibleFailJson): + module.refresh() + + +def test_result_200(monkeypatch, module) -> None: + """ + Function description: + + ControllerVersion.result returns the result of its superclass + method ImageUpgradeCommon._handle_response() + + Expectations: + + 1. For a 200 response with "message" key == "OK", + ControllerVersion.result == {'found': True, 'success': True} + + Expected results: + + 1. ControllerVersion_result_200 == {'found': True, 'success': True} + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + key = "ControllerVersion_get_return_code_200" + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.result == {"found": True, "success": True} + + +def test_result_404(monkeypatch, module) -> None: + """ + Function description: + + See: test_result_200 + + Expectations: + + 1. For a 404 response with "message" key == "Not Found", + ControllerVersion.result == {'found': False, 'success': True} + and ControllerVersion.refresh() calls fail_json + + Expected results: + + 1. fail_json is called + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + key = "ControllerVersion_get_return_code_404" + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + with pytest.raises(AnsibleFailJson): + module.refresh() + + +def test_result_500(monkeypatch, module) -> None: + """ + Function description: + + See: test_result_200 + + Expectations: + + 1. For a 500 response with any "message" key value, + ControllerVersion.result == {'found': False, 'success': False} + and ControllerVersion.refresh() calls fail_json + + Expected results: + + 1. fail_json is called + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + key = "ControllerVersion_get_return_code_500" + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + with pytest.raises(AnsibleFailJson): + module.refresh() + + +@pytest.mark.parametrize( + "key, expected", [("ControllerVersion_mode_LAN", "LAN"), ("ControllerVersion_mode_none", None)] +) +def test_mode(monkeypatch, module, key, expected) -> None: + """ + Function description: + + ControllerVersion.mode returns: + - If NDFC response "mode" key is present, its value + - If NDFC response "mode" key is not present, None + + Expectations: + + 1. ControllerVersion.mode returns above values + given corresponding responses + + Expected results: + + 1. ControllerVersion_mode_LAN == "LAN" + 2. ControllerVersion_mode_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.mode == expected + + +@pytest.mark.parametrize( + "key, expected", + [("ControllerVersion_uuid_UUID", "foo-uuid"), ("ControllerVersion_uuid_none", None)], +) +def test_uuid(monkeypatch, module, key, expected) -> None: + """ + Function description: + + ControllerVersion.uuid returns: + - If NDFC response "uuid" key is present, its value + - If NDFC response "uuid" key is not present, None + + Expectations: + + 1. ControllerVersion.uuid returns above values + given corresponding responses + + Expected results: + + 1. ControllerVersion_uuid_UUID == "foo-uuid" + 2. ControllerVersion_uuid_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.uuid == expected + + +@pytest.mark.parametrize( + "key, expected", + [("ControllerVersion_version_12.1.3b", "12.1.3b"), ("ControllerVersion_version_none", None)], +) +def test_version(monkeypatch, module, key, expected) -> None: + """ + Function description: + + ControllerVersion.version returns: + - If NDFC response "version" key is present, its value + - If NDFC response "version" key is not present, None + + Expectations: + + 1. ControllerVersion.version returns above values + given corresponding responses + + Expected results: + + 1. ControllerVersion_version_12.1.3b == "12.1.3b" + 2. ControllerVersion_version_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.version == expected + + +@pytest.mark.parametrize( + "key, expected", + [("ControllerVersion_version_12.1.3b", "12"), ("ControllerVersion_version_none", None)], +) +def test_version_major(monkeypatch, module, key, expected) -> None: + """ + Function description: + + version_major returns the major version of NDFC + It derives this from the "version" key in the NDFC response + by splitting the string on "." and returning the first element + + ControllerVersion.version_major returns: + - If NDFC response "version" key is present, the major version + - If NDFC response "version" key is not present, None + + Expectations: + + 1. ControllerVersion.version_major returns above values + given corresponding responses + + Expected results: + + 1. ControllerVersion_version_12.1.3b == "12" + 2. ControllerVersion_version_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.version_major == expected + + +@pytest.mark.parametrize( + "key, expected", + [("ControllerVersion_version_12.1.3b", "1"), ("ControllerVersion_version_none", None)], +) +def test_version_minor(monkeypatch, module, key, expected) -> None: + """ + Function description: + + version_minor returns the minor version of NDFC + It derives this from the "version" key in the NDFC response + by splitting the string on "." and returning the second element + + ControllerVersion.version_minor returns: + - If NDFC response "version" key is present, the minor version + - If NDFC response "version" key is not present, None + + Expectations: + + 1. ControllerVersion.version_minor returns above values + given corresponding responses + + Expected results: + + 1. ControllerVersion_version_12.1.3b == "1" + 2. ControllerVersion_version_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.version_minor == expected + + +@pytest.mark.parametrize( + "key, expected", + [("ControllerVersion_version_12.1.3b", "3b"), ("ControllerVersion_version_none", None)], +) +def test_version_patch(monkeypatch, module, key, expected) -> None: + """ + Function description: + + version_patch returns the patch version of NDFC + It derives this from the "version" key in the NDFC response + by splitting the string on "." and returning the third element + + ControllerVersion.version_patch returns: + - If NDFC response "version" key is present, the patch version + - If NDFC response "version" key is not present, None + + Expectations: + + 1. ControllerVersion.version_patch returns above values + given corresponding responses + + Expected results: + + 1. ControllerVersion_version_12.1.3b == "3b" + 2. ControllerVersion_version_none == None + """ + + def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) + + module.refresh() + assert module.version_patch == expected diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py similarity index 75% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index 4c9e60de9..6a7938bb5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -3,22 +3,20 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# NdfcImageInstallOptions -# ) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import NdfcImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import ImageInstallOptions from .fixture import load_fixture """ -ndfc_version: 12 -description: Verify functionality of class NdfcImageInstallOptions +controller_version: 12 +description: Verify functionality of class ImageInstallOptions """ -class_name = "NdfcImageInstallOptions" -response_file = f"dcnm_image_upgrade_responses_{class_name}" -#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options.dcnm_send" +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." + +dcnm_send_install_options = patch_image_mgmt + "install_options.dcnm_send" + class MockAnsibleModule: params = {} @@ -26,7 +24,8 @@ def fail_json(msg) -> AnsibleFailJson: raise AnsibleFailJson(msg) -def response_data(key: str) -> Dict[str, str]: +def responses_image_install_options(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ImageInstallOptions" response = load_fixture(response_file).get(key) print(f"{key} : : {response}") return response @@ -34,7 +33,7 @@ def response_data(key: str) -> Dict[str, str]: @pytest.fixture def module(): - return NdfcImageInstallOptions(MockAnsibleModule) + return ImageInstallOptions(MockAnsibleModule) def test_policy_name_not_defined(module) -> None: @@ -42,7 +41,7 @@ def test_policy_name_not_defined(module) -> None: fail_json() is called if policy_name is not set when refresh() is called. """ module.serial_number = "FOO" - error_message = "NdfcImageInstallOptions.refresh: " + error_message = "ImageInstallOptions.refresh: " error_message += "instance.policy_name must be set before " error_message += r"calling refresh\(\)" with pytest.raises(AnsibleFailJson, match=error_message): @@ -54,7 +53,7 @@ def test_serial_number_not_defined(module) -> None: fail_json() is called if serial_number is not set when refresh() is called. """ module.policy_name = "FOO" - error_message = "NdfcImageInstallOptions.refresh: " + error_message = "ImageInstallOptions.refresh: " error_message += "instance.serial_number must be set before " error_message += r"calling refresh\(\)" with pytest.raises(AnsibleFailJson, match=error_message): @@ -68,15 +67,15 @@ def test_refresh_return_code_200(monkeypatch, module) -> None: """ key = "imageupgrade_install_options_post_return_code_200" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_image_install_options(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) module.policy_name = "KRM5" module.serial_number = "BAR" module.refresh() - assert isinstance(module.ndfc_response, dict) + assert isinstance(module.response, dict) assert module.device_name == "cvd-1314-leaf" assert module.err_message == "" assert module.epld_modules is None @@ -88,7 +87,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: assert module.version == "10.2.5" comp_disp = "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin" assert module.comp_disp == comp_disp - assert module.ndfc_result.get("success") == True + assert module.result.get("success") == True def test_refresh_return_code_500(monkeypatch, module) -> None: @@ -97,14 +96,14 @@ def test_refresh_return_code_500(monkeypatch, module) -> None: """ key = "imageupgrade_install_options_post_return_code_500" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_image_install_options(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) module.policy_name = "KRM5" module.serial_number = "BAR" - error_message = "NdfcImageInstallOptions.refresh: " + error_message = "ImageInstallOptions.refresh: " error_message += "Bad result when retrieving install-options from NDFC" with pytest.raises(AnsibleFailJson, match=rf"{error_message}"): module.refresh() @@ -147,7 +146,7 @@ def test_invalid_value_issu(module) -> None: """ fail_json() is called if issu is not a boolean. """ - error_message = "NdfcImageInstallOptions.issu.setter: issu must be a " + error_message = "ImageInstallOptions.issu.setter: issu must be a " error_message += "boolean value" with pytest.raises(AnsibleFailJson, match=error_message): module.issu = "FOO" @@ -157,7 +156,7 @@ def test_invalid_value_epld(module) -> None: """ fail_json() is called if epld is not a boolean. """ - error_message = "NdfcImageInstallOptions.epld.setter: epld must be a " + error_message = "ImageInstallOptions.epld.setter: epld must be a " error_message += "boolean value" with pytest.raises(AnsibleFailJson, match=error_message): module.epld = "FOO" @@ -167,7 +166,7 @@ def test_invalid_value_package_install(module) -> None: """ fail_json() is called if package_install is not a boolean. """ - error_message = "NdfcImageInstallOptions.package_install.setter: " + error_message = "ImageInstallOptions.package_install.setter: " error_message += "package_install must be a boolean value" with pytest.raises(AnsibleFailJson, match=error_message): module.package_install = "FOO" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py new file mode 100644 index 000000000..53cff00a1 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py @@ -0,0 +1,219 @@ +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# ImagePolicies +# ) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import ImagePolicies + +from .fixture import load_fixture + +""" +controller_version: 12 +description: Verify functionality of class ImagePolicies +""" + +#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies.dcnm_send" + +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." + +dcnm_send_image_policies = patch_image_mgmt + "image_policies.dcnm_send" + +class MockAnsibleModule: + params = {} + + def fail_json(msg) -> AnsibleFailJson: + raise AnsibleFailJson(msg) + + +def responses_image_policies(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ImagePolicies" + response = load_fixture(response_file).get(key) + print(f"responses_image_policies: {key} : {response}") + return response + + +@pytest.fixture +def module(): + return ImagePolicies(MockAnsibleModule) + + +def test_init_properties(module) -> None: + """ + Properties are initialized to None + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("policy_name") == None + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None + + +def test_refresh_return_code_200(monkeypatch, module) -> None: + """ + Properties are initialized based on 200 response from endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policies + """ + key = "policymgnt_policies_get_return_code_200" + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + module.refresh() + module.policy_name = "KR5M" + assert isinstance(module.response, dict) + assert module.agnostic == False + assert module.description == "10.2.(5) with EPLD" + assert module.epld_image_name == "n9000-epld.10.2.5.M.img" + assert module.image_name == "nxos64-cs.10.2.5.M.bin" + assert module.nxos_version == "10.2.5_nxos64-cs_64bit" + assert module.package_name == None + assert module.platform == "N9K/N3K" + assert module.platform_policies == None + assert module.policy_name == "KR5M" + assert module.policy_type == "PLATFORM" + assert module.ref_count == 10 + assert module.rpm_images == None + + +def test_result_return_code_200(monkeypatch, module) -> None: + """ + result contains expected key/values on 200 response from endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policies + """ + key = "policymgnt_policies_get_return_code_200" + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + module.refresh() + assert isinstance(module.result, dict) + assert module.result.get("found") == True + assert module.result.get("success") == True + + +def test_result_return_code_404(monkeypatch, module) -> None: + """ + fail_json is called on 404 response from malformed endpoint. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "policymgnt_policies_get_return_code_404" + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + error_message = "ImagePolicies.refresh: Bad response when retrieving " + error_message += "image policy information from the controller." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_result_return_code_200_empty_data(monkeypatch, module) -> None: + """ + fail_json is called on 200 response with empty DATA key. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "policymgnt_policies_get_return_code_200_empty_DATA" + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + error_message = "ImagePolicies.refresh: Bad response when retrieving " + error_message += "image policy information from the controller." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_result_return_code_200_controller_has_no_defined_image_policies( + monkeypatch, module +) -> None: + """ + fail_json is called on 200 response with DATA.lastOperDataObject length 0. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + """ + key = "policymgnt_policies_get_return_code_200" + key += "_controller_has_no_defined_image_policies" + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + error_message = "ImagePolicies.refresh: " + error_message += "the controller has no defined image policies." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() + + +def test_policy_name_not_found(monkeypatch, module) -> None: + """ + fail_json() is called if response does not contain policy_name. + i.e. image policy with name FOO has not yet been created on NDFC. + """ + key = "policymgnt_policies_get_return_code_200" + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + module.refresh() + module.policy_name = "FOO" + error_message = "ImagePolicies._get: " + error_message += "policy_name FOO is not defined on the controller." + with pytest.raises(AnsibleFailJson, match=error_message): + module.policy_type == "PLATFORM" + + +def test_get_with_policy_name_None(module) -> None: + """ + fail_json is called when _get() is called prior to setting policy_name. + """ + error_message = "ImagePolicies._get: instance.policy_name must be " + error_message += "set before accessing property imageName." + with pytest.raises(AnsibleFailJson, match=error_message): + module._get("imageName") + + +def test_result_return_code_200_policy_name_missing_in_response( + monkeypatch, module +) -> None: + """ + fail_json is called on 200 response with missing policyName key. + endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + + NOTE: This is to cover a check in ImagePolicies.refresh() for a scenario that should never happen. + TODO: Consider removing this check, and this testcase. + """ + key = "policymgnt_policies_get_return_code_200" + key += "_policyName_missing_in_response" + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + error_message = "ImagePolicies.refresh: " + error_message += "Cannot parse policy information from the controller." + with pytest.raises(AnsibleFailJson, match=error_message): + module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py similarity index 74% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index 12d1fd108..ae03b994a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -1,6 +1,6 @@ """ -ndfc_version: 12 -description: Verify functionality of NdfcImageStage +controller_version: 12 +description: Verify functionality of ImageStage """ from contextlib import contextmanager @@ -9,13 +9,9 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# NdfcImagePolicies, NdfcImagePolicyAction, -# NdfcSwitchIssuDetailsBySerialNumber) -# from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import NdfcImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import NdfcImagePolicyAction -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import ImagePolicyAction +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber from .fixture import load_fixture @@ -25,15 +21,15 @@ def does_not_raise(): yield -# dcnm_send_patch = ( -# "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -# ) -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details.dcnm_send" +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." -def response_data_issu_details(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" +dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" + +def responses_issu_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) - print(f"response_data_issu_details: {key} : {response}") + print(f"responses_issu_details: {key} : {response}") return response @@ -46,12 +42,12 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture def module(): - return NdfcImagePolicyAction(MockAnsibleModule) + return ImagePolicyAction(MockAnsibleModule) @pytest.fixture -def mock_issu_details() -> NdfcSwitchIssuDetailsBySerialNumber: - return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) +def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: + return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) # test_init @@ -59,8 +55,8 @@ def mock_issu_details() -> NdfcSwitchIssuDetailsBySerialNumber: def test_init(module) -> None: module.__init__(MockAnsibleModule) - assert isinstance(module, NdfcImagePolicyAction) - assert isinstance(module.switch_issu_details, NdfcSwitchIssuDetailsBySerialNumber) + assert isinstance(module, ImagePolicyAction) + assert isinstance(module.switch_issu_details, SwitchIssuDetailsBySerialNumber) assert module.valid_actions == {"attach", "detach", "query"} @@ -74,8 +70,8 @@ def test_init_properties(module) -> None: module._init_properties() assert isinstance(module.properties, dict) assert module.properties.get("action") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None assert module.properties.get("policy_name") == None assert module.properties.get("query_result") == None assert module.properties.get("serial_numbers") == None @@ -98,11 +94,11 @@ def test_build_attach_payload(monkeypatch, module, mock_issu_details) -> None: 3. module.payloads should have length 5 """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImagePolicyAction_test_build_attach_payload" - return response_data_issu_details(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_build_attach_payload" + return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.switch_issu_details = mock_issu_details module.policy_name = "KR5M" @@ -132,11 +128,11 @@ def test_build_attach_payload_fail_json(monkeypatch, module, mock_issu_details) 1. module.fail_json should be called because deviceName is None in the issu_details response """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImagePolicyAction_test_build_attach_payload_fail_json" - return response_data_issu_details(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_build_attach_payload_fail_json" + return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.switch_issu_details = mock_issu_details module.policy_name = "KR5M" @@ -168,7 +164,7 @@ def test_validate_request_action_none(module, mock_issu_details) -> None: module.serial_numbers = [ "FDO2112189M", ] - match = "NdfcImagePolicyAction.validate_request: " + match = "ImagePolicyAction.validate_request: " match += "instance.action must be set before calling commit()" with pytest.raises(AnsibleFailJson, match=match): module.validate_request() @@ -176,7 +172,7 @@ def test_validate_request_action_none(module, mock_issu_details) -> None: # test_validate_request_policy_name_none -match = "NdfcImagePolicyAction.validate_request: " +match = "ImagePolicyAction.validate_request: " match += "instance.policy_name must be set before calling commit()" @@ -212,7 +208,7 @@ def test_validate_request_policy_name_none( # test_validate_request_serial_numbers_none -match = "NdfcImagePolicyAction.validate_request: " +match = "ImagePolicyAction.validate_request: " match += "instance.serial_numbers must be set before calling commit()" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py similarity index 68% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py index f68566659..acf3f78b3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py @@ -1,7 +1,7 @@ """ -ndfc_version: 12 -description: Verify functionality of NdfcImageStage -TODO:2 NdfcImageStage.commit unit test +controller_version: 12 +description: Verify functionality of ImageStage +TODO:2 ImageStage.commit unit test """ from contextlib import contextmanager @@ -10,13 +10,9 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# NdfcEndpoints, NdfcImageStage, NdfcSwitchIssuDetailsBySerialNumber, -# NdfcVersion) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import NdfcImageStage -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import NdfcSwitchIssuDetailsBySerialNumber -#from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import NdfcVersion +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import ImageStage +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber from .fixture import load_fixture @@ -25,26 +21,25 @@ def does_not_raise(): yield +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_common = patch_module_utils + "common." -# dcnm_send_patch = ( -# "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -# ) +dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" +dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" +dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" -dcnm_send_issu_details = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details.dcnm_send" -dcnm_send_ndfc_version = "ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version.dcnm_send" -dcnm_send_image_stage = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage.dcnm_send" - -def response_data_issu_details(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" +def responses_issu_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) - print(f"response_data_issu_details: {key} : {response}") + print(f"responses_issu_details: {key} : {response}") return response -def response_data_ndfc_version(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcVersion" +def responses_controller_version(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ControllerVersion" response = load_fixture(response_file).get(key) - print(f"response_data_ndfc_version: {key} : {response}") + print(f"responses_controller_version: {key} : {response}") return response @@ -57,12 +52,12 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture def module(): - return NdfcImageStage(MockAnsibleModule) + return ImageStage(MockAnsibleModule) @pytest.fixture -def mock_issu_details() -> NdfcSwitchIssuDetailsBySerialNumber: - return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) +def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: + return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) # test_init @@ -74,15 +69,15 @@ class attributes are initialized to expected values """ module.__init__(MockAnsibleModule) assert module.module == MockAnsibleModule - assert module.class_name == "NdfcImageStage" + assert module.class_name == "ImageStage" assert isinstance(module.properties, dict) assert isinstance(module.serial_numbers_done, set) - assert module.ndfc_version == None + assert module.controller_version == None assert module.path == None assert module.verb == None assert module.payload == None - assert isinstance(module.issu_detail, NdfcSwitchIssuDetailsBySerialNumber) - assert isinstance(module.endpoints, NdfcEndpoints) + assert isinstance(module.issu_detail, SwitchIssuDetailsBySerialNumber) + assert isinstance(module.endpoints, ApiEndpoints) # test_init_properties @@ -94,46 +89,46 @@ def test_init_properties(module) -> None: """ module._init_properties() assert isinstance(module.properties, dict) - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None assert module.properties.get("serial_numbers") == None assert module.properties.get("check_interval") == 10 assert module.properties.get("check_timeout") == 1800 -# test_populate_ndfc_version +# test_populate_controller_version @pytest.mark.parametrize( "key, expected", [ - ("NdfcImageStage_12_1_2e", "12.1.2e"), - ("NdfcImageStage_12_1_3b", "12.1.3b"), + ("ImageStage_12_1_2e", "12.1.2e"), + ("ImageStage_12_1_3b", "12.1.3b"), ], ) -def test_populate_ndfc_version(monkeypatch, module, key, expected) -> None: +def test_populate_controller_version(monkeypatch, module, key, expected) -> None: """ - _populate_ndfc_version retrieves the controller version from NDFC. + _populate_controller_version retrieves the controller version from NDFC. This is used in commit() to populate the payload with either a misspelled "sereialNum" key/value (12.1.2e) or a correctly-spelled "serialNumbers" key/value (12.1.3b). Expectations: - 1. module.ndfc_version should be set + 1. module.controller_version should be set Expected results: - 1. NdfcImageStage_12_1_2e -> module.ndfc_version == "12.1.2e" - 2. NdfcImageStage_12_1_3b -> module.ndfc_version == "12.1.3b" + 1. ImageStage_12_1_2e -> module.controller_version == "12.1.2e" + 2. ImageStage_12_1_3b -> module.controller_version == "12.1.3b" """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return response_data_ndfc_version(key) + def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) - monkeypatch.setattr(dcnm_send_ndfc_version, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) - module._populate_ndfc_version() - assert module.ndfc_version == expected + module._populate_controller_version() + assert module.controller_version == expected # test_prune_serial_numbers @@ -155,11 +150,11 @@ def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: 2. module.serial_numbers != ["FDO211218FV", "FDO211218GC"] """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_prune_serial_numbers" - return response_data_issu_details(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageStage_test_prune_serial_numbers" + return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -192,11 +187,11 @@ def test_validate_serial_numbers_failed(monkeypatch, module, mock_issu_details) FDO2112189M should fail since imageStaged == "Failed" """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_validate_serial_numbers" - return response_data_issu_details(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageStage_test_validate_serial_numbers" + return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] @@ -209,7 +204,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: # test_commit_serial_numbers -match = r"NdfcImageStage.commit\(\) call instance.serial_numbers " +match = r"ImageStage.commit\(\) call instance.serial_numbers " match += r"before calling commit\(\)." @@ -224,7 +219,7 @@ def test_commit_serial_numbers( monkeypatch, module, serial_numbers_is_set, expected ) -> None: """ - fail_json is called when NdfcImageStage.commit() is called without + fail_json is called when ImageStage.commit() is called without setting instance.serial_numbers. Expectations: @@ -233,21 +228,21 @@ def test_commit_serial_numbers( 2. fail_json is not called when serial_numbers is set """ - def mock_dcnm_send_ndfc_version(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcVersion_get_return_code_200" - return response_data_ndfc_version(key) + def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: + key = "ControllerVersion_get_return_code_200" + return responses_controller_version(key) def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_validate_serial_numbers" - return response_data_issu_details(key) + key = "ImageStage_test_validate_serial_numbers" + return responses_issu_details(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_validate_serial_numbers" - return response_data_issu_details(key) + key = "ImageStage_test_validate_serial_numbers" + return responses_issu_details(key) monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_ndfc_version, mock_dcnm_send_ndfc_version) + monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) if serial_numbers_is_set: module.serial_numbers = ["FDO21120U5D"] @@ -260,10 +255,10 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_commit_path_verb(monkeypatch, module) -> None: """ - NdfcImageStage.path should be set to: + ImageStage.path should be set to: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image - NdfcImageStage.verb should be set to: + ImageStage.verb should be set to: POST Expectations: @@ -271,21 +266,21 @@ def test_commit_path_verb(monkeypatch, module) -> None: 1. both self.path and self.verb should be set, per above """ - def mock_dcnm_send_ndfc_version(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcVersion_get_return_code_200" - return response_data_ndfc_version(key) + def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: + key = "ControllerVersion_get_return_code_200" + return responses_controller_version(key) def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_validate_serial_numbers" - return response_data_issu_details(key) + key = "ImageStage_test_validate_serial_numbers" + return responses_issu_details(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_validate_serial_numbers" - return response_data_issu_details(key) + key = "ImageStage_test_validate_serial_numbers" + return responses_issu_details(key) monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_ndfc_version, mock_dcnm_send_ndfc_version) + monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) module.serial_numbers = ["FDO21120U5D"] module.commit() @@ -300,14 +295,14 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( - "ndfc_version, expected_serial_number_key", + "controller_version, expected_serial_number_key", [ ("12.1.2e", "sereialNum"), ("12.1.3b", "serialNumbers"), ], ) def test_commit_payload_serial_number_key_name( - monkeypatch, module, ndfc_version, expected_serial_number_key + monkeypatch, module, controller_version, expected_serial_number_key ) -> None: """ commit() will set the payload key name for the serial number @@ -317,24 +312,25 @@ def test_commit_payload_serial_number_key_name( 1. The correct serial number key name should be used based on NDFC version Expected results: - ndfc_version 12.1.2e -> key name "sereialNum" (yes, misspelled) - ndfc_version 12.1.3b -> key name "serialNumbers + controller_version 12.1.2e -> key name "sereialNum" (yes, misspelled) + controller_version 12.1.3b -> key name "serialNumbers """ - def mock_ndfc_version(*args, **kwargs) -> None: - module.ndfc_version = ndfc_version + def mock_controller_version(*args, **kwargs) -> None: + module.controller_version = controller_version - ndfc_version_patch = "ansible_collections.cisco.dcnm.plugins.modules." - ndfc_version_patch += "dcnm_image_upgrade.NdfcImageStage._populate_ndfc_version" - monkeypatch.setattr(ndfc_version_patch, mock_ndfc_version) + controller_version_patch = "ansible_collections.cisco.dcnm.plugins." + controller_version_patch += "modules.dcnm_image_upgrade." + controller_version_patch += "ImageStage._populate_controller_version" + monkeypatch.setattr(controller_version_patch, mock_controller_version) def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_validate_serial_numbers" - return response_data_issu_details(key) + key = "ImageStage_test_validate_serial_numbers" + return responses_issu_details(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_commit_payload_serial_number_key_name" - return response_data_issu_details(key) + key = "ImageStage_test_commit_payload_serial_number_key_name" + return responses_issu_details(key) monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -363,8 +359,8 @@ def test_wait_for_image_stage_to_complete( 4. The module should return without calling fail_json. """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_wait_for_image_stage_to_complete" - return response_data_issu_details(key) + key = "ImageStage_test_wait_for_image_stage_to_complete" + return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -400,8 +396,8 @@ def test_wait_for_image_stage_to_complete_stage_failed( 4. Call fail_json on serial number FDO2112189M, imageStaged is "Failed" """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_wait_for_image_stage_to_complete_fail_json" - return response_data_issu_details(key) + key = "ImageStage_test_wait_for_image_stage_to_complete_fail_json" + return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -440,8 +436,8 @@ def test_wait_for_image_stage_to_complete_timout( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_wait_for_image_stage_to_complete_timeout" - return response_data_issu_details(key) + key = "ImageStage_test_wait_for_image_stage_to_complete_timeout" + return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -453,7 +449,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.check_interval = 1 module.check_timeout = 1 - error_message = "NdfcImageStage._wait_for_image_stage_to_complete: " + error_message = "ImageStage._wait_for_image_stage_to_complete: " error_message += "Timed out waiting for image stage to complete. " error_message += "serial_numbers_done: FDO21120U5D, " error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" @@ -474,7 +470,7 @@ def test_wait_for_current_actions_to_complete( """ _wait_for_current_actions_to_complete waits until staging, validation, and upgrade actions are complete for all serial numbers. It calls - NdfcSwitchIssuDetailsBySerialNumber.actions_in_progress() and expects + SwitchIssuDetailsBySerialNumber.actions_in_progress() and expects this to return False. actions_in_progress() returns True until none of the following keys has a value of "In-Progress": @@ -488,11 +484,11 @@ def test_wait_for_current_actions_to_complete( 4. The function should return without calling fail_json. """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_wait_for_current_actions_to_complete" - return response_data_issu_details(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageStage_test_wait_for_current_actions_to_complete" + return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -524,11 +520,11 @@ def test_wait_for_current_actions_to_complete_timout( 4. The function should call fail_json due to timeout """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageStage_test_wait_for_current_actions_to_complete_timeout" - return response_data_issu_details(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageStage_test_wait_for_current_actions_to_complete_timeout" + return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -538,7 +534,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: module.check_interval = 1 module.check_timeout = 1 - error_message = "NdfcImageStage._wait_for_current_actions_to_complete: " + error_message = "ImageStage._wait_for_current_actions_to_complete: " error_message += "Timed out waiting for actions to complete. " error_message += "serial_numbers_done: FDO21120U5D, " error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py similarity index 68% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageUpgrade.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 623364938..995d47cf6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -1,5 +1,5 @@ """ -ndfc_version: 12 +controller_version: 12 description: Verify functionality of NdfcSwitchUpgrade """ from contextlib import contextmanager @@ -8,8 +8,10 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( - NdfcImageUpgrade, NdfcSwitchDetails, NdfcVersion) +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# ImageUpgrade, SwitchDetails, ControllerVersion) + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import ImageUpgrade from .fixture import load_fixture @@ -19,15 +21,22 @@ def does_not_raise(): yield -dcnm_send_patch = ( - "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -) +# dcnm_send_patch = ( +# "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +# ) +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_common = patch_module_utils + "common." + +dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" +dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" +dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" -def responses_ndfc_switch_details(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcSwitchDetails" +def responses_switch_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchDetails" response = load_fixture(response_file).get(key) - print(f"responses_ndfc_switch_details: {key} : {response}") + print(f"responses_switch_details: {key} : {response}") return response @@ -40,13 +49,13 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture def module(): - return NdfcImageUpgrade(MockAnsibleModule) + return ImageUpgrade(MockAnsibleModule) def test_init(module) -> None: module.__init__(MockAnsibleModule) - assert isinstance(module, NdfcImageUpgrade) - assert module.class_name == "NdfcImageUpgrade" + assert isinstance(module, ImageUpgrade) + assert module.class_name == "ImageUpgrade" assert module.max_module_number == 9 def test_init_defaults(module) -> None: @@ -85,9 +94,9 @@ def test_init_properties(module) -> None: assert module.properties.get("epld_module") == "ALL" assert module.properties.get("epld_upgrade") == False assert module.properties.get("force_non_disruptive") == False - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None assert module.properties.get("non_disruptive") == False assert module.properties.get("force_non_disruptive") == False assert module.properties.get("package_install") == False @@ -102,7 +111,7 @@ def test_init_properties(module) -> None: # """ # Function description: -# NdfcSwitchDetails.ip_address returns: +# SwitchDetails.ip_address returns: # - IP Address, if the user has set ip_address # - None, if the user has not already set ip_address @@ -117,7 +126,7 @@ def test_init_properties(module) -> None: # """ # Function description: -# NdfcSwitchDetails.ip_address returns: +# SwitchDetails.ip_address returns: # - IP Address, if the user has set ip_address # - None, if the user has not already set ip_address @@ -133,31 +142,31 @@ def test_init_properties(module) -> None: # """ # Function description: -# NdfcSwitchDetails.refresh sets the following properties: -# - ndfc_data -# - ndfc_response -# - ndfc_result +# SwitchDetails.refresh sets the following properties: +# - response_data +# - response +# - result # Expected results: -# 1. instance.ndfc_data is a dictionary -# 2. instance.ndfc_response is a dictionary -# 3. instance.ndfc_data is a list +# 1. instance.response_data is a dictionary +# 2. instance.response is a dictionary +# 3. instance.response_data is a list # """ # def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: -# key = "NdfcSwitchDetails_get_return_code_200" -# return responses_ndfc_switch_details(key) +# key = "SwitchDetails_get_return_code_200" +# return responses_switch_details(key) # monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) # module.refresh() -# assert isinstance(module.ndfc_data, dict) -# assert isinstance(module.ndfc_result, dict) -# assert isinstance(module.ndfc_response, dict) +# assert isinstance(module.response_data, dict) +# assert isinstance(module.result, dict) +# assert isinstance(module.response, dict) -# def test_refresh_ndfc_data(monkeypatch, module) -> None: +# def test_refresh_response_data(monkeypatch, module) -> None: # """ # Function description: @@ -165,18 +174,18 @@ def test_init_properties(module) -> None: # Expected results: -# 1. instance.ndfc_data is a dictionary +# 1. instance.response_data is a dictionary # 2. When instance.ip_address is set, getter properties will return values specific to ip_address # """ # def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: -# key = "NdfcSwitchDetails_get_return_code_200" -# return responses_ndfc_switch_details(key) +# key = "SwitchDetails_get_return_code_200" +# return responses_switch_details(key) # monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) # module.refresh() -# assert isinstance(module.ndfc_data, dict) +# assert isinstance(module.response_data, dict) # module.ip_address = "172.22.150.110" # assert module.hostname == "cvd-1111-bgw" # module.ip_address = "172.22.150.111" @@ -197,44 +206,44 @@ def test_init_properties(module) -> None: # @pytest.mark.parametrize( # "key,expected", # [ -# ("NdfcSwitchDetails_get_return_code_200", does_not_raise()), +# ("SwitchDetails_get_return_code_200", does_not_raise()), # ( -# "NdfcSwitchDetails_get_return_code_404", +# "SwitchDetails_get_return_code_404", # pytest.raises(AnsibleFailJson, match=match), # ), # ( -# "NdfcSwitchDetails_get_return_code_500", +# "SwitchDetails_get_return_code_500", # pytest.raises(AnsibleFailJson, match=match), # ), # ], # ) -# def test_ndfc_result(monkeypatch, module, key, expected) -> None: +# def test_result(monkeypatch, module, key, expected) -> None: # """ # Function description: -# NdfcSwitchDetails.ndfc_result returns the result of its superclass -# method NdfcAnsibleImageUpgradeCommon._handle_response() +# SwitchDetails.result returns the result of its superclass +# method ImageUpgradeCommon._handle_response() # Expectations: # 1. 200 RETURN_CODE, MESSAGE == "OK", -# NdfcSwitchDetails.ndfc_result == {'found': True, 'success': True} +# SwitchDetails.result == {'found': True, 'success': True} # 2. 404 RETURN_CODE, MESSAGE == "Not Found", -# NdfcSwitchDetails.ndfc_result == {'found': False, 'success': True} +# SwitchDetails.result == {'found': False, 'success': True} # 3. 500 RETURN_CODE, MESSAGE ~= "Internal Server Error", -# NdfcSwitchDetails.ndfc_result == {'found': False, 'success': False} +# SwitchDetails.result == {'found': False, 'success': False} # Expected results: -# 1. NdfcSwitchDetails_result_200 == {'found': True, 'success': True} -# 2. NdfcSwitchDetails_result_404 == {'found': False, 'success': True} -# 3. NdfcSwitchDetails_result_500 == {'found': False, 'success': False} +# 1. SwitchDetails_result_200 == {'found': True, 'success': True} +# 2. SwitchDetails_result_404 == {'found': False, 'success': True} +# 3. SwitchDetails_result_500 == {'found': False, 'success': False} # """ # def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: -# return responses_ndfc_switch_details(key) +# return responses_switch_details(key) # monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) @@ -261,7 +270,7 @@ def test_init_properties(module) -> None: # """ # Function description: -# NdfcSwitchDetails._get is called by all getter properties. +# SwitchDetails._get is called by all getter properties. # It raises AnsibleFailJson if the user has not set ip_address. # It returns the value of the requested property if the user has set ip_address. # The property value is passed to both make_boolean() and make_none(), which @@ -272,18 +281,18 @@ def test_init_properties(module) -> None: # Expectations: -# 1. NdfcVersion._get returns above values +# 1. ControllerVersion._get returns above values # given corresponding responses # Expected results: -# 1. NdfcVersion_mode_LAN == "LAN" -# 2. NdfcVersion_mode_none == None +# 1. ControllerVersion_mode_LAN == "LAN" +# 2. ControllerVersion_mode_none == None # """ # def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: -# key = "NdfcSwitchDetails_get_return_code_200" -# return responses_ndfc_switch_details(key) +# key = "SwitchDetails_get_return_code_200" +# return responses_switch_details(key) # monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py similarity index 90% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py index 9e0b6f273..3a6d43dca 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcAnsibleImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py @@ -4,15 +4,15 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson # from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# NdfcAnsibleImageUpgradeCommon, NdfcEndpoints) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.ndfc_common import NdfcCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.endpoints import NdfcEndpoints +# ImageUpgradeCommon, ApiEndpoints) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints from .fixture import load_fixture """ -ndfc_version: 12 -description: Verify functionality of class NdfcAnsibleImageUpgradeCommon +controller_version: 12 +description: Verify functionality of class ImageUpgradeCommon """ @@ -25,11 +25,11 @@ def fail_json(msg) -> dict: @pytest.fixture def module(): - # return NdfcAnsibleImageUpgradeCommon(MockAnsibleModule) - return NdfcCommon(MockAnsibleModule) + # return ImageUpgradeCommon(MockAnsibleModule) + return ImageUpgradeCommon(MockAnsibleModule) -def responses_ndfc_ansible_image_upgrade_common(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcAnsibleImageUpgradeCommon" +def responses_image_upgrade_common(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ImageUpgradeCommon" response = load_fixture(response_file).get(key) verb = response.get("METHOD") print(f"{key} : {verb} : {response}") @@ -46,7 +46,7 @@ def test_init_(module) -> None: assert module.fd == None # assert module.logfile == "/tmp/dcnm_image_upgrade.log" assert module.logfile == "/tmp/ndfc.log" - # assert isinstance(module.endpoints, NdfcEndpoints) + # assert isinstance(module.endpoints, ApiEndpoints) @pytest.mark.parametrize( @@ -68,7 +68,7 @@ def test_handle_response_post(module, key, expected) -> None: verify _handle_reponse() return values for 200/OK response to POST request """ - data = responses_ndfc_ansible_image_upgrade_common(key) + data = responses_image_upgrade_common(key) result = module._handle_response(data.get("response"), data.get("verb")) assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") @@ -90,7 +90,7 @@ def test_handle_response_get(module, key, expected) -> None: """ verify _handle_reponse() return values for GET requests """ - data = responses_ndfc_ansible_image_upgrade_common(key) + data = responses_image_upgrade_common(key) result = module._handle_response(data.get("response"), data.get("verb")) # TODO: We could assert on the dictionary, with a less granular error message # assert result == expected @@ -102,7 +102,7 @@ def test_handle_response_unknown_response_verb(module) -> None: """ verify that fail_json() is called if a unknown request verb is provided """ - data = responses_ndfc_ansible_image_upgrade_common("mock_unknown_response_verb") + data = responses_image_upgrade_common("mock_unknown_response_verb") with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): module._handle_response(data.get("response"), data.get("verb")) @@ -125,7 +125,7 @@ def test_handle_get_response(module, key, expected) -> None: NOTE: Adding this test increases coverage by 2% according to pytest-cov """ - data = responses_ndfc_ansible_image_upgrade_common(key) + data = responses_image_upgrade_common(key) result = module._handle_get_response(data.get("response")) assert result.get("success") == expected.get("success") @@ -152,7 +152,7 @@ def test_handle_post_put_delete_response(module, key, expected) -> None: NOTE: This method is covered in test_handle_response_post() above, but... NOTE: Adding this test increases coverage by 2% according to pytest-cov """ - data = responses_ndfc_ansible_image_upgrade_common(key) + data = responses_image_upgrade_common(key) result = module._handle_post_put_delete_response(data.get("response")) assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py similarity index 75% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index a1e5f3d0f..f85feb950 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -1,6 +1,6 @@ """ -ndfc_version: 12 -description: Verify functionality of NdfcImageValidate +controller_version: 12 +description: Verify functionality of ImageValidate """ from typing import Any, Dict @@ -8,14 +8,25 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( - NdfcImageValidate, NdfcSwitchIssuDetailsBySerialNumber, NdfcVersion) +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# ImageValidate, SwitchIssuDetailsBySerialNumber, ControllerVersion) + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import ImageValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber + from .fixture import load_fixture -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_common = patch_module_utils + "common." + +dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" +dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" +dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" + def response_data_issu_details(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcSwitchIssuDetails" + response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) print(f"response_data_issu_details: {key} : {response}") return response @@ -30,12 +41,12 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture def module(): - return NdfcImageValidate(MockAnsibleModule) + return ImageValidate(MockAnsibleModule) @pytest.fixture -def mock_issu_details() -> NdfcSwitchIssuDetailsBySerialNumber: - return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) +def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: + return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) def test_init_properties(module) -> None: @@ -46,9 +57,9 @@ def test_init_properties(module) -> None: assert isinstance(module.properties, dict) assert module.properties.get("check_interval") == 10 assert module.properties.get("check_timeout") == 1800 - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None assert module.properties.get("non_disruptive") == False assert module.properties.get("serial_numbers") == None @@ -69,11 +80,11 @@ def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: 2. module.serial_numbers != ["FDO211218FV", "FDO211218GC"] """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageValidate_test_prune_serial_numbers" + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageValidate_test_prune_serial_numbers" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -103,16 +114,16 @@ def test_validate_serial_numbers_failed(monkeypatch, module, mock_issu_details) FDO2112189M should fail since validated == "Failed" """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageValidate_test_validate_serial_numbers" + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageValidate_test_validate_serial_numbers" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] - error_message = "NdfcImageValidate.validate_serial_numbers: " + error_message = "ImageValidate.validate_serial_numbers: " error_message += "image validation is failing for the following switch: " error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. If this " error_message += "persists, check the switch connectivity to NDFC and " @@ -138,11 +149,11 @@ def test_wait_for_image_validate_to_complete( 4. The module should return without calling fail_json. """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageValidate_test_wait_for_image_validate_to_complete" + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageValidate_test_wait_for_image_validate_to_complete" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -173,11 +184,11 @@ def test_wait_for_image_validate_to_complete_validate_failed( 4. Call fail_json on serial number FDO2112189M, "validated" is "Failed" """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageValidate_test_wait_for_image_validate_to_complete_fail_json" + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageValidate_test_wait_for_image_validate_to_complete_fail_json" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -213,11 +224,11 @@ def test_wait_for_image_validate_to_complete_timeout( 4. The function should call fail_json due to timeout """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageValidate_test_wait_for_image_validate_to_complete_timeout" + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageValidate_test_wait_for_image_validate_to_complete_timeout" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -227,7 +238,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: module.check_interval = 1 module.check_timeout = 1 - error_message = "NdfcImageValidate._wait_for_image_validate_to_complete: " + error_message = "ImageValidate._wait_for_image_validate_to_complete: " error_message += "Timed out waiting for image validation to complete. " error_message += "serial_numbers_done: FDO21120U5D, " error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" @@ -245,7 +256,7 @@ def test_wait_for_current_actions_to_complete( """ _wait_for_current_actions_to_complete waits until staging, validation, and upgrade actions are complete for all serial numbers. It calls - NdfcSwitchIssuDetailsBySerialNumber.actions_in_progress() and expects + SwitchIssuDetailsBySerialNumber.actions_in_progress() and expects this to return False. actions_in_progress() returns True until none of the following keys has a value of "In-Progress": @@ -259,11 +270,11 @@ def test_wait_for_current_actions_to_complete( 4. The function should return without calling fail_json. """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageValidate_test_wait_for_current_actions_to_complete" + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageValidate_test_wait_for_current_actions_to_complete" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -292,11 +303,11 @@ def test_wait_for_current_actions_to_complete_timeout( 4. The function should call fail_json due to timeout """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcImageValidate_test_wait_for_current_actions_to_complete_timeout" + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageValidate_test_wait_for_current_actions_to_complete_timeout" return response_data_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.issu_detail = mock_issu_details module.serial_numbers = [ @@ -306,7 +317,7 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: module.check_interval = 1 module.check_timeout = 1 - error_message = "NdfcImageValidate._wait_for_current_actions_to_complete: " + error_message = "ImageValidate._wait_for_current_actions_to_complete: " error_message += "Timed out waiting for actions to complete. " error_message += "serial_numbers_done: FDO21120U5D, " error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py similarity index 50% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py index 77e689c52..8b4e9ffc2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py @@ -1,6 +1,6 @@ """ -ndfc_version: 12 -description: Verify functionality of NdfcSwitchDetails +controller_version: 12 +description: Verify functionality of SwitchDetails """ from contextlib import contextmanager from typing import Any, Dict @@ -8,8 +8,11 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( - NdfcAnsibleImageUpgradeCommon, NdfcSwitchDetails, NdfcVersion) +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( +# ImageUpgradeCommon, SwitchDetails, ControllerVersion) + +# from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import ImageValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import SwitchDetails from .fixture import load_fixture @@ -19,15 +22,23 @@ def does_not_raise(): yield -dcnm_send_patch = ( - "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -) +# dcnm_send_patch = ( +# "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +# ) + +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_common = patch_module_utils + "common." +dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" +dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" +dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +dcnm_send_switch_details = patch_image_mgmt + "switch_details.dcnm_send" -def responses_ndfc_switch_details(key: str) -> Dict[str, str]: - response_file = f"dcnm_image_upgrade_responses_NdfcSwitchDetails" +def responses_switch_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchDetails" response = load_fixture(response_file).get(key) - print(f"responses_ndfc_switch_details: {key} : {response}") + print(f"responses_switch_details: {key} : {response}") return response @@ -40,13 +51,13 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture def module(): - return NdfcSwitchDetails(MockAnsibleModule) + return SwitchDetails(MockAnsibleModule) def test_init(module) -> None: module.__init__(MockAnsibleModule) - assert isinstance(module, NdfcSwitchDetails) - assert module.class_name == "NdfcSwitchDetails" + assert isinstance(module, SwitchDetails) + assert module.class_name == "SwitchDetails" def test_init_properties(module) -> None: @@ -56,9 +67,9 @@ def test_init_properties(module) -> None: module._init_properties() assert isinstance(module.properties, dict) assert module.properties.get("ip_address") == None - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None # test_ip_address @@ -75,7 +86,7 @@ def test_ip_address(module, ip_address_is_set, expected) -> None: """ Function description: - NdfcSwitchDetails.ip_address returns: + SwitchDetails.ip_address returns: - IP Address, if the user has set ip_address - None, if the user has not already set ip_address @@ -93,31 +104,31 @@ def test_refresh(monkeypatch, module) -> None: """ Function description: - NdfcSwitchDetails.refresh sets the following properties: - - ndfc_data - - ndfc_response - - ndfc_result + SwitchDetails.refresh sets the following properties: + - response_data + - response + - result Expected results: - 1. instance.ndfc_data is a dictionary - 2. instance.ndfc_response is a dictionary - 3. instance.ndfc_data is a dictionary + 1. instance.response_data is a dictionary + 2. instance.response is a dictionary + 3. instance.response_data is a dictionary """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcSwitchDetails_get_return_code_200" - return responses_ndfc_switch_details(key) + def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + key = "SwitchDetails_get_return_code_200" + return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) module.refresh() - assert isinstance(module.ndfc_data, dict) - assert isinstance(module.ndfc_result, dict) - assert isinstance(module.ndfc_response, dict) + assert isinstance(module.response_data, dict) + assert isinstance(module.result, dict) + assert isinstance(module.response, dict) -def test_refresh_ndfc_data(monkeypatch, module) -> None: +def test_refresh_response_data(monkeypatch, module) -> None: """ Function description: @@ -125,18 +136,18 @@ def test_refresh_ndfc_data(monkeypatch, module) -> None: Expected results: - 1. instance.ndfc_data is a dictionary + 1. instance.response_data is a dictionary 2. When instance.ip_address is set, getter properties will return values specific to ip_address """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcSwitchDetails_get_return_code_200" - return responses_ndfc_switch_details(key) + def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + key = "SwitchDetails_get_return_code_200" + return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) module.refresh() - assert isinstance(module.ndfc_data, dict) + assert isinstance(module.response_data, dict) module.ip_address = "172.22.150.110" assert module.hostname == "cvd-1111-bgw" module.ip_address = "172.22.150.111" @@ -157,46 +168,46 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key,expected", [ - ("NdfcSwitchDetails_get_return_code_200", does_not_raise()), + ("SwitchDetails_get_return_code_200", does_not_raise()), ( - "NdfcSwitchDetails_get_return_code_404", + "SwitchDetails_get_return_code_404", pytest.raises(AnsibleFailJson, match=match), ), ( - "NdfcSwitchDetails_get_return_code_500", + "SwitchDetails_get_return_code_500", pytest.raises(AnsibleFailJson, match=match), ), ], ) -def test_ndfc_result(monkeypatch, module, key, expected) -> None: +def test_result(monkeypatch, module, key, expected) -> None: """ Function description: - NdfcSwitchDetails.ndfc_result returns the result of its superclass - method NdfcAnsibleImageUpgradeCommon._handle_response() + SwitchDetails.result returns the result of its superclass + method ImageUpgradeCommon._handle_response() Expectations: 1. 200 RETURN_CODE, MESSAGE == "OK", - NdfcSwitchDetails.ndfc_result == {'found': True, 'success': True} + SwitchDetails.result == {'found': True, 'success': True} 2. 404 RETURN_CODE, MESSAGE == "Not Found", - NdfcSwitchDetails.ndfc_result == {'found': False, 'success': True} + SwitchDetails.result == {'found': False, 'success': True} 3. 500 RETURN_CODE, MESSAGE ~= "Internal Server Error", - NdfcSwitchDetails.ndfc_result == {'found': False, 'success': False} + SwitchDetails.result == {'found': False, 'success': False} Expected results: - 1. NdfcSwitchDetails_result_200 == {'found': True, 'success': True} - 2. NdfcSwitchDetails_result_404 == {'found': False, 'success': True} - 3. NdfcSwitchDetails_result_500 == {'found': False, 'success': False} + 1. SwitchDetails_result_200 == {'found': True, 'success': True} + 2. SwitchDetails_result_404 == {'found': False, 'success': True} + 3. SwitchDetails_result_500 == {'found': False, 'success': False} """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - return responses_ndfc_switch_details(key) + def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) with expected: module.refresh() @@ -221,7 +232,7 @@ def test_get_with_ip_address_set(monkeypatch, module, item, expected) -> None: """ Function description: - NdfcSwitchDetails._get is called by all getter properties. + SwitchDetails._get is called by all getter properties. It raises AnsibleFailJson if the user has not set ip_address. It returns the value of the requested property if the user has set ip_address. The property value is passed to both make_boolean() and make_none(), which @@ -232,20 +243,20 @@ def test_get_with_ip_address_set(monkeypatch, module, item, expected) -> None: Expectations: - 1. NdfcVersion._get returns above values + 1. ControllerVersion._get returns above values given corresponding responses Expected results: - 1. NdfcVersion_mode_LAN == "LAN" - 2. NdfcVersion_mode_none == None + 1. ControllerVersion_mode_LAN == "LAN" + 2. ControllerVersion_mode_none == None """ - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - key = "NdfcSwitchDetails_get_return_code_200" - return responses_ndfc_switch_details(key) + def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + key = "SwitchDetails_get_return_code_200" + return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) module.refresh() module.ip_address = "172.22.150.110" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py similarity index 57% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByDeviceName.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py index 3040f5ee5..7e989de3b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py @@ -3,21 +3,23 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ - NdfcSwitchIssuDetailsByDeviceName +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ +# SwitchIssuDetailsByDeviceName + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsByDeviceName + from .fixture import load_fixture """ -ndfc_version: 12 -description: Verify functionality of subclass NdfcSwitchIssuDetailsByDeviceName +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsByDeviceName """ -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -# Here, we are using the superclass name, since we are sharing the -# same response file across all subclasses. -class_name = "NdfcSwitchIssuDetails" -response_file = f"dcnm_image_upgrade_responses_{class_name}" +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." +dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" class MockAnsibleModule: params = {} @@ -26,15 +28,16 @@ def fail_json(msg) -> AnsibleFailJson: raise AnsibleFailJson(msg) -def response_data(key: str) -> Dict[str, str]: +def responses_switch_issu_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) - print(f"response_data: {key} : {response}") + print(f"responses_switch_issu_details: {key} : {response}") return response @pytest.fixture def module(): - return NdfcSwitchIssuDetailsByDeviceName(MockAnsibleModule) + return SwitchIssuDetailsByDeviceName(MockAnsibleModule) def test_init_properties(module) -> None: @@ -47,9 +50,9 @@ def test_init_properties(module) -> None: assert isinstance(module.properties, dict) assert isinstance(module.properties.get("action_keys"), set) assert module.properties.get("action_keys") == action_keys - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None assert module.properties.get("device_name") == None @@ -61,15 +64,15 @@ def test_refresh_return_code_200(monkeypatch, module) -> None: """ key = "packagemgnt_issu_get_return_code_200_one_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() - assert isinstance(module.ndfc_response, dict) - assert isinstance(module.ndfc_data, list) + assert isinstance(module.response, dict) + assert isinstance(module.response_data, list) def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: @@ -79,11 +82,11 @@ def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: """ key = "packagemgnt_issu_get_return_code_200_many_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() module.device_name = "leaf1" @@ -137,63 +140,63 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: assert module.vpc_role == None -def test_ndfc_result_return_code_200(monkeypatch, module) -> None: +def test_result_return_code_200(monkeypatch, module) -> None: """ - ndfc_result contains expected key/values on 200 response from endpoint. + result contains expected key/values on 200 response from endpoint. endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu """ key = "packagemgnt_issu_get_return_code_200_one_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() - assert isinstance(module.ndfc_result, dict) - assert module.ndfc_result.get("found") == True - assert module.ndfc_result.get("success") == True + assert isinstance(module.result, dict) + assert module.result.get("found") == True + assert module.result.get("success") == True -def test_ndfc_result_return_code_404(monkeypatch, module) -> None: +def test_result_return_code_404(monkeypatch, module) -> None: """ fail_json is called on 404 response from malformed endpoint. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_404" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "Bad result when retriving switch information from NDFC" with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_ndfc_result_return_code_200_empty_data(monkeypatch, module) -> None: +def test_result_return_code_200_empty_data(monkeypatch, module) -> None: """ fail_json is called on 200 response with empty DATA key. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_200_empty_DATA" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "NdfcSwitchIssuDetailsByDeviceName.refresh: " + error_message = "SwitchIssuDetailsByDeviceName.refresh: " error_message += "NDFC has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( +def test_result_return_code_200_switch_issu_info_length_0( monkeypatch, module ) -> None: """ @@ -201,15 +204,15 @@ def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_200" - key += "_ndfc_switch_issu_info_length_0" + key += "_switch_issu_info_length_0" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "NdfcSwitchIssuDetailsByDeviceName.refresh: " + error_message = "SwitchIssuDetailsByDeviceName.refresh: " error_message += "NDFC has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py similarity index 58% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByIpAddress.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py index b6bff76a8..eb633b285 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py @@ -3,20 +3,23 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ - NdfcSwitchIssuDetailsByIpAddress +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ +# SwitchIssuDetailsByIpAddress + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsByIpAddress + from .fixture import load_fixture """ -ndfc_version: 12 -description: Verify functionality of subclass NdfcSwitchIssuDetailsByIpAddress +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsByIpAddress """ -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +# dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." -# Here, we are using the superclass name, since we are sharing the -# same response file across all subclasses. -class_name = "NdfcSwitchIssuDetails" -response_file = f"dcnm_image_upgrade_responses_{class_name}" +dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" class MockAnsibleModule: @@ -26,15 +29,16 @@ def fail_json(msg) -> AnsibleFailJson: raise AnsibleFailJson(msg) -def response_data(key: str) -> Dict[str, str]: +def responses_switch_issu_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) - print(f"response_data: {key} : {response}") + print(f"responses_switch_issu_details: {key} : {response}") return response @pytest.fixture def module(): - return NdfcSwitchIssuDetailsByIpAddress(MockAnsibleModule) + return SwitchIssuDetailsByIpAddress(MockAnsibleModule) def test_init_properties(module) -> None: @@ -47,9 +51,9 @@ def test_init_properties(module) -> None: assert isinstance(module.properties, dict) assert isinstance(module.properties.get("action_keys"), set) assert module.properties.get("action_keys") == action_keys - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None assert module.properties.get("ip_address") == None @@ -61,15 +65,15 @@ def test_refresh_return_code_200(monkeypatch, module) -> None: """ key = "packagemgnt_issu_get_return_code_200_one_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() - assert isinstance(module.ndfc_response, dict) - assert isinstance(module.ndfc_data, list) + assert isinstance(module.response, dict) + assert isinstance(module.response_data, list) def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: @@ -79,11 +83,11 @@ def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: """ key = "packagemgnt_issu_get_return_code_200_many_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() module.ip_address = "172.22.150.102" @@ -137,63 +141,63 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: assert module.vpc_role == None -def test_ndfc_result_return_code_200(monkeypatch, module) -> None: +def test_result_return_code_200(monkeypatch, module) -> None: """ - ndfc_result contains expected key/values on 200 response from endpoint. + result contains expected key/values on 200 response from endpoint. endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu """ key = "packagemgnt_issu_get_return_code_200_one_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() - assert isinstance(module.ndfc_result, dict) - assert module.ndfc_result.get("found") == True - assert module.ndfc_result.get("success") == True + assert isinstance(module.result, dict) + assert module.result.get("found") == True + assert module.result.get("success") == True -def test_ndfc_result_return_code_404(monkeypatch, module) -> None: +def test_result_return_code_404(monkeypatch, module) -> None: """ fail_json is called on 404 response from malformed endpoint. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_404" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "Bad result when retriving switch information from NDFC" with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_ndfc_result_return_code_200_empty_data(monkeypatch, module) -> None: +def test_result_return_code_200_empty_data(monkeypatch, module) -> None: """ fail_json is called on 200 response with empty DATA key. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_200_empty_DATA" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "NdfcSwitchIssuDetailsByIpAddress.refresh: " + error_message = "SwitchIssuDetailsByIpAddress.refresh: " error_message += "NDFC has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( +def test_result_return_code_200_switch_issu_info_length_0( monkeypatch, module ) -> None: """ @@ -201,15 +205,15 @@ def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_200" - key += "_ndfc_switch_issu_info_length_0" + key += "_switch_issu_info_length_0" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "NdfcSwitchIssuDetailsByIpAddress.refresh: " + error_message = "SwitchIssuDetailsByIpAddress.refresh: " error_message += "NDFC has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py similarity index 57% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsBySerialNumber.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py index 7d2449016..90be45011 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_dcnm_image_upgrade_NdfcSwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py @@ -3,20 +3,28 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ - NdfcSwitchIssuDetailsBySerialNumber +# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ +# SwitchIssuDetailsBySerialNumber + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber + from .fixture import load_fixture """ -ndfc_version: 12 -description: Verify functionality of subclass NdfcSwitchIssuDetailsBySerialNumber +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsBySerialNumber """ -dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" +# dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" + +patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." +patch_image_mgmt = patch_module_utils + "image_mgmt." + +dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" # Here, we are using the superclass name, since we are sharing the # same response file across all subclasses. -class_name = "NdfcSwitchIssuDetails" -response_file = f"dcnm_image_upgrade_responses_{class_name}" +# class_name = "SwitchIssuDetails" +# response_file = f"image_upgrade_responses_{class_name}" class MockAnsibleModule: @@ -26,15 +34,17 @@ def fail_json(msg) -> AnsibleFailJson: raise AnsibleFailJson(msg) -def response_data(key: str) -> Dict[str, str]: +def responses_switch_issu_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) - print(f"response_data: {key} : {response}") + print(f"responses_switch_issu_details: {key} : {response}") return response + @pytest.fixture def module(): - return NdfcSwitchIssuDetailsBySerialNumber(MockAnsibleModule) + return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) def test_init_properties(module) -> None: @@ -47,9 +57,9 @@ def test_init_properties(module) -> None: assert isinstance(module.properties, dict) assert isinstance(module.properties.get("action_keys"), set) assert module.properties.get("action_keys") == action_keys - assert module.properties.get("ndfc_data") == None - assert module.properties.get("ndfc_response") == None - assert module.properties.get("ndfc_result") == None + assert module.properties.get("response_data") == None + assert module.properties.get("response") == None + assert module.properties.get("result") == None assert module.properties.get("serial_number") == None @@ -61,15 +71,15 @@ def test_refresh_return_code_200(monkeypatch, module) -> None: """ key = "packagemgnt_issu_get_return_code_200_one_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() - assert isinstance(module.ndfc_response, dict) - assert isinstance(module.ndfc_data, list) + assert isinstance(module.response, dict) + assert isinstance(module.response_data, list) def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: @@ -79,11 +89,11 @@ def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: """ key = "packagemgnt_issu_get_return_code_200_many_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() module.serial_number = "FDO21120U5D" @@ -137,63 +147,63 @@ def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: assert module.vpc_role == None -def test_ndfc_result_return_code_200(monkeypatch, module) -> None: +def test_result_return_code_200(monkeypatch, module) -> None: """ - ndfc_result contains expected key/values on 200 response from endpoint. + result contains expected key/values on 200 response from endpoint. endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu """ key = "packagemgnt_issu_get_return_code_200_one_switch" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) module.refresh() - assert isinstance(module.ndfc_result, dict) - assert module.ndfc_result.get("found") == True - assert module.ndfc_result.get("success") == True + assert isinstance(module.result, dict) + assert module.result.get("found") == True + assert module.result.get("success") == True -def test_ndfc_result_return_code_404(monkeypatch, module) -> None: +def test_result_return_code_404(monkeypatch, module) -> None: """ fail_json is called on 404 response from malformed endpoint. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_404" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "Bad result when retriving switch information from NDFC" with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_ndfc_result_return_code_200_empty_data(monkeypatch, module) -> None: +def test_result_return_code_200_empty_data(monkeypatch, module) -> None: """ fail_json is called on 200 response with empty DATA key. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_200_empty_DATA" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "NdfcSwitchIssuDetailsBySerialNumber.refresh: " + error_message = "SwitchIssuDetailsBySerialNumber.refresh: " error_message += "NDFC has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( +def test_result_return_code_200_switch_issu_info_length_0( monkeypatch, module ) -> None: """ @@ -201,15 +211,15 @@ def test_ndfc_result_return_code_200_ndfc_switch_issu_info_length_0( endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess """ key = "packagemgnt_issu_get_return_code_200" - key += "_ndfc_switch_issu_info_length_0" + key += "_switch_issu_info_length_0" - def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send: {response_data(key)}") - return response_data(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "NdfcSwitchIssuDetailsBySerialNumber.refresh: " + error_message = "SwitchIssuDetailsBySerialNumber.refresh: " error_message += "NDFC has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() From 1b72659fd304ef638325e49ce476f0fd6200f918 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 1 Nov 2023 15:01:25 -1000 Subject: [PATCH 017/300] Add unit tests for instance._get() and remove commented code --- .../test_image_upgrade_SwitchDetails.py | 90 +++++++++++++----- ...e_upgrade_SwitchIssuDetailsByDeviceName.py | 89 ++++++++++++++---- ...ge_upgrade_SwitchIssuDetailsByIpAddress.py | 86 +++++++++++++---- ...upgrade_SwitchIssuDetailsBySerialNumber.py | 93 ++++++++++++++----- 4 files changed, 277 insertions(+), 81 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py index 8b4e9ffc2..dec29c405 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py @@ -8,11 +8,8 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# ImageUpgradeCommon, SwitchDetails, ControllerVersion) - -# from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import ImageValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import SwitchDetails +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ + SwitchDetails from .fixture import load_fixture @@ -22,19 +19,12 @@ def does_not_raise(): yield -# dcnm_send_patch = ( -# "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -# ) - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." -patch_common = patch_module_utils + "common." +patch_image_mgmt = patch_module_utils + "image_mgmt." -dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" -dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" -dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" dcnm_send_switch_details = patch_image_mgmt + "switch_details.dcnm_send" + def responses_switch_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchDetails" response = load_fixture(response_file).get(key) @@ -162,7 +152,7 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: assert module.serial_number == "FOX2109PGD1" -match = "Unable to retrieve switch information from NDFC. " +match = "Unable to retrieve switch information from the controller. " @pytest.mark.parametrize( @@ -233,23 +223,75 @@ def test_get_with_ip_address_set(monkeypatch, module, item, expected) -> None: Function description: SwitchDetails._get is called by all getter properties. - It raises AnsibleFailJson if the user has not set ip_address. - It returns the value of the requested property if the user has set ip_address. + + It raises AnsibleFailJson if the user has not set ip_address or if + the ip_address is unknown, or if an unknown property name is queried. + + It returns the value of the requested property if the user has set + ip_address and the property name is known. + The property value is passed to both make_boolean() and make_none(), which either: - converts it to a boolean - converts it to NoneType - returns the value unchanged - Expectations: + Expected results: + + 1. Property values are returned as expected + """ + + def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + key = "SwitchDetails_get_return_code_200" + return responses_switch_details(key) + + monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + + module.refresh() + module.ip_address = "172.22.150.110" + assert module._get(item) == expected + +def test_get_with_unknown_ip_address(monkeypatch, module) -> None: + """ + Function description: - 1. ControllerVersion._get returns above values - given corresponding responses + SwitchDetails._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set ip_address or if + the ip_address is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has set a known + ip_address. Expected results: - 1. ControllerVersion_mode_LAN == "LAN" - 2. ControllerVersion_mode_none == None + 1. fail_json is called with appropriate error message since an unknown + ip_address is set. + """ + + def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + key = "SwitchDetails_get_return_code_200" + return responses_switch_details(key) + + monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + + module.refresh() + module.ip_address = "1.1.1.1" + match = "SwitchDetails._get: 1.1.1.1 does not exist " + match += "on the controller." + with pytest.raises(AnsibleFailJson, match=match): + module._get("hostName") + +def test_get_with_unknown_property_name(monkeypatch, module) -> None: + """ + Function description: + + SwitchDetails._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set ip_address or if + the ip_address is unknown, or if an unknown property name is queried. + + Expected results: + + 1. fail_json is called with appropriate error message since an + unknown property name is queried. """ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: @@ -260,4 +302,6 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: module.refresh() module.ip_address = "172.22.150.110" - assert module._get(item) == expected + match = "SwitchDetails._get: 172.22.150.110 does not have a key named FOO." + with pytest.raises(AnsibleFailJson, match=match): + module._get("FOO") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py index 7e989de3b..aae571c70 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py @@ -1,26 +1,24 @@ +""" +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsByDeviceName +""" + from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ -# SwitchIssuDetailsByDeviceName - -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsByDeviceName +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsByDeviceName from .fixture import load_fixture -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsByDeviceName -""" -#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_image_mgmt = patch_module_utils + "image_mgmt." dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" + class MockAnsibleModule: params = {} @@ -60,7 +58,6 @@ def test_refresh_return_code_200(monkeypatch, module) -> None: """ NDFC response data for 200 response has expected types. endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu - """ key = "packagemgnt_issu_get_return_code_200_one_switch" @@ -172,7 +169,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "Bad result when retriving switch information from NDFC" + error_message = "Bad result when retriving switch information from the controller" with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() @@ -191,14 +188,12 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "SwitchIssuDetailsByDeviceName.refresh: " - error_message += "NDFC has no switch ISSU information." + error_message += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_result_return_code_200_switch_issu_info_length_0( - monkeypatch, module -) -> None: +def test_result_return_code_200_switch_issu_info_length_0(monkeypatch, module) -> None: """ fail_json is called on 200 response with DATA.lastOperDataObject length 0. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess @@ -213,6 +208,64 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "SwitchIssuDetailsByDeviceName.refresh: " - error_message += "NDFC has no switch ISSU information." + error_message += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() + +def test_get_with_unknown_device_name(monkeypatch, module) -> None: + """ + Function description: + + SwitchIssuDetailsByDeviceName._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set device_name or if + device_name is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has set a known + device_name. + + Expected results: + + 1. fail_json is called with appropriate error message since an unknown + device_name is set. + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "packagemgnt_issu_get_return_code_200_one_switch" + return responses_switch_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.refresh() + module.device_name = "FOO" + match = "SwitchIssuDetailsByDeviceName._get: FOO does not exist " + match += "on the controller." + with pytest.raises(AnsibleFailJson, match=match): + module._get("serialNumber") + +def test_get_with_unknown_property_name(monkeypatch, module) -> None: + """ + Function description: + + SwitchIssuDetailsByDeviceName._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set device_name or if + device_name is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has set a known + ip_address. + + Expected results: + + 1. fail_json is called with appropriate error message since an unknown + property is queried. + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "packagemgnt_issu_get_return_code_200_one_switch" + return responses_switch_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.refresh() + module.device_name = "leaf1" + match = "SwitchIssuDetailsByDeviceName._get: leaf1 unknown " + match += f"property name: FOO" + with pytest.raises(AnsibleFailJson, match=match): + module._get("FOO") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py index eb633b285..cd29cd774 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py @@ -1,23 +1,21 @@ +""" +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsByIpAddress +""" + from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ -# SwitchIssuDetailsByIpAddress - -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsByIpAddress +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsByIpAddress from .fixture import load_fixture -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsByIpAddress -""" -# dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_image_mgmt = patch_module_utils + "image_mgmt." dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" @@ -173,7 +171,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "Bad result when retriving switch information from NDFC" + error_message = "Bad result when retriving switch information from the controller" with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() @@ -192,14 +190,12 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "SwitchIssuDetailsByIpAddress.refresh: " - error_message += "NDFC has no switch ISSU information." + error_message += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_result_return_code_200_switch_issu_info_length_0( - monkeypatch, module -) -> None: +def test_result_return_code_200_switch_issu_info_length_0(monkeypatch, module) -> None: """ fail_json is called on 200 response with DATA.lastOperDataObject length 0. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess @@ -214,6 +210,64 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "SwitchIssuDetailsByIpAddress.refresh: " - error_message += "NDFC has no switch ISSU information." + error_message += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() + +def test_get_with_unknown_ip_address(monkeypatch, module) -> None: + """ + Function description: + + SwitchIssuDetailsByIpAddress._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set ip_address or if + the ip_address is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has set a known + ip_address. + + Expected results: + + 1. fail_json is called with appropriate error message since an unknown + ip_address is set. + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "packagemgnt_issu_get_return_code_200_one_switch" + return responses_switch_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.refresh() + module.ip_address = "1.1.1.1" + match = "SwitchIssuDetailsByIpAddress._get: 1.1.1.1 does not exist " + match += "on the controller." + with pytest.raises(AnsibleFailJson, match=match): + module._get("serialNumber") + +def test_get_with_unknown_property_name(monkeypatch, module) -> None: + """ + Function description: + + SwitchIssuDetailsByIpAddress._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set ip_address or if + the ip_address is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has set a + known ip_address and the property name is valid. + + Expected results: + + 1. fail_json is called with appropriate error message since an unknown + property is queried. + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "packagemgnt_issu_get_return_code_200_one_switch" + return responses_switch_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.refresh() + module.ip_address = "172.22.150.102" + match = "SwitchIssuDetailsByIpAddress._get: 172.22.150.102 unknown " + match += f"property name: FOO" + with pytest.raises(AnsibleFailJson, match=match): + module._get("FOO") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py index 90be45011..8ffd90971 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py @@ -1,32 +1,22 @@ +""" +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsBySerialNumber +""" from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ -# SwitchIssuDetailsBySerialNumber - -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsBySerialNumber from .fixture import load_fixture -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsBySerialNumber -""" -# dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_image_mgmt = patch_module_utils + "image_mgmt." dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" -# Here, we are using the superclass name, since we are sharing the -# same response file across all subclasses. -# class_name = "SwitchIssuDetails" -# response_file = f"image_upgrade_responses_{class_name}" - - class MockAnsibleModule: params = {} @@ -41,7 +31,6 @@ def responses_switch_issu_details(key: str) -> Dict[str, str]: return response - @pytest.fixture def module(): return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) @@ -179,7 +168,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "Bad result when retriving switch information from NDFC" + error_message = "Bad result when retriving switch information from the controller" with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() @@ -198,14 +187,12 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "SwitchIssuDetailsBySerialNumber.refresh: " - error_message += "NDFC has no switch ISSU information." + error_message += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() -def test_result_return_code_200_switch_issu_info_length_0( - monkeypatch, module -) -> None: +def test_result_return_code_200_switch_issu_info_length_0(monkeypatch, module) -> None: """ fail_json is called on 200 response with DATA.lastOperDataObject length 0. endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess @@ -220,6 +207,64 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) error_message = "SwitchIssuDetailsBySerialNumber.refresh: " - error_message += "NDFC has no switch ISSU information." + error_message += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() + +def test_get_with_unknown_serial_number(monkeypatch, module) -> None: + """ + Function description: + + SwitchIssuDetailsBySerialNumber._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set serial_number or if + serial_number is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has set a known + serial_number and the property name is valid. + + Expected results: + + 1. fail_json is called with appropriate error message since an unknown + serial_number is set. + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "packagemgnt_issu_get_return_code_200_one_switch" + return responses_switch_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.refresh() + module.serial_number = "FOO00000BAR" + match = "SwitchIssuDetailsBySerialNumber._get: FOO00000BAR does not exist " + match += "on the controller." + with pytest.raises(AnsibleFailJson, match=match): + module._get("serialNumber") + +def test_get_with_unknown_property_name(monkeypatch, module) -> None: + """ + Function description: + + SwitchIssuDetailsBySerialNumber._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set serial_number or if + serial_number is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has set a known + serial_number and the property name is valid. + + Expected results: + + 1. fail_json is called with appropriate error message since an unknown + property is queried. + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "packagemgnt_issu_get_return_code_200_one_switch" + return responses_switch_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.refresh() + module.serial_number = "FDO21120U5D" + match = "SwitchIssuDetailsBySerialNumber._get: FDO21120U5D unknown " + match += f"property name: FOO" + with pytest.raises(AnsibleFailJson, match=match): + module._get("FOO") From 240600dccd0c57c81ea75666549103dc260bc312 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 1 Nov 2023 15:03:27 -1000 Subject: [PATCH 018/300] Remove commented code, cleanup more NDFC refs --- .../image_mgmt/image_policy_action.py | 5 +- .../module_utils/image_mgmt/image_stage.py | 18 +-- .../module_utils/image_mgmt/image_upgrade.py | 21 +-- .../image_mgmt/image_upgrade_common.py | 8 +- .../module_utils/image_mgmt/image_validate.py | 36 ++--- .../image_mgmt/install_options.py | 21 +-- .../module_utils/image_mgmt/switch_details.py | 19 ++- .../image_mgmt/switch_issu_details.py | 52 ++++--- plugins/modules/dcnm_image_upgrade.py | 142 ++++++++++++------ plugins/modules/dcnm_image_upgrade_orig.py | 10 +- .../dcnm/dcnm_image_upgrade/fixture.py | 5 +- .../test_image_upgrade_ApiEndpoints.py | 7 +- .../test_image_upgrade_ControllerVersion.py | 32 +++- .../test_image_upgrade_ImageInstallOptions.py | 18 ++- .../test_image_upgrade_ImagePolicies.py | 21 ++- .../test_image_upgrade_ImagePolicyAction.py | 13 +- .../test_image_upgrade_ImageStage.py | 20 ++- .../test_image_upgrade_ImageUpgrade.py | 34 +++-- .../test_image_upgrade_ImageUpgradeCommon.py | 45 +----- .../test_image_upgrade_ImageValidate.py | 24 ++- 20 files changed, 306 insertions(+), 245 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 3646428c8..2af77d8f5 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -62,7 +62,7 @@ def build_attach_payload(self): caller _attach_policy() """ self.payloads = [] - # TODO:2 this results in more calls than necessary to NDFC. Need a better way to handle this. + # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it self.switch_issu_details.refresh() for serial_number in self.serial_numbers: self.switch_issu_details.serial_number = serial_number @@ -78,7 +78,8 @@ def build_attach_payload(self): msg += f"{self.switch_issu_details.ip_address}, " msg += f"{self.switch_issu_details.serial_number}, " msg += f"{self.switch_issu_details.device_name}. " - msg += "Please verify that the switch is managed by NDFC." + msg += "Please verify that the switch is managed by " + msg += "the controller." self.module.fail_json(msg) self.payloads.append(payload) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 28aef8bd6..00d70a647 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -95,7 +95,7 @@ def _init_properties(self): def _populate_controller_version(self): """ - Populate self.controller_version with the NDFC version. + Populate self.controller_version with the running controller version. Notes: 1. This cannot go into ImageUpgradeCommon() due to circular @@ -136,7 +136,7 @@ def validate_serial_numbers(self): msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " msg += f"{self.issu_detail.serial_number}. " - msg += f"Check the switch connectivity to NDFC " + msg += f"Check the switch connectivity to the controller " msg += "and try again." self.module.fail_json(msg) else: @@ -144,7 +144,7 @@ def validate_serial_numbers(self): def commit(self): """ - Commit the image staging request to NDFC and wait + Commit the image staging request to the controller and wait for the images to be staged. """ if self.serial_numbers is None: @@ -167,7 +167,7 @@ def commit(self): self.payload = {} self._populate_controller_version() if self.controller_version == "12.1.2e": - # Yes, NDFC 12.1.2e wants serialNum to be misspelled + # Yes, version 12.1.2e wants serialNum to be misspelled self.payload["sereialNum"] = self.serial_numbers else: self.payload["serialNumbers"] = self.serial_numbers @@ -181,15 +181,15 @@ def commit(self): self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") if not self.result["success"]: msg = f"{self.class_name}.commit() failed: {self.result}. " - msg += f"NDFC response was: {self.response}" + msg += f"Controller response: {self.response}" self.module.fail_json(msg) self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_stage_to_complete() def _wait_for_current_actions_to_complete(self): """ - NDFC will not stage an image if there are any actions in progress. - Wait for all actions to complete before staging image. + The controller will not stage an image if there are any actions in + progress. Wait for all actions to complete before staging image. Actions include image staging, image upgrade, and image validation. """ self.serial_numbers_done = set() @@ -339,14 +339,14 @@ def response_data(self): @property def result(self): """ - Return the POST result from NDFC + Return the POST result from the controller """ return self.properties.get("result") @property def response(self): """ - Return the POST response from NDFC + Return the POST response from the controller """ return self.properties.get("response") diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index b3e82b5b1..cca718bee 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -93,7 +93,7 @@ class ImageUpgrade(ImageUpgradeCommon): } Response bodies: Responses are text, not JSON, and are returned immediately. - They do not contain useful information. We need to poll NDFC + They do not contain useful information. We need to poll the controller to determine when the upgrade is complete. Basically, we ignore these responses in favor of the poll responses. - If an action is in progress, text is returned: @@ -294,7 +294,7 @@ def build_payload(self, device): # nxos_mode: The choices for nxos_mode are mutually-exclusive. # If one is set to True, the others must be False. - # nonDisruptive corresponds to NDFC Allow Non-Disruptive GUI option + # nonDisruptive corresponds to Allow Non-Disruptive GUI option self.payload["issuUpgradeOptions1"] = {} self.payload["issuUpgradeOptions1"]["nonDisruptive"] = False self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = False @@ -312,7 +312,7 @@ def build_payload(self, device): if nxos_mode == "force_non_disruptive": self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True - # biosForce corresponds to NDFC BIOS Force GUI option + # biosForce corresponds to BIOS Force GUI option bios_force = device.get("options").get("nxos").get("bios_force") if not isinstance(bios_force, bool): msg = f"{self.class_name}.build_payload() options.nxos.bios_force " @@ -376,7 +376,7 @@ def build_payload(self, device): def commit(self): """ - Commit the image upgrade request to NDFC and wait + Commit the image upgrade request to the controller and wait for the images to be upgraded. """ if self.devices is None: @@ -405,15 +405,15 @@ def commit(self): self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") if not self.result["success"]: msg = f"{self.class_name}.commit() failed: {self.result}. " - msg += f"NDFC response was: {self.response}" + msg += f"Controller response: {self.response}" self.module.fail_json(msg) self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_upgrade_to_complete() def _wait_for_current_actions_to_complete(self): """ - NDFC will not upgrade an image if there are any actions in progress. - Wait for all actions to complete before upgrading image. + The controller will not upgrade an image if there are any actions + in progress. Wait for all actions to complete before upgrading image. Actions include image staging, image upgrade, and image validation. """ ipv4_todo = copy.copy(self.ip_addresses) @@ -770,7 +770,8 @@ def check_timeout(self): @property def response_data(self): """ - Return the data retrieved from NDFC for the image upgrade request. + Return the data retrieved from the controller for the + image upgrade request. instance.devices must be set first. instance.commit() must be called first. @@ -780,7 +781,7 @@ def response_data(self): @property def result(self): """ - Return the POST result from NDFC + Return the POST result. instance.devices must be set first. instance.commit() must be called first. """ @@ -789,7 +790,7 @@ def result(self): @property def response(self): """ - Return the POST response from NDFC + Return the POST response from the controller instance.devices must be set first. instance.commit() must be called first. """ diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 43c73b657..6961a773b 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -1,6 +1,6 @@ class ImageUpgradeCommon: """ - Base class for the other NDFC classes + Base class for the other image upgrade classes Usage (where module is an instance of AnsibleModule): @@ -33,7 +33,7 @@ def _handle_get_response(self, response): """ Caller: - self._handle_response() - Handle NDFC responses to GET requests + Handle controller responses to GET requests Returns: dict() with the following keys: - found: - False, if request error was "Not found" and RETURN_CODE == 404 @@ -67,11 +67,11 @@ def _handle_post_put_delete_response(self, response): Caller: - self.self._handle_response() - Handle POST, PUT responses from NDFC. + Handle POST, PUT responses from the controller. Returns: dict() with the following keys: - changed: - - True if changes were made to NDFC + - True if changes were made to by the controller - False otherwise - success: - False if RETURN_CODE != 200 or MESSAGE != "OK" diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 1386f4020..ca52e62de 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -58,20 +58,6 @@ def _init_properties(self): self.properties["non_disruptive"] = False self.properties["serial_numbers"] = None - # def _populate_controller_version(self): - # """ - # Populate self.controller_version with the NDFC version. - - # TODO:3 Remove if 12.1.3b works with no changes to request/response payloads. - - # Notes: - # 1. This cannot go into ImageUpgradeCommon() due to circular - # imports resulting in RecursionError - # """ - # instance = ControllerVersion(self.module) - # instance.refresh() - # self.controller_version = instance.version - def prune_serial_numbers(self): """ If the image is already validated on a switch, remove that switch's @@ -108,8 +94,8 @@ def validate_serial_numbers(self): msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " msg += f"{self.issu_detail.serial_number}. " - msg += "If this persists, check the switch connectivity to NDFC and " - msg += "try again." + msg += "If this persists, check the switch connectivity to " + msg += "the controller and try again." #self.log_msg(msg) self.module.fail_json(msg) @@ -120,7 +106,7 @@ def build_payload(self): def commit(self): """ - Commit the image validation request to NDFC and wait + Commit the image validation request to the controller and wait for the images to be validated. """ if self.serial_numbers is None: @@ -148,15 +134,15 @@ def commit(self): self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") if not self.result["success"]: msg = f"{self.class_name}.commit() failed: {self.result}. " - msg += f"NDFC response was: {self.response}" + msg += f"Controller response: {self.response}" self.module.fail_json(msg) self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_validate_to_complete() def _wait_for_current_actions_to_complete(self): """ - NDFC will not validate an image if there are any actions in progress. - Wait for all actions to complete before validating image. + The controller will not validate an image if there are any actions in + progress. Wait for all actions to complete before validating image. Actions include image staging, image upgrade, and image validation. """ self.serial_numbers_done = set() @@ -241,9 +227,9 @@ def _wait_for_image_validate_to_complete(self): msg += f"image validated percent: {validated_percent}. " msg += "Check the switch e.g. show install log detail, " msg += "show incompatibility-all nxos . Or " - msg += "check NDFC Operations > Image Management > " - msg += "Devices > View Details > Validate for " - msg += "more details." + msg += "check Operations > Image Management > " + msg += "Devices > View Details > Validate on the " + msg += "controller GUI for more details." self.module.fail_json(msg) if validated_status == "Success": @@ -331,14 +317,14 @@ def response_data(self): @property def result(self): """ - Return the POST result from NDFC + Return the POST result """ return self.properties.get("result") @property def response(self): """ - Return the POST response from NDFC + Return the POST response from the controller """ return self.properties.get("response") diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index ade4afe39..705b5c381 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -8,7 +8,7 @@ class ImageInstallOptions(ImageUpgradeCommon): """ - Retrieve install-options details for ONE switch from NDFC and + Retrieve install-options details for ONE switch from the controller and provide property accessors for the policy attributes. Caveats: @@ -26,10 +26,12 @@ class ImageInstallOptions(ImageUpgradeCommon): instance.epld = True instance.package_install = True instance.issu = True - # Retrieve install-options details from NDFC + # Retrieve install-options details from the controller instance.refresh() if instance.device_name is None: - print("Cannot retrieve policy/serial_number combination from NDFC") + msg = "Cannot retrieve policy/serial_number combination from " + msg += "the controller" + print(msg) exit(1) status = instance.status platform = instance.platform @@ -131,7 +133,7 @@ def _init_properties(self): def refresh(self): """ - Refresh self.data with current install-options from NDFC + Refresh self.data with current install-options from the controller """ if self.policy_name is None: msg = f"{self.class_name}.refresh: " @@ -157,11 +159,10 @@ def refresh(self): msg += f"response: {self.response}" self.log_msg(msg) self.properties["result"] = self._handle_response(self.response, verb) - # TODO:2 should error message contain full response or just DATA.error? if self.result["success"] is False: msg = f"{self.class_name}.refresh: " - msg += "Bad result when retrieving install-options from NDFC. " - msg += f"NDFC response: {self.response}" + msg += "Bad result when retrieving install-options from " + msg += f"the controller. Controller response: {self.response}" self.module.fail_json(msg) self.properties["response_data"] = self.response.get("DATA") @@ -358,21 +359,21 @@ def ip_address(self): @property def response_data(self): """ - Return the raw data from the NDFC response. + Return the DATA portion of the controller response. """ return self.properties.get("response_data") @property def response(self): """ - Return the response from NDFC of the query. + Return the controller response. """ return self.properties.get("response") @property def result(self): """ - Return the result from NDFC of the query. + Return the query result. """ return self.properties.get("result") diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index c7dae0135..c95392709 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -6,7 +6,7 @@ class SwitchDetails(ImageUpgradeCommon): """ - Retrieve switch details from NDFC and provide property accessors + Retrieve switch details from the controller and provide property accessors for the switch attributes. Usage (where module is an instance of AnsibleModule): @@ -41,7 +41,8 @@ def refresh(self): """ Caller: __init__() - Refresh switch_details with current switch details from NDFC + Refresh switch_details with current switch details from + the controller. """ path = self.endpoints.switches_info.get("path") verb = self.endpoints.switches_info.get("verb") @@ -55,7 +56,7 @@ def refresh(self): self.log_msg(msg) if self.response["RETURN_CODE"] != 200: - msg = "Unable to retrieve switch information from NDFC. " + msg = "Unable to retrieve switch information from the controller. " msg += f"Got response {self.response}" self.module.fail_json(msg) @@ -69,9 +70,17 @@ def refresh(self): def _get(self, item): if self.ip_address is None: - msg = f"{self.class_name}: set instance.ip_address " + msg = f"{self.class_name}._get: set instance.ip_address " msg += f"before accessing property {item}." self.module.fail_json(msg) + if self.properties["response_data"].get(self.ip_address) is None: + msg = f"{self.class_name}._get: {self.ip_address} does not exist " + msg += f"on the controller." + self.module.fail_json(msg) + if self.properties["response_data"][self.ip_address].get(item) is None: + msg = f"{self.class_name}._get: {self.ip_address} does not have a key" + msg += f" named {item}." + self.module.fail_json(msg) return self.make_boolean( self.make_none( self.properties["response_data"][self.ip_address].get(item) @@ -159,7 +168,7 @@ def platform(self): Return the platform of the switch with ip_address, if it exists. Return None otherwise - NOTE: This is derived from "model" and is not in the NDFC response + NOTE: This is derived from "model". Is not in the controller response. """ model = self._get("model") if model is None: diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index b7b1277f8..88cc221d7 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -5,8 +5,8 @@ class SwitchIssuDetails(ImageUpgradeCommon): """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes. + Retrieve switch issu details from the controller and provide + property accessors for the switch attributes. Usage: See subclasses. @@ -83,7 +83,7 @@ def _init_properties(self): def refresh(self) -> None: """ - Refresh current issu details from NDFC + Refresh current issu details from the controller. """ path = self.endpoints.issu_info.get("path") verb = self.endpoints.issu_info.get("verb") @@ -101,17 +101,17 @@ def refresh(self) -> None: if self.result["success"] == False or self.result["found"] == False: msg = f"{self.class_name}.refresh: " msg += "Bad result when retriving switch " - msg += "information from NDFC" + msg += "information from the controller" self.module.fail_json(msg) data = self.response.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.refresh: " - msg += "NDFC has no switch ISSU information." + msg += "The controller has no switch ISSU information." self.module.fail_json(msg) if len(data) == 0: msg = f"{self.class_name}.refresh: " - msg += "NDFC has no switch ISSU information." + msg += "The controller has no switch ISSU information." self.module.fail_json(msg) self.properties["response_data"] = self.response.get("DATA", {}).get( @@ -139,7 +139,7 @@ def _get(self, item): @property def response_data(self): """ - Return the raw data retrieved from NDFC + Return the raw data retrieved from the controller """ return self.properties["response_data"] @@ -615,8 +615,8 @@ def vpc_role(self): class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes retrieved by ip address. + Retrieve switch issu details from the controller and provide + property accessors for the switch attributes retrieved by ip address. Usage (where module is an instance of AnsibleModule): @@ -643,7 +643,7 @@ def refresh(self): """ Caller: __init__() - Refresh ip_address current issu details from NDFC + Refresh ip_address current issu details from the controller """ super().refresh() self.data_subclass = {} @@ -654,12 +654,16 @@ def refresh(self): def _get(self, item): if self.ip_address is None: - msg = f"{self.class_name}: set instance.ip_address " + msg = f"{self.class_name}._get: set instance.ip_address " msg += f"before accessing property {item}." self.module.fail_json(msg) if self.data_subclass.get(self.ip_address) is None: - msg = f"{self.class_name}: {self.ip_address} is not " - msg += f"defined in NDFC." + msg = f"{self.class_name}._get: {self.ip_address} does not " + msg += f"exist on the controller." + self.module.fail_json(msg) + if self.data_subclass[self.ip_address].get(item) is None: + msg = f"{self.class_name}._get: {self.ip_address} unknown " + msg += f"property name: {item}." self.module.fail_json(msg) return self.make_none(self.data_subclass[self.ip_address].get(item)) @@ -667,7 +671,7 @@ def _get(self, item): def filtered_data(self): """ Return a dictionary of the switch matching self.ip_address. - Return None of the switch does not exist in NDFC. + Return None if the switch does not exist on the controller. """ return self.data_subclass.get(self.ip_address) @@ -726,12 +730,16 @@ def refresh(self): def _get(self, item): if self.serial_number is None: - msg = f"{self.class_name}: set instance.serial_number " + msg = f"{self.class_name}._get: set instance.serial_number " msg += f"before accessing property {item}." self.module.fail_json(msg) if self.data_subclass.get(self.serial_number) is None: - msg = f"{self.class_name}: {self.serial_number} is not " - msg += f"defined in NDFC." + msg = f"{self.class_name}._get: {self.serial_number} does not " + msg += f"exist on the controller." + self.module.fail_json(msg) + if self.data_subclass[self.serial_number].get(item) is None: + msg = f"{self.class_name}._get: {self.serial_number} unknown " + msg += f"property name: {item}." self.module.fail_json(msg) return self.make_none(self.data_subclass[self.serial_number].get(item)) @@ -797,12 +805,16 @@ def refresh(self): def _get(self, item): if self.device_name is None: - msg = f"{self.class_name}: set instance.device_name " + msg = f"{self.class_name}._get: set instance.device_name " msg += f"before accessing property {item}." self.module.fail_json(msg) if self.data_subclass.get(self.device_name) is None: - msg = f"{self.class_name}: {self.device_name} is not " - msg += f"defined in NDFC." + msg = f"{self.class_name}._get: {self.device_name} does not " + msg += f"exist on the controller." + self.module.fail_json(msg) + if self.data_subclass[self.device_name].get(item) is None: + msg = f"{self.class_name}._get: {self.device_name} unknown " + msg += f"property name: {item}." self.module.fail_json(msg) return self.make_none(self.data_subclass[self.device_name].get(item)) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 38090a1eb..635dd2879 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -28,16 +28,36 @@ import json from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import ImagePolicyAction -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import ImageStage -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import ImageUpgrade -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import ImageValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import ImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import SwitchDetails -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsByIpAddress +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ( + ApiEndpoints, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import ( + ImagePolicies, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import ( + ImagePolicyAction, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import ( + ImageStage, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ( + ImageUpgradeCommon, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import ( + ImageUpgrade, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import ( + ImageValidate, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import ( + ImageInstallOptions, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import ( + SwitchDetails, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import ( + SwitchIssuDetailsByIpAddress, +) from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, validate_list_of_dicts, @@ -405,6 +425,7 @@ """ + class ImageUpgradeTask(ImageUpgradeCommon): """ Ansible support for image policy attach, detach, and query. @@ -482,7 +503,9 @@ def get_want(self): self._validate_switch_configs() if not self.switch_configs: return - self.log_msg(f"{self.class_name}.get_want() self.switch_configs: {self.switch_configs}") + self.log_msg( + f"{self.class_name}.get_want() self.switch_configs: {self.switch_configs}" + ) self.want_create = self.switch_configs def _get_idempotent_want(self, want): @@ -491,7 +514,7 @@ def _get_idempotent_want(self, want): The have item is obtained from an instance of SwitchIssuDetails created in self.get_have(). - + want structure passed to this method: { @@ -518,7 +541,7 @@ def _get_idempotent_want(self, want): The returned idempotent_want structure is identical to the above structure, except that the policy_changed key is added, and values are modified based on results from the have item, - and the information returned by ImageInstallOptions. + and the information returned by ImageInstallOptions. Caller: self.get_need_merged() """ @@ -526,16 +549,20 @@ def _get_idempotent_want(self, want): self.have.ip_address = want["ip_address"] want["policy_changed"] = True - # In NDFC, the switch does not have an image policy attached + # The switch does not have an image policy attached. # Return the want item as-is with policy_changed = True if self.have.serial_number is None: - self.log_msg(f"{self.class_name}._get_idempotent_want() no serial_number. returning want: {want}") + self.log_msg( + f"{self.class_name}._get_idempotent_want() no serial_number. returning want: {want}" + ) return want # The switch has an image policy attached which is # different from the want policy. # Return the want item as-is with policy_changed = True if want["policy"] != self.have.policy: - self.log_msg(f"{self.class_name}._get_idempotent_want() different policy attached. returning want: {want}") + self.log_msg( + f"{self.class_name}._get_idempotent_want() different policy attached. returning want: {want}" + ) return want # start with a copy of the want item @@ -552,7 +579,11 @@ def _get_idempotent_want(self, want): if self.have.validated == "Success": idempotent_want["validate"] = False # if the image is already upgraded, don't upgrade it again - if self.have.status == "In-Sync" and self.have.reason == "Upgrade" and self.have.policy == want["policy"]: + if ( + self.have.status == "In-Sync" + and self.have.reason == "Upgrade" + and self.have.policy == want["policy"] + ): idempotent_want["upgrade"]["nxos"] = False # Get relevant install options from NDFC based on the @@ -572,7 +603,9 @@ def _get_idempotent_want(self, want): if instance.epld_modules is None: idempotent_want["upgrade"]["epld"] = False - self.log_msg(f"REMOVE: {self.class_name}._get_idempotent_want() returned idempotent_want: {idempotent_want}") + self.log_msg( + f"REMOVE: {self.class_name}._get_idempotent_want() returned idempotent_want: {idempotent_want}" + ) return idempotent_want def get_need_merged(self): @@ -689,14 +722,14 @@ def _build_params_spec_for_merged_state(): params_spec[section][sub_section]["default"] = {} params_spec[section][sub_section]["module"] = {} - params_spec[section][sub_section]["module"] ["required"] = False - params_spec[section][sub_section]["module"] ["type"] = "str" - params_spec[section][sub_section]["module"] ["default"] = "ALL" + params_spec[section][sub_section]["module"]["required"] = False + params_spec[section][sub_section]["module"]["type"] = "str" + params_spec[section][sub_section]["module"]["default"] = "ALL" params_spec[section][sub_section]["golden"] = {} - params_spec[section][sub_section]["golden"] ["required"] = False - params_spec[section][sub_section]["golden"] ["type"] = "bool" - params_spec[section][sub_section]["golden"] ["default"] = False + params_spec[section][sub_section]["golden"]["required"] = False + params_spec[section][sub_section]["golden"]["type"] = "bool" + params_spec[section][sub_section]["golden"]["default"] = False sub_section = "reboot" params_spec[section][sub_section] = {} @@ -705,14 +738,14 @@ def _build_params_spec_for_merged_state(): params_spec[section][sub_section]["default"] = {} params_spec[section][sub_section]["config_reload"] = {} - params_spec[section][sub_section]["config_reload"] ["required"] = False - params_spec[section][sub_section]["config_reload"] ["type"] = "bool" - params_spec[section][sub_section]["config_reload"] ["default"] = False + params_spec[section][sub_section]["config_reload"]["required"] = False + params_spec[section][sub_section]["config_reload"]["type"] = "bool" + params_spec[section][sub_section]["config_reload"]["default"] = False params_spec[section][sub_section]["write_erase"] = {} - params_spec[section][sub_section]["write_erase"] ["required"] = False - params_spec[section][sub_section]["write_erase"] ["type"] = "bool" - params_spec[section][sub_section]["write_erase"] ["default"] = False + params_spec[section][sub_section]["write_erase"]["required"] = False + params_spec[section][sub_section]["write_erase"]["type"] = "bool" + params_spec[section][sub_section]["write_erase"]["default"] = False sub_section = "package" params_spec[section][sub_section] = {} @@ -721,14 +754,14 @@ def _build_params_spec_for_merged_state(): params_spec[section][sub_section]["default"] = {} params_spec[section][sub_section]["install"] = {} - params_spec[section][sub_section]["install"] ["required"] = False - params_spec[section][sub_section]["install"] ["type"] = "bool" - params_spec[section][sub_section]["install"] ["default"] = False + params_spec[section][sub_section]["install"]["required"] = False + params_spec[section][sub_section]["install"]["type"] = "bool" + params_spec[section][sub_section]["install"]["default"] = False params_spec[section][sub_section]["uninstall"] = {} - params_spec[section][sub_section]["uninstall"] ["required"] = False - params_spec[section][sub_section]["uninstall"] ["type"] = "bool" - params_spec[section][sub_section]["uninstall"] ["default"] = False + params_spec[section][sub_section]["uninstall"]["required"] = False + params_spec[section][sub_section]["uninstall"]["type"] = "bool" + params_spec[section][sub_section]["uninstall"]["default"] = False return copy.deepcopy(params_spec) @@ -746,7 +779,9 @@ def validate_input(self): self.module.fail_json(msg) if state == "merged": - self.log_msg(f"{self.class_name}.validate_input: call _validate_input_for_merged_state()") + self.log_msg( + f"{self.class_name}.validate_input: call _validate_input_for_merged_state()" + ) self._validate_input_for_merged_state() return if state == "deleted": @@ -768,8 +803,12 @@ def _validate_input_for_merged_state(self): params_spec = self._build_params_spec_for_merged_state() - self.log_msg(f"{self.class_name}._validate_input_for_merged_state: params_spec: {params_spec}") - self.log_msg(f"{self.class_name}._validate_input_for_merged_state: self.config: {self.config}") + self.log_msg( + f"{self.class_name}._validate_input_for_merged_state: params_spec: {params_spec}" + ) + self.log_msg( + f"{self.class_name}._validate_input_for_merged_state: self.config: {self.config}" + ) valid_params, invalid_params = validate_list_of_dicts( self.config.get("switches"), params_spec, self.module ) @@ -1012,7 +1051,7 @@ def _verify_install_options(self, devices): 'policy': 'KR3F', 'stage': False, 'upgrade': { - 'nxos': True, + 'nxos': True, 'epld': False }, 'options': { @@ -1040,20 +1079,28 @@ def _verify_install_options(self, devices): # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests self.switch_details.refresh() for device in devices: - self.log_msg(f"REMOVE: {self.class_name}._verify_install_options: device: {device}") + self.log_msg( + f"REMOVE: {self.class_name}._verify_install_options: device: {device}" + ) self.switch_details.ip_address = device.get("ip_address") install_options.serial_number = self.switch_details.serial_number install_options.policy_name = device["policy"] install_options.epld = device["upgrade"]["epld"] install_options.issu = device["upgrade"]["nxos"] install_options.refresh() - if install_options.status not in ["Success", "Skipped"] and device["upgrade"]["nxos"] is True: + if ( + install_options.status not in ["Success", "Skipped"] + and device["upgrade"]["nxos"] is True + ): msg = f"NXOS upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " msg += f"NX-OS image" self.module.fail_json(msg) - if install_options.epld_modules is None and device["upgrade"]["epld"] is True: + if ( + install_options.epld_modules is None + and device["upgrade"]["epld"] is True + ): msg = f"EPLD upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " @@ -1124,8 +1171,9 @@ def handle_merged_state(self): if switch.get("validate") is not False: validate_devices.append(device["serial_number"]) if ( - switch.get("upgrade").get("nxos") is not False or - switch.get("upgrade").get("epld") is not False): + switch.get("upgrade").get("nxos") is not False + or switch.get("upgrade").get("epld") is not False + ): upgrade_devices.append(switch) msg = f"REMOVE: {self.class_name}.handle_merged_state: stage_devices: {stage_devices}" @@ -1208,7 +1256,6 @@ def handle_query_state(self): self.result["diff"] = [] self.result["changed"] = False - def _failure(self, resp): """ Caller: self.attach_policies() @@ -1241,9 +1288,6 @@ def _failure(self, resp): self.module.fail_json(msg=res) - - - def main(): """main entry point for module execution""" diff --git a/plugins/modules/dcnm_image_upgrade_orig.py b/plugins/modules/dcnm_image_upgrade_orig.py index b6d05b1d9..eaf1f912b 100644 --- a/plugins/modules/dcnm_image_upgrade_orig.py +++ b/plugins/modules/dcnm_image_upgrade_orig.py @@ -1558,7 +1558,7 @@ def refresh(self): self.log_msg(msg) if self.response["RETURN_CODE"] != 200: - msg = "Unable to retrieve switch information from NDFC. " + msg = "Unable to retrieve switch information from the controller. " msg += f"Got response {self.response}" self.module.fail_json(msg) @@ -2467,14 +2467,14 @@ def refresh(self): if not self.result["success"]: msg = f"{self.class_name}.refresh: " msg += "Bad result when retriving image policy " - msg += "information from NDFC." + msg += "information from the controller." self.module.fail_json(msg) data = self.response.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.refresh: " msg += "Bad response when retrieving image policy " - msg += "information from NDFC." + msg += "information from the controller." self.module.fail_json(msg) if len(data) == 0: msg = f"{self.class_name}.refresh: " @@ -2745,7 +2745,7 @@ def refresh(self) -> None: if self.result["success"] == False or self.result["found"] == False: msg = f"{self.class_name}.refresh: " msg += "Bad result when retriving switch " - msg += "information from NDFC" + msg += "information from the controller" self.module.fail_json(msg) data = self.response.get("DATA").get("lastOperDataObject") @@ -5005,7 +5005,7 @@ def serial_numbers(self): class ControllerVersion(ImageUpgradeCommon): """ - Return image version information from NDFC + Return image version information from the controller NOTES: 1. considered using dcnm_version_supported() but it does not return diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py index 15dc580de..3bd7625b5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py @@ -17,11 +17,12 @@ __metaclass__ = type -import os import json +import os fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") + def load_fixture(filename): path = os.path.join(fixture_path, "{0}.json".format(filename)) @@ -34,5 +35,3 @@ def load_fixture(filename): print(f"Exception loading fixture {filename}. Exception detail: {exception}") return fixture - - diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py index aec3a01c5..a36ac8923 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py @@ -1,13 +1,10 @@ -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ -# ApiEndpoints - -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints - """ controller_version: 12 description: Verify that class ApiEndpoints returns the correct API endpoints """ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints def test_dcnm_image_upgrade_endpoints_init() -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py index 18dcde11e..70392223a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py @@ -8,7 +8,8 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import ControllerVersion +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ + ControllerVersion from .fixture import load_fixture @@ -17,6 +18,7 @@ dcnm_send_version = patch_common + "controller_version.dcnm_send" + def responses_controller_version(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_ControllerVersion" response = load_fixture(response_file).get(key) @@ -372,7 +374,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( - "key, expected", [("ControllerVersion_mode_LAN", "LAN"), ("ControllerVersion_mode_none", None)] + "key, expected", + [("ControllerVersion_mode_LAN", "LAN"), ("ControllerVersion_mode_none", None)], ) def test_mode(monkeypatch, module, key, expected) -> None: """ @@ -404,7 +407,10 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", - [("ControllerVersion_uuid_UUID", "foo-uuid"), ("ControllerVersion_uuid_none", None)], + [ + ("ControllerVersion_uuid_UUID", "foo-uuid"), + ("ControllerVersion_uuid_none", None), + ], ) def test_uuid(monkeypatch, module, key, expected) -> None: """ @@ -436,7 +442,10 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", - [("ControllerVersion_version_12.1.3b", "12.1.3b"), ("ControllerVersion_version_none", None)], + [ + ("ControllerVersion_version_12.1.3b", "12.1.3b"), + ("ControllerVersion_version_none", None), + ], ) def test_version(monkeypatch, module, key, expected) -> None: """ @@ -468,7 +477,10 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", - [("ControllerVersion_version_12.1.3b", "12"), ("ControllerVersion_version_none", None)], + [ + ("ControllerVersion_version_12.1.3b", "12"), + ("ControllerVersion_version_none", None), + ], ) def test_version_major(monkeypatch, module, key, expected) -> None: """ @@ -504,7 +516,10 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", - [("ControllerVersion_version_12.1.3b", "1"), ("ControllerVersion_version_none", None)], + [ + ("ControllerVersion_version_12.1.3b", "1"), + ("ControllerVersion_version_none", None), + ], ) def test_version_minor(monkeypatch, module, key, expected) -> None: """ @@ -540,7 +555,10 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", - [("ControllerVersion_version_12.1.3b", "3b"), ("ControllerVersion_version_none", None)], + [ + ("ControllerVersion_version_12.1.3b", "3b"), + ("ControllerVersion_version_none", None), + ], ) def test_version_patch(monkeypatch, module, key, expected) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index 6a7938bb5..e3bf66a5f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -1,22 +1,25 @@ +""" +controller_version: 12 +description: Verify functionality of class ImageInstallOptions +""" + from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import ImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ + ImageInstallOptions from .fixture import load_fixture -""" -controller_version: 12 -description: Verify functionality of class ImageInstallOptions -""" patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_image_mgmt = patch_module_utils + "image_mgmt." dcnm_send_install_options = patch_image_mgmt + "install_options.dcnm_send" + class MockAnsibleModule: params = {} @@ -104,7 +107,8 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: module.policy_name = "KRM5" module.serial_number = "BAR" error_message = "ImageInstallOptions.refresh: " - error_message += "Bad result when retrieving install-options from NDFC" + error_message += "Bad result when retrieving install-options from " + error_message += "the controller. Controller response:" with pytest.raises(AnsibleFailJson, match=rf"{error_message}"): module.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py index 53cff00a1..4fd0ce008 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py @@ -1,28 +1,25 @@ +""" +controller_version: 12 +description: Verify functionality of class ImagePolicies +""" + from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# ImagePolicies -# ) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ + ImagePolicies from .fixture import load_fixture -""" -controller_version: 12 -description: Verify functionality of class ImagePolicies -""" - -#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -#dcnm_send_patch = "ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies.dcnm_send" patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_image_mgmt = patch_module_utils + "image_mgmt." dcnm_send_image_policies = patch_image_mgmt + "image_policies.dcnm_send" + class MockAnsibleModule: params = {} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index ae03b994a..2415f1e02 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -9,9 +9,10 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson - -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import ImagePolicyAction -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ + ImagePolicyAction +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsBySerialNumber from .fixture import load_fixture @@ -22,10 +23,11 @@ def does_not_raise(): patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_image_mgmt = patch_module_utils + "image_mgmt." dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" + def responses_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -141,7 +143,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: ] error_message = "Unable to determine hostName for switch " error_message += "172.22.150.108, FDO2112189M, None. " - error_message += "Please verify that the switch is managed by NDFC." + error_message += "Please verify that the switch is managed by " + error_message += "the controller." with pytest.raises(AnsibleFailJson, match=error_message): module.build_attach_payload() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py index acf3f78b3..7b84ede7a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py @@ -1,7 +1,6 @@ """ controller_version: 12 description: Verify functionality of ImageStage -TODO:2 ImageStage.commit unit test """ from contextlib import contextmanager @@ -10,9 +9,12 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import ImageStage -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ + ImageStage +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsBySerialNumber from .fixture import load_fixture @@ -21,14 +23,16 @@ def does_not_raise(): yield + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_image_mgmt = patch_module_utils + "image_mgmt." patch_common = patch_module_utils + "common." dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" + def responses_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -197,7 +201,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] error_message = "Image staging is failing for the following switch: " - error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M." + error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. " + error_message += "Check the switch connectivity to the controller " + error_message += "and try again." with pytest.raises(AnsibleFailJson, match=error_message): module.validate_serial_numbers() @@ -358,6 +364,7 @@ def test_wait_for_image_stage_to_complete( 3. module.serial_numbers_done should contain all serial numbers module.serial_numbers 4. The module should return without calling fail_json. """ + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "ImageStage_test_wait_for_image_stage_to_complete" return responses_issu_details(key) @@ -395,6 +402,7 @@ def test_wait_for_image_stage_to_complete_stage_failed( 3. module.serial_numbers_done contains FDO21120U5D, imageStaged is "Success" 4. Call fail_json on serial number FDO2112189M, imageStaged is "Failed" """ + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "ImageStage_test_wait_for_image_stage_to_complete_fail_json" return responses_issu_details(key) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 995d47cf6..07bb44c7c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -8,10 +8,8 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# ImageUpgrade, SwitchDetails, ControllerVersion) - -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import ImageUpgrade +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ + ImageUpgrade from .fixture import load_fixture @@ -21,11 +19,8 @@ def does_not_raise(): yield -# dcnm_send_patch = ( -# "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade.dcnm_send" -# ) patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +patch_image_mgmt = patch_module_utils + "image_mgmt." patch_common = patch_module_utils + "common." dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" @@ -58,6 +53,7 @@ def test_init(module) -> None: assert module.class_name == "ImageUpgrade" assert module.max_module_number == 9 + def test_init_defaults(module) -> None: """ Defaults are initialized to expected values @@ -78,6 +74,7 @@ def test_init_defaults(module) -> None: assert module.defaults["options"]["package"]["install"] == False assert module.defaults["options"]["package"]["uninstall"] == False + def test_init_properties(module) -> None: """ Properties are initialized to expected values @@ -103,8 +100,23 @@ def test_init_properties(module) -> None: assert module.properties.get("package_uninstall") == False assert module.properties.get("reboot") == False assert module.properties.get("write_erase") == False - assert module.valid_epld_module == {"ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9"} - assert module.valid_nxos_mode == {"disruptive", "non_disruptive", "force_non_disruptive"} + assert module.valid_epld_module == { + "ALL", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + } + assert module.valid_nxos_mode == { + "disruptive", + "non_disruptive", + "force_non_disruptive", + } # def test_ip_address_not_set(module) -> None: @@ -200,7 +212,7 @@ def test_init_properties(module) -> None: # assert module.serial_number == "FOX2109PGD1" -# match = "Unable to retrieve switch information from NDFC. " +# match = "Unable to retrieve switch information from the controller. " # @pytest.mark.parametrize( diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py index 3a6d43dca..be5f2b238 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py @@ -1,19 +1,18 @@ +""" +controller_version: 12 +description: Verify functionality of class ImageUpgradeCommon +""" + from typing import Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# ImageUpgradeCommon, ApiEndpoints) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon from .fixture import load_fixture -""" -controller_version: 12 -description: Verify functionality of class ImageUpgradeCommon -""" class MockAnsibleModule: @@ -25,9 +24,9 @@ def fail_json(msg) -> dict: @pytest.fixture def module(): - # return ImageUpgradeCommon(MockAnsibleModule) return ImageUpgradeCommon(MockAnsibleModule) + def responses_image_upgrade_common(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_ImageUpgradeCommon" response = load_fixture(response_file).get(key) @@ -44,9 +43,7 @@ def test_init_(module) -> None: assert module.params == {} assert module.debug == True assert module.fd == None - # assert module.logfile == "/tmp/dcnm_image_upgrade.log" assert module.logfile == "/tmp/ndfc.log" - # assert isinstance(module.endpoints, ApiEndpoints) @pytest.mark.parametrize( @@ -92,8 +89,6 @@ def test_handle_response_get(module, key, expected) -> None: """ data = responses_image_upgrade_common(key) result = module._handle_response(data.get("response"), data.get("verb")) - # TODO: We could assert on the dictionary, with a less granular error message - # assert result == expected assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") @@ -185,19 +180,6 @@ def test_make_boolean(module, key, expected) -> None: assert module.make_boolean(key) == expected -# def test_dcnm_image_upgrade_common_make_boolean(module) -> None: -# """ -# NOTE: The above parameterized testcase results in 7% greater coverage according to pytest-cov versus for loops (below) -# verify that make_boolean() returns expected values for all cases -# """ -# for value in ["True", "true", "TRUE", True]: -# assert module.make_boolean(value) == True -# for value in ["False", "false", "FALSE", False]: -# assert module.make_boolean(value) == False -# for value in ["foo", 1, 0, None, {"foo": 10}, [1, 2, "3"]]: -# assert module.make_boolean(value) == value - - @pytest.mark.parametrize( "key, expected", [ @@ -225,17 +207,6 @@ def test_make_none(module, key, expected) -> None: assert module.make_none(key) == expected -# def test_dcnm_image_upgrade_common_make_none(module) -> None: -# """ -# NOTE: The above parameterized testcase results in 7% greater coverage according to pytest-cov versus for loops (below) -# verify that make_none() returns expected values for all cases -# """ -# for value in ["", "none", "None", "NONE", "null", "Null", "NULL", None]: -# assert module.make_none(value) == None -# for value in ["foo", 1, 0, True, False, {"foo": 10}, [1, 2, "3"]]: -# assert module.make_none(value) == value - - def test_log_msg_disabled(module) -> None: """ verify that make_none() returns expected values for all cases diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index f85feb950..9cd41c56d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -8,20 +8,17 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import ( -# ImageValidate, SwitchIssuDetailsBySerialNumber, ControllerVersion) - -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import ImageValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ + ImageValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsBySerialNumber from .fixture import load_fixture + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." -patch_common = patch_module_utils + "common." +patch_image_mgmt = patch_module_utils + "image_mgmt." -dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" -dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" @@ -126,8 +123,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: error_message = "ImageValidate.validate_serial_numbers: " error_message += "image validation is failing for the following switch: " error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. If this " - error_message += "persists, check the switch connectivity to NDFC and " - error_message += "try again." + error_message += "persists, check the switch connectivity to the " + error_message += "controller and try again." with pytest.raises(AnsibleFailJson, match=error_message): module.validate_serial_numbers() @@ -200,8 +197,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M, " error_message += "image validated percent: 100. Check the switch e.g. " error_message += "show install log detail, show incompatibility-all nxos " - error_message += ". Or check NDFC Operations > Image Management > " - error_message += "Devices > View Details > Validate for more details." + error_message += ". Or check Operations > Image Management > " + error_message += "Devices > View Details > Validate on the controller " + error_message += "GUI for more details." with pytest.raises(AnsibleFailJson, match=error_message): module._wait_for_image_validate_to_complete() assert isinstance(module.serial_numbers_done, set) From 0f8698ee0515ff4bfc85651b2e38adcd36affeac Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 1 Nov 2023 15:05:08 -1000 Subject: [PATCH 019/300] run thru black and isort --- .../dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py | 1 + .../test_image_upgrade_ImageInstallOptions.py | 1 - .../dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py | 1 - .../test_image_upgrade_ImageUpgradeCommon.py | 1 - .../dcnm_image_upgrade/test_image_upgrade_ImageValidate.py | 1 - .../dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py | 2 ++ .../test_image_upgrade_SwitchIssuDetailsByDeviceName.py | 2 ++ .../test_image_upgrade_SwitchIssuDetailsByIpAddress.py | 3 ++- .../test_image_upgrade_SwitchIssuDetailsBySerialNumber.py | 3 +++ 9 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py index a36ac8923..f36827006 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py @@ -6,6 +6,7 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints + def test_dcnm_image_upgrade_endpoints_init() -> None: """ Endpoints.__init__ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index e3bf66a5f..2bdcd3143 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -13,7 +13,6 @@ from .fixture import load_fixture - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py index 4fd0ce008..0eb17c4a2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py @@ -13,7 +13,6 @@ from .fixture import load_fixture - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py index be5f2b238..e8e1ac419 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py @@ -14,7 +14,6 @@ from .fixture import load_fixture - class MockAnsibleModule: params = {} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index 9cd41c56d..63c7e04ba 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -15,7 +15,6 @@ from .fixture import load_fixture - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py index dec29c405..7afe8db09 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py @@ -251,6 +251,7 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: module.ip_address = "172.22.150.110" assert module._get(item) == expected + def test_get_with_unknown_ip_address(monkeypatch, module) -> None: """ Function description: @@ -280,6 +281,7 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: with pytest.raises(AnsibleFailJson, match=match): module._get("hostName") + def test_get_with_unknown_property_name(monkeypatch, module) -> None: """ Function description: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py index aae571c70..dd4dba1a7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py @@ -212,6 +212,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() + def test_get_with_unknown_device_name(monkeypatch, module) -> None: """ Function description: @@ -241,6 +242,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: with pytest.raises(AnsibleFailJson, match=match): module._get("serialNumber") + def test_get_with_unknown_property_name(monkeypatch, module) -> None: """ Function description: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py index cd29cd774..71da71580 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py @@ -13,7 +13,6 @@ from .fixture import load_fixture - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." @@ -214,6 +213,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() + def test_get_with_unknown_ip_address(monkeypatch, module) -> None: """ Function description: @@ -243,6 +243,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: with pytest.raises(AnsibleFailJson, match=match): module._get("serialNumber") + def test_get_with_unknown_property_name(monkeypatch, module) -> None: """ Function description: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py index 8ffd90971..f6547d7e9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py @@ -17,6 +17,7 @@ dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" + class MockAnsibleModule: params = {} @@ -211,6 +212,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: with pytest.raises(AnsibleFailJson, match=error_message): module.refresh() + def test_get_with_unknown_serial_number(monkeypatch, module) -> None: """ Function description: @@ -240,6 +242,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: with pytest.raises(AnsibleFailJson, match=match): module._get("serialNumber") + def test_get_with_unknown_property_name(monkeypatch, module) -> None: """ Function description: From df8f2e597dbe5482d2f2bd79f5a9613e866291fb Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 1 Nov 2023 17:08:11 -1000 Subject: [PATCH 020/300] ImageUpgrade - unit tests for validate_devices --- ...e_upgrade_responses_SwitchIssuDetails.json | 180 ++++++++++++ .../test_image_upgrade_ImageUpgrade.py | 262 +++++------------- 2 files changed, 250 insertions(+), 192 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 3531636a1..19d840c14 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -2596,5 +2596,185 @@ ], "message": "" } + }, + "ImageUpgrade_test_validate_devices_success": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Failed", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "ImageUpgrade_test_validate_devices_failed": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Failed", + "validated": "Success", + "upgrade": "Failed", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 07bb44c7c..69a65de20 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -21,17 +21,13 @@ def does_not_raise(): patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." -patch_common = patch_module_utils + "common." -dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" -dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" - -def responses_switch_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchDetails" +def responses_issu_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) - print(f"responses_switch_details: {key} : {response}") + print(f"response_data_issu_details: {key} : {response}") return response @@ -118,196 +114,78 @@ def test_init_properties(module) -> None: "force_non_disruptive", } +def test_validate_devices_success(monkeypatch, module) -> None: + """ + Function description: -# def test_ip_address_not_set(module) -> None: -# """ -# Function description: - -# SwitchDetails.ip_address returns: -# - IP Address, if the user has set ip_address -# - None, if the user has not already set ip_address - -# Expected results: - -# 1. instance.ip_address will return None -# """ -# assert module.ip_address == None - - -# def test_ip_address_is_set(module) -> None: -# """ -# Function description: - -# SwitchDetails.ip_address returns: -# - IP Address, if the user has set ip_address -# - None, if the user has not already set ip_address - -# Expected results: - -# 1. instance.ip_address will return the value set by the user -# """ -# module.ip_address = "1.2.3.4" -# assert module.ip_address == "1.2.3.4" - - -# def test_refresh(monkeypatch, module) -> None: -# """ -# Function description: - -# SwitchDetails.refresh sets the following properties: -# - response_data -# - response -# - result - -# Expected results: - -# 1. instance.response_data is a dictionary -# 2. instance.response is a dictionary -# 3. instance.response_data is a list -# """ - -# def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: -# key = "SwitchDetails_get_return_code_200" -# return responses_switch_details(key) - -# monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - -# module.refresh() -# assert isinstance(module.response_data, dict) -# assert isinstance(module.result, dict) -# assert isinstance(module.response, dict) - - -# def test_refresh_response_data(monkeypatch, module) -> None: -# """ -# Function description: - -# See test_refresh - -# Expected results: - -# 1. instance.response_data is a dictionary -# 2. When instance.ip_address is set, getter properties will return values specific to ip_address -# """ - -# def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: -# key = "SwitchDetails_get_return_code_200" -# return responses_switch_details(key) - -# monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - -# module.refresh() -# assert isinstance(module.response_data, dict) -# module.ip_address = "172.22.150.110" -# assert module.hostname == "cvd-1111-bgw" -# module.ip_address = "172.22.150.111" -# # We use the above IP address to test the remaining properties -# assert module.fabric_name == "easy" -# assert module.hostname == "cvd-1112-bgw" -# assert module.logical_name == "cvd-1112-bgw" -# assert module.model == "N9K-C9504" -# # This is derived from "model" and is not in the NDFC response -# assert module.platform == "N9K" -# assert module.role == "border gateway" -# assert module.serial_number == "FOX2109PGD1" - + ImageUpgrade.validate_devices updates the set ImageUpgrade.ip_addresses + with the ip addresses of the devices that have issu_detail.upgrade is + not "Failed" -# match = "Unable to retrieve switch information from the controller. " + Expected results: + 1. instance.ip_addresses will contain {"172.22.150.102", "172.22.150.108"} + 2. fail_json will not be called + """ -# @pytest.mark.parametrize( -# "key,expected", -# [ -# ("SwitchDetails_get_return_code_200", does_not_raise()), -# ( -# "SwitchDetails_get_return_code_404", -# pytest.raises(AnsibleFailJson, match=match), -# ), -# ( -# "SwitchDetails_get_return_code_500", -# pytest.raises(AnsibleFailJson, match=match), -# ), -# ], -# ) -# def test_result(monkeypatch, module, key, expected) -> None: -# """ -# Function description: + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageUpgrade_test_validate_devices_success" + return responses_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + devices = [ + { + "ip_address": "172.22.150.102" + }, + { + "ip_address": "172.22.150.108" + } + ] + + module.devices = devices + module.validate_devices() + assert isinstance(module.ip_addresses, set) + assert len(module.ip_addresses) == 2 + assert "172.22.150.102" in module.ip_addresses + assert "172.22.150.108" in module.ip_addresses + +def test_validate_devices_failed(monkeypatch, module) -> None: + """ + Function description: -# SwitchDetails.result returns the result of its superclass -# method ImageUpgradeCommon._handle_response() - -# Expectations: + ImageUpgrade.validate_devices updates the set ImageUpgrade.ip_addresses + with the ip addresses of the devices that have issu_detail.upgrade is + not "Failed" -# 1. 200 RETURN_CODE, MESSAGE == "OK", -# SwitchDetails.result == {'found': True, 'success': True} + Expected results: -# 2. 404 RETURN_CODE, MESSAGE == "Not Found", -# SwitchDetails.result == {'found': False, 'success': True} - -# 3. 500 RETURN_CODE, MESSAGE ~= "Internal Server Error", -# SwitchDetails.result == {'found': False, 'success': False} - -# Expected results: + 1. instance.ip_addresses will contain {"172.22.150.102"} + 2. fail_json will be called + """ -# 1. SwitchDetails_result_200 == {'found': True, 'success': True} -# 2. SwitchDetails_result_404 == {'found': False, 'success': True} -# 3. SwitchDetails_result_500 == {'found': False, 'success': False} -# """ - -# def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: -# return responses_switch_details(key) - -# monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - -# with expected: -# module.refresh() - - -# @pytest.mark.parametrize( -# "item, expected", -# [ -# ("fabricName", "easy"), -# ("hostName", "cvd-1111-bgw"), -# ("licenseViolation", False), -# ("location", None), -# ("logicalName", "cvd-1111-bgw"), -# ("managable", True), -# ("model", "N9K-C9504"), -# ("present", True), -# ("serialNumber", "FOX2109PGCT"), -# ("switchRole", "border gateway"), -# ], -# ) -# def test_get_with_ip_address_set(monkeypatch, module, item, expected) -> None: -# """ -# Function description: - -# SwitchDetails._get is called by all getter properties. -# It raises AnsibleFailJson if the user has not set ip_address. -# It returns the value of the requested property if the user has set ip_address. -# The property value is passed to both make_boolean() and make_none(), which -# either: -# - converts it to a boolean -# - converts it to NoneType -# - returns the value unchanged - -# Expectations: - -# 1. ControllerVersion._get returns above values -# given corresponding responses - -# Expected results: - -# 1. ControllerVersion_mode_LAN == "LAN" -# 2. ControllerVersion_mode_none == None -# """ - -# def mock_dcnm_send(*args, **kwargs) -> Dict[str, Any]: -# key = "SwitchDetails_get_return_code_200" -# return responses_switch_details(key) - -# monkeypatch.setattr(dcnm_send_patch, mock_dcnm_send) - -# module.refresh() -# module.ip_address = "172.22.150.110" -# assert module._get(item) == expected + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImageUpgrade_test_validate_devices_failed" + return responses_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + devices = [ + { + "ip_address": "172.22.150.102" + }, + { + "ip_address": "172.22.150.108" + } + ] + + match = "ImageUpgrade.validate_devices: Image upgrade is failing for the " + match += "following switch: cvd-2313-leaf, 172.22.150.108, FDO2112189M. " + match += "Please check the switch to determine the cause and try again." + module.devices = devices + with pytest.raises(AnsibleFailJson, match=match): + module.validate_devices() + assert isinstance(module.ip_addresses, set) + assert len(module.ip_addresses) == 1 + assert "172.22.150.102" in module.ip_addresses + assert "172.22.150.108" not in module.ip_addresses From 86820162486facd2f381a6e5e8dddf19dbab1953 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 1 Nov 2023 17:15:50 -1000 Subject: [PATCH 021/300] Remove unused import --- plugins/module_utils/common/controller_version.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index bde9aaf50..077745251 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -1,4 +1,3 @@ -from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) From 1be545dda852e35ba0b2757803f00563be4f6607 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 07:35:41 -1000 Subject: [PATCH 022/300] cleanup log and fail_json messages --- plugins/modules/dcnm_image_upgrade.py | 257 +++++++++++++++++--------- 1 file changed, 166 insertions(+), 91 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 635dd2879..4b2a2e692 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -433,17 +433,26 @@ class ImageUpgradeTask(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) + self.method_name = "__init__" self.params = self.module.params self.class_name = self.__class__.__name__ self.endpoints = ApiEndpoints() - self.log_msg(f"{self.class_name}.__init__") + + msg = f"REMOVE: {self.class_name}.{self.method_name}: entered" + self.log_msg(msg) + # populated in self._build_policy_attach_payload() self.payloads = [] self.config = module.params.get("config") - self.log_msg(f"{self.class_name}.__init__() self.config {self.config}") + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"self.config: {self.config}" + self.log_msg(msg) + if not isinstance(self.config, dict): - msg = "expected dict type for self.config. " + msg = f"{self.class_name}.{self.method_name}: " + msg += "expected dict type for self.config. " msg = +f"got {type(self.config).__name__}" self.module.fail_json(msg) @@ -460,28 +469,26 @@ def __init__(self, module): self.mandatory_switch_keys = {"ip_address"} if not self.mandatory_global_keys.issubset(self.config): - msg = f"{self.class_name}.__init__: " + msg = f"{self.class_name}.{self.method_name}: " msg += "Missing mandatory key(s) in playbook global config. " msg += f"expected {self.mandatory_global_keys}, " msg += f"got {self.config.keys()}" self.module.fail_json(msg) if self.config["switches"] is None: - msg = f"{self.class_name}.__init__: " + msg = f"{self.class_name}.{self.method_name}: " msg += "missing list of switches in playbook config." self.module.fail_json(msg) for switch in self.config["switches"]: if not self.mandatory_switch_keys.issubset(switch): - msg = f"{self.class_name}.__init__: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"missing mandatory key(s) in playbook switch config. " msg += f"expected {self.mandatory_switch_keys}, " msg += f"got {switch.keys()}" self.module.fail_json(msg) - self.log_msg(f"{self.class_name}.__init__: instantiate SwitchDetails") self.switch_details = SwitchDetails(self.module) - self.log_msg(f"{self.class_name}.__init__: instantiate ImagePolicies") self.image_policies = ImagePolicies(self.module) def get_have(self): @@ -490,6 +497,7 @@ def get_have(self): Determine current switch ISSU state on NDFC """ + self.method_name = "get_have" self.have = SwitchIssuDetailsByIpAddress(self.module) self.have.refresh() @@ -499,13 +507,16 @@ def get_want(self): Update self.want_create for all switches defined in the playbook """ + self.method_name = "get_want" self._merge_global_and_switch_configs(self.config) self._validate_switch_configs() if not self.switch_configs: return - self.log_msg( - f"{self.class_name}.get_want() self.switch_configs: {self.switch_configs}" - ) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"self.switch_configs: {self.switch_configs}" + self.log_msg(msg) + self.want_create = self.switch_configs def _get_idempotent_want(self, want): @@ -545,24 +556,30 @@ def _get_idempotent_want(self, want): Caller: self.get_need_merged() """ - self.log_msg(f"{self.class_name}._get_idempotent_want() want: {want}") + self.method_name = "_get_idempotent_want" + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"want: {want}" + self.log_msg(msg) + self.have.ip_address = want["ip_address"] want["policy_changed"] = True # The switch does not have an image policy attached. # Return the want item as-is with policy_changed = True if self.have.serial_number is None: - self.log_msg( - f"{self.class_name}._get_idempotent_want() no serial_number. returning want: {want}" - ) + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"no serial_number. return want: {want}" + self.log_msg(msg) return want + # The switch has an image policy attached which is # different from the want policy. # Return the want item as-is with policy_changed = True if want["policy"] != self.have.policy: - self.log_msg( - f"{self.class_name}._get_idempotent_want() different policy attached. returning want: {want}" - ) + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"different policy attached. return want: {want}" + self.log_msg(msg) return want # start with a copy of the want item @@ -590,12 +607,14 @@ def _get_idempotent_want(self, want): # options in our want item instance = ImageInstallOptions(self.module) instance.policy_name = want["policy"] - msg = f"REMOVE: {self.class_name}._get_idempotent_want() " + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"calling ImageInstallOptions.refresh() with " msg += f"serial_number {self.have.serial_number} " msg += f"ip_address {self.have.ip_address} " msg += f"device_name {self.have.device_name}" self.log_msg(msg) + instance.serial_number = self.have.serial_number instance.epld = want["upgrade"]["epld"] instance.issu = want["upgrade"]["nxos"] @@ -603,9 +622,11 @@ def _get_idempotent_want(self, want): if instance.epld_modules is None: idempotent_want["upgrade"]["epld"] = False - self.log_msg( - f"REMOVE: {self.class_name}._get_idempotent_want() returned idempotent_want: {idempotent_want}" - ) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f" return {idempotent_want}" + self.log_msg(msg) + return idempotent_want def get_need_merged(self): @@ -613,9 +634,10 @@ def get_need_merged(self): Caller: main() For merged state, populate self.need list() with items from - our want list that are not in our have list. These items will be sent - to NDFC. + our want list that are not in our have list. These items will + be sent to the controller. """ + self.method_name = "get_need_merged" need = [] for want_create in self.want_create: @@ -631,7 +653,8 @@ def get_need_merged(self): continue need.append(idempotent_want) self.need = need - msg = f"REMOVE: {self.class_name}.get_need_merged: " + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"need: {self.need}" self.log_msg(msg) @@ -640,8 +663,10 @@ def get_need_deleted(self): Caller: main() For deleted state, populate self.need list() with items from our want - list that are not in our have list. These items will be sent to NDFC. + list that are not in our have list. These items will be sent to + the controller. """ + self.method_name = "get_need_deleted" need = [] for want in self.want_create: self.have.ip_address = want["ip_address"] @@ -656,9 +681,10 @@ def get_need_query(self): """ Caller: main() - For query state, populate self.need list() with all items from our want - list. These items will be sent to NDFC. + For query state, populate self.need list() with all items from + our want list. These items will be sent to the controller. """ + self.method_name = "get_need_query" need = [] for want in self.want_create: need.append(want) @@ -771,17 +797,16 @@ def validate_input(self): Validate the playbook parameters """ + self.method_name = "validate_input" state = self.params["state"] - self.log_msg(f"{self.class_name}.validate_input: state: {state}") if state not in ["merged", "deleted", "query"]: - msg = f"This module supports deleted, merged, and query states. Got state {state}" + msg = f"{self.class_name}.{self.method_name}: " + msg += "This module supports deleted, merged, and query states. " + msg += f"Got state {state}" self.module.fail_json(msg) if state == "merged": - self.log_msg( - f"{self.class_name}.validate_input: call _validate_input_for_merged_state()" - ) self._validate_input_for_merged_state() return if state == "deleted": @@ -797,18 +822,23 @@ def _validate_input_for_merged_state(self): Validate that self.config contains appropriate values for merged state """ + self.method_name = "_validate_input_for_merged_state" + if not self.config: - msg = "config: element is mandatory for state merged" + msg = f"{self.class_name}.{self.method_name}: " + msg += "config: element is mandatory for state merged" self.module.fail_json(msg) params_spec = self._build_params_spec_for_merged_state() - self.log_msg( - f"{self.class_name}._validate_input_for_merged_state: params_spec: {params_spec}" - ) - self.log_msg( - f"{self.class_name}._validate_input_for_merged_state: self.config: {self.config}" - ) + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"params_spec: {params_spec}" + self.log_msg(msg) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"self.config: {self.config}" + self.log_msg(msg) + valid_params, invalid_params = validate_list_of_dicts( self.config.get("switches"), params_spec, self.module ) @@ -817,7 +847,8 @@ def _validate_input_for_merged_state(self): self.validated = copy.deepcopy(valid_params) if invalid_params: - msg = "Invalid parameters in playbook: " + msg = f"{self.class_name}.{self.method_name}: " + msg += "Invalid parameters in playbook: " msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) @@ -831,9 +862,12 @@ def _validate_input_for_deleted_state(self): 1. This is currently identical to _validate_input_for_merged_state() 2. Adding in case there are differences in the future """ + self.method_name = "_validate_input_for_deleted_state" + params_spec = self._build_params_spec_for_merged_state() if not self.config: - msg = "config: element is mandatory for state deleted" + msg = f"{self.class_name}.{self.method_name}: " + msg += "config: element is mandatory for state deleted" self.module.fail_json(msg) valid_params, invalid_params = validate_list_of_dicts( @@ -844,7 +878,8 @@ def _validate_input_for_deleted_state(self): self.validated = copy.deepcopy(valid_params) if invalid_params: - msg = "Invalid parameters in playbook: " + msg = f"{self.class_name}.{self.method_name}: " + msg += "Invalid parameters in playbook: " msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) @@ -858,9 +893,12 @@ def _validate_input_for_query_state(self): 1. This is currently identical to _validate_input_for_merged_state() 2. Adding in case there are differences in the future """ + self.method_name = "_validate_input_for_query_state" params_spec = self._build_params_spec_for_merged_state() + if not self.config: - msg = "config: element is mandatory for state query" + msg = f"{self.class_name}.{self.method_name}: " + msg += "config: element is mandatory for state query" self.module.fail_json(msg) valid_params, invalid_params = validate_list_of_dicts( @@ -871,7 +909,8 @@ def _validate_input_for_query_state(self): self.validated = copy.deepcopy(valid_params) if invalid_params: - msg = "Invalid parameters in playbook: " + msg = f"{self.class_name}.{self.method_name}: " + msg += "Invalid parameters in playbook: " msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) @@ -890,30 +929,36 @@ def _merge_global_and_switch_configs(self, config): 5. If global_config and switch_config are both missing a mandatory parameter, fail. """ + self.method_name = "_merge_global_and_switch_configs" + if not config.get("switches"): - msg = f"{self.class_name}._merge_global_and_switch_configs: " + msg = f"{self.class_name}.{self.method_name}: " msg += "playbook is missing list of switches" self.module.fail_json(msg) - msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"config: {config}" self.log_msg(msg) + global_config = {} global_config["policy"] = config.get("policy") global_config["stage"] = config.get("stage") global_config["upgrade"] = config.get("upgrade") global_config["options"] = config.get("options") global_config["validate"] = config.get("validate") - msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"global_config: {global_config}" self.log_msg(msg) + self.switch_configs = [] for switch in config["switches"]: switch_config = global_config.copy() | switch.copy() - msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " + self.switch_configs.append(switch_config) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"merged switch_config: {switch_config}" self.log_msg(msg) - self.switch_configs.append(switch_config) def _validate_switch_configs(self): """ @@ -928,34 +973,40 @@ def _validate_switch_configs(self): Callers: - self.get_want """ + self.method_name = "_validate_switch_configs" for switch in self.switch_configs: - msg = f"REMOVE: {self.class_name}._validate_switch_configs: " + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"switch: {switch}" self.log_msg(msg) + if not switch.get("ip_address"): + msg = f"{self.class_name}.{self.method_name}: " msg = "playbook is missing ip_address for at least one switch" self.module.fail_json(msg) + # for query state, the only mandatory parameter is ip_address # so skip the remaining checks if self.params.get("state") == "query": continue + if switch.get("policy") is None: - msg = "playbook is missing image policy for switch " + msg = f"{self.class_name}.{self.method_name}: " + msg += "playbook is missing image policy for switch " msg += f"{switch.get('ip_address')} " msg += "and global image policy is not defined." self.module.fail_json(msg) def _build_policy_attach_payload(self): """ - Build the payload for the policy attach request to NDFC - Verify that the image policy exists on NDFC + Build the payload for the policy attach request + Verify that the image policy exists on the controller Verify that the image policy supports the switch platform Callers: - self.handle_merged_state """ + self.method_name = "_build_policy_attach_payload" self.payloads = [] - # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests self.switch_details.refresh() self.image_policies.refresh() for switch in self.need: @@ -967,13 +1018,17 @@ def _build_policy_attach_payload(self): # Fail if the image policy does not exist. # Image policy creation is handled by a different module. if self.image_policies.name is None: - msg = f"policy {switch.get('policy')} does not exist on NDFC" + msg = f"{self.class_name}.{self.method_name}: " + msg += f"policy {switch.get('policy')} does not exist on " + msg += "the controller" self.module.fail_json(msg) # Fail if the image policy does not support the switch platform if self.switch_details.platform not in self.image_policies.platform: - msg = f"policy {switch.get('policy')} does not support platform " - msg += f"{self.switch_details.platform}. {switch.get('policy')} " + msg = f"{self.class_name}.{self.method_name}: " + msg += f"policy {switch.get('policy')} does not support " + msg += f"platform {self.switch_details.platform}. " + msg += f"Policy {switch.get('policy')} " msg += "supports the following platform(s): " msg += f"{self.image_policies.platform}" self.module.fail_json(msg) @@ -990,8 +1045,11 @@ def _build_policy_attach_payload(self): for item in payload: if payload[item] is None: - msg = f"Unable to determine {item} for switch {switch.get('ip_address')}. " - msg += "Please verify that the switch is managed by NDFC." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"Unable to determine {item} for switch " + msg += f"{switch.get('ip_address')}. " + msg += "Please verify that the switch is managed by " + msg += "the controller." self.module.fail_json(msg) self.payloads.append(payload) @@ -1002,8 +1060,10 @@ def _send_policy_attach_payload(self): Callers: - self.handle_merged_state """ + self.method_name = "_send_policy_attach_payload" if len(self.payloads) == 0: return + path = self.endpoints.policy_attach.get("path") verb = self.endpoints.policy_attach.get("verb") payload = {} @@ -1016,11 +1076,13 @@ def _send_policy_attach_payload(self): def _stage_images(self, serial_numbers): """ - Initiate image staging to the switch(es) associated with serial_numbers + Initiate image staging to the switch(es) associated + with serial_numbers Callers: - handle_merged_state """ + self.method_name = "_stage_images" instance = ImageStage(self.module) instance.serial_numbers = serial_numbers instance.commit() @@ -1032,6 +1094,7 @@ def _validate_images(self, serial_numbers): Callers: - handle_merged_state """ + self.method_name = "_validate_images" instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers # TODO:2 Discuss with Mike/Shangxin - ImageValidate.non_disruptive @@ -1073,15 +1136,16 @@ def _verify_install_options(self, devices): Callers: - self.handle_merged_state """ + self.method_name = "_verify_install_options" if len(devices) == 0: return install_options = ImageInstallOptions(self.module) - # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests self.switch_details.refresh() for device in devices: - self.log_msg( - f"REMOVE: {self.class_name}._verify_install_options: device: {device}" - ) + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"device: {device}" + self.log_msg(msg) + self.switch_details.ip_address = device.get("ip_address") install_options.serial_number = self.switch_details.serial_number install_options.policy_name = device["policy"] @@ -1092,7 +1156,8 @@ def _verify_install_options(self, devices): install_options.status not in ["Success", "Skipped"] and device["upgrade"]["nxos"] is True ): - msg = f"NXOS upgrade is set to True for switch " + msg = f"{self.class_name}.{self.method_name}: " + msg += f"NXOS upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " msg += f"NX-OS image" @@ -1101,6 +1166,7 @@ def _verify_install_options(self, devices): install_options.epld_modules is None and device["upgrade"]["epld"] is True ): + msg = f"{self.class_name}.{self.method_name}: " msg = f"EPLD upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " @@ -1114,18 +1180,14 @@ def _upgrade_images(self, devices): Callers: - handle_merged_state """ - self.log_msg(f"REMOVE: {self.class_name}._upgrade_images: devices: {devices}") + self.method_name = "_upgrade_images" + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"devices: {devices}" + self.log_msg(msg) + upgrade = ImageUpgrade(self.module) upgrade.devices = devices - # TODO:2 Discuss with Mike/Shangxin. Upgrade option handling mutex options. - # I'm leaning toward doing this in ImageUpgrade().validate_options() - # which would cover the various scenarios and fail_json() on invalid - # combinations. - # For epld upgrade disrutive must be True and non_disruptive must be False - # upgrade.epld_upgrade = True - # upgrade.disruptive = True - # upgrade.non_disruptive = False - # upgrade.epld_module = "ALL" upgrade.commit() def handle_merged_state(self): @@ -1137,6 +1199,7 @@ def handle_merged_state(self): Caller: main() """ + self.method_name = "handle_merged_state" # TODO:1 Replace these with ImagePolicyAction # See commented code below self._build_policy_attach_payload() @@ -1155,11 +1218,13 @@ def handle_merged_state(self): validate_devices = [] upgrade_devices = [] - # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests self.switch_details.refresh() for switch in self.need: - msg = f"REMOVE: {self.class_name}.handle_merged_state: switch: {switch}" + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"switch: {switch}" self.log_msg(msg) + self.switch_details.ip_address = switch.get("ip_address") device = {} device["serial_number"] = self.switch_details.serial_number @@ -1176,18 +1241,20 @@ def handle_merged_state(self): ): upgrade_devices.append(switch) - msg = f"REMOVE: {self.class_name}.handle_merged_state: stage_devices: {stage_devices}" + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"stage_devices: {stage_devices}" self.log_msg(msg) self._stage_images(stage_devices) - self.log_msg( - f"REMOVE: {self.class_name}.handle_merged_state: validate_devices: {validate_devices}" - ) + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"validate_devices: {validate_devices}" + self.log_msg(msg) self._validate_images(validate_devices) - self.log_msg( - f"REMOVE: {self.class_name}.handle_merged_state: upgrade_devices: {upgrade_devices}" - ) + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"upgrade_devices: {upgrade_devices}" + self.log_msg(msg) + self._verify_install_options(upgrade_devices) self._upgrade_images(upgrade_devices) @@ -1197,11 +1264,13 @@ def handle_deleted_state(self): Caller: main() """ - msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + self.method_name = "handle_deleted_state" + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"Entered with self.need {self.need}" self.log_msg(msg) + detach_policy_devices = {} - # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests self.switch_details.refresh() self.image_policies.refresh() for switch in self.need: @@ -1214,7 +1283,7 @@ def handle_deleted_state(self): detach_policy_devices[self.image_policies.policy_name].append( self.switch_details.serial_number ) - msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"detach_policy_devices: {detach_policy_devices}" self.log_msg(msg) @@ -1223,10 +1292,11 @@ def handle_deleted_state(self): return instance = ImagePolicyAction(self.module) for policy_name in detach_policy_devices: - msg = f"REMOVE: {self.class_name}.handle_deleted_state: " + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"detach policy_name: {policy_name}" msg += f" from devices: {detach_policy_devices[policy_name]}" self.log_msg(msg) + instance.policy_name = policy_name instance.action = "detach" instance.serial_numbers = detach_policy_devices[policy_name] @@ -1238,20 +1308,25 @@ def handle_query_state(self): Caller: main() """ + self.method_name = "handle_query_state" instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() - msg = f"REMOVE: {self.class_name}.handle_query_state: " + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"Entered. self.need {self.need}" self.log_msg(msg) + query_devices = [] for switch in self.need: instance.ip_address = switch.get("ip_address") if instance.filtered_data is None: continue query_devices.append(instance.filtered_data) - msg = f"REMOVE: {self.class_name}.handle_query_state: " + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"query_policies: {query_devices}" self.log_msg(msg) + self.result["response"] = query_devices self.result["diff"] = [] self.result["changed"] = False From 516b277099224569c41f70f43aaba657750520ee Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 07:47:02 -1000 Subject: [PATCH 023/300] Update docstring and DOCUMENTATION --- plugins/modules/dcnm_image_upgrade.py | 34 ++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 4b2a2e692..130570631 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -18,9 +18,9 @@ Ansible states "merged", "deleted", and "query" are implemented. -merged: attach image policy to one or more devices +merged: stage, validate, upgrade image for one or more devices deleted: delete image policy from one or more devices -query: return image policy details for one or more devices +query: return switch issu details for one or more devices """ from __future__ import absolute_import, division, print_function @@ -69,16 +69,18 @@ DOCUMENTATION = """ --- module: dcnm_image_upgrade -short_description: Attach, detach, and query device image policies. +short_description: Image management for Nexus switches version_added: "0.9.0" description: - - Attach, detach, and query device image policies. + - Stage, validate, upgrade images. + - Attach, detach, image policies. + - Query device issu details. author: Cisco Systems, Inc. options: state: description: - The state of the feature or object after module completion. - - I(merged) and I(query) are the only states supported. + - I(merged), I(deleted), and I(query) states are supported. type: str choices: - merged @@ -348,9 +350,12 @@ # # merged: # Attach image policy to one or more devices. +# Stage image on one or more devices. +# Validate image on one or more devices. +# Upgrade image on one or more devices. # # query: -# Return image policy details for one or more devices. +# Return ISSU details for one or more devices. # # deleted: # Delete image policy from one or more devices @@ -423,6 +428,23 @@ - ip_address: 192.168.1.1 - ip_address: 192.168.1.2 +# Query ISSU details for three devices + - name: query switch ISSU status + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + policy: KMR5 + switches: + - ip_address: 192.168.1.1 + policy: OR1F + - ip_address: 192.168.1.2 + policy: NR2F + - ip_address: 192.168.1.3 # will query policy KMR5 + register: result + - name: print result + ansible.builtin.debug: + var: result + """ From fe6aecf90d5f0efa36ff49a949a90b7103ae2abf Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 08:02:42 -1000 Subject: [PATCH 024/300] cleanup log and fail_json messages --- .../module_utils/image_mgmt/image_policies.py | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 4c386de7f..2eff7af67 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -31,13 +31,16 @@ class ImagePolicies(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ + self.method_name = "__init__" self.endpoints = ApiEndpoints() - self.log_msg(f"{self.class_name}.__init__ entered") self._init_properties() - # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it - #self.refresh() + + msg = f"REMOVE: {self.class_name}.{self.method_name}: entered" + self.log_msg(msg) + def _init_properties(self): + self.method_name = "_init_properties" self.properties = {} self.properties["policy_name"] = None self.properties["response_data"] = None @@ -48,53 +51,66 @@ def refresh(self): """ Refresh self.image_policies with current image policies from the controller """ + self.method_name = "refresh" + path = self.endpoints.policies_info.get("path") verb = self.endpoints.policies_info.get("verb") self.properties["response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - self.properties["result"] = self._handle_response(self.response, verb) - self.log_msg(f"{self.class_name}.refresh: result: {self.result}") - msg = f"REMOVE: {self.class_name}.refresh: " + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"response: {self.response}" + self.log_msg(msg) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"result: {self.result}" + self.log_msg(msg) + if not self.result["success"]: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retriving image policy " msg += "information from the controller." self.module.fail_json(msg) data = self.response.get("DATA").get("lastOperDataObject") + if data is None: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "Bad response when retrieving image policy " msg += "information from the controller." self.module.fail_json(msg) + if len(data) == 0: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "the controller has no defined image policies." self.module.fail_json(msg) + self.properties["response_data"] = {} + for policy in data: policy_name = policy.get("policyName") if policy_name is None: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "Cannot parse policy information from the controller." self.module.fail_json(msg) self.properties["response_data"][policy_name] = policy def _get(self, item): + self.method_name = "_get" + if self.policy_name is None: - msg = f"{self.class_name}._get: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"instance.policy_name must be set before " msg += f"accessing property {item}." self.module.fail_json(msg) + if self.properties['response_data'].get(self.policy_name) is None: - msg = f"{self.class_name}._get: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"policy_name {self.policy_name} is not defined on " msg += "the controller." self.module.fail_json(msg) + return_item = self.make_boolean(self.properties["response_data"][self.policy_name].get(item)) return_item = self.make_none(return_item) return return_item From 617bc127499891e695609375cb6c444caca8a9fa Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 08:40:19 -1000 Subject: [PATCH 025/300] cleanup log and fail_json messages --- .../image_mgmt/image_policy_action.py | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 2af77d8f5..c57fbbda5 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -39,6 +39,7 @@ class ImagePolicyAction(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ + self.method_name = "__init__" self.endpoints = ApiEndpoints() self._init_properties() self.image_policies = ImagePolicies(self.module) @@ -46,6 +47,7 @@ def __init__(self, module): self.valid_actions = {"attach", "detach", "query"} def _init_properties(self): + self.method_name = "_init_properties" self.properties = {} self.properties["action"] = None self.properties["response"] = None @@ -61,8 +63,9 @@ def build_attach_payload(self): caller _attach_policy() """ + self.method_name = "build_attach_payload" self.payloads = [] - # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it + self.switch_issu_details.refresh() for serial_number in self.serial_numbers: self.switch_issu_details.serial_number = serial_number @@ -74,7 +77,8 @@ def build_attach_payload(self): payload["serialNumber"] = self.switch_issu_details.serial_number for item in payload: if payload[item] is None: - msg = f"Unable to determine {item} for switch " + msg = f"{self.class_name}.{self.method_name}: " + msg += f" Unable to determine {item} for switch " msg += f"{self.switch_issu_details.ip_address}, " msg += f"{self.switch_issu_details.serial_number}, " msg += f"{self.switch_issu_details.device_name}. " @@ -87,48 +91,56 @@ def validate_request(self): """ validations prior to commit() should be added here. """ - self.log_msg(f"REMOVE: {self.class_name}.validate_request: Entered") + self.method_name = "validate_request" + if self.action is None: - msg = f"{self.class_name}.validate_request: " + msg = f"{self.class_name}.{self.method_name}: " msg += "instance.action must be set before " msg += "calling commit()" self.module.fail_json(msg) if self.policy_name is None: - msg = f"{self.class_name}.validate_request: " + msg = f"{self.class_name}.{self.method_name}: " msg += "instance.policy_name must be set before " msg += "calling commit()" self.module.fail_json(msg) - self.log_msg(f"REMOVE: {self.class_name}.validate_request: action {self.action}") + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg = f"action {self.action}" + self.log_msg(msg) if self.action == "query": return - self.log_msg(f"REMOVE: {self.class_name}.validate_request: serial_numbers {self.serial_numbers}") + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg = f"serial_numbers {self.serial_numbers}" + self.log_msg(msg) if self.serial_numbers is None: - msg = f"{self.class_name}.validate_request: " + msg = f"{self.class_name}.{self.method_name}: " msg += "instance.serial_numbers must be set before " msg += "calling commit()" self.module.fail_json(msg) - # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it self.image_policies.refresh() self.switch_issu_details.refresh() + # Fail if the image policy does not support the switch platform self.image_policies.policy_name = self.policy_name for serial_number in self.serial_numbers: self.switch_issu_details.serial_number = serial_number if self.switch_issu_details.platform not in self.image_policies.platform: - msg = f"policy {self.policy_name} does not support platform " + msg = f"{self.class_name}.{self.method_name}: " + msg += f"policy {self.policy_name} does not support platform " msg += f"{self.switch_issu_details.platform}. {self.policy_name} " msg += "supports the following platform(s): " msg += f"{self.image_policies.platform}" self.module.fail_json(msg) def commit(self): + self.method_name = "commit" + self.validate_request() if self.action == "attach": self._attach_policy() @@ -137,7 +149,7 @@ def commit(self): elif self.action == "query": self._query_policy() else: - msg = f"{self.class_name}.commit: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"Unknown action {self.action}." self.module.fail_json(msg) @@ -150,6 +162,8 @@ def _attach_policy(self): are accessible via properties response and result, respectively. """ + self.method_name = "_attach_policy" + self.build_attach_payload() path = self.endpoints.policy_attach.get("path") verb = self.endpoints.policy_attach.get("verb") @@ -158,13 +172,16 @@ def _attach_policy(self): for payload in self.payloads: response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) result = self._handle_response(response, verb) + if not result["success"]: - msg = f"{self.class_name}._attach_policy: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " msg += f"to switch {payload['ipAddr']}." self.module.fail_json(msg) + responses.append(response) results.append(result) + self.properties["response"] = responses self.properties["result"] = results @@ -175,6 +192,8 @@ def _detach_policy(self): endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy query_params: ?serialNumber=FDO211218GC,FDO21120U5D """ + self.method_name = "_detach_policy" + path = self.endpoints.policy_detach.get("path") verb = self.endpoints.policy_detach.get("verb") query_params = ",".join(self.serial_numbers) @@ -192,6 +211,8 @@ def _query_policy(self): verb: GET endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ """ + self.method_name = "_query_policy" + path = self.endpoints.policy_info.get("path") verb = self.endpoints.policy_info.get("verb") path = path.replace("__POLICY_NAME__", self.policy_name) @@ -221,10 +242,14 @@ def action(self): @action.setter def action(self, value): + self.method_name = "action.setter" + if value not in self.valid_actions: - msg = f"{self.class_name}: instance.action must be " - msg += f"one of {','.join(sorted(self.valid_actions))}" + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.action must be one of " + msg += f"{','.join(sorted(self.valid_actions))}" self.module.fail_json(msg) + self.properties["action"] = value @property @@ -274,9 +299,10 @@ def serial_numbers(self): @serial_numbers.setter def serial_numbers(self, value): + self.method_name = "serial_numbers.setter" if not isinstance(value, list): - msg = f"{self.class_name}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.serial_numbers must be a " + msg += f"python list of switch serial numbers." self.module.fail_json(msg) self.properties["serial_numbers"] = value - From cc6bb476bb90f405b77d38a7d95dd73a8bbcd65b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 09:22:07 -1000 Subject: [PATCH 026/300] cleanup log and fail_json messages, more... Run through black/isort --- .../image_mgmt/switch_issu_details.py | 111 ++++++++++++------ 1 file changed, 77 insertions(+), 34 deletions(-) diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 88cc221d7..9bbedee07 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -1,6 +1,11 @@ -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import dcnm_send -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +import inspect + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send class SwitchIssuDetails(ImageUpgradeCommon): @@ -64,10 +69,14 @@ class SwitchIssuDetails(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ + self.method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() + msg = f"REMOVE: {self.class_name}.{self.method_name}: entered" + self.log_msg(msg) self._init_properties() def _init_properties(self): + self.method_name = inspect.stack()[0][3] self.properties = {} self.properties["response"] = None self.properties["result"] = None @@ -80,37 +89,41 @@ def _init_properties(self): self.properties["action_keys"].add("upgrade") self.properties["action_keys"].add("validated") - def refresh(self) -> None: """ Refresh current issu details from the controller. """ + self.method_name = inspect.stack()[0][3] + path = self.endpoints.issu_info.get("path") verb = self.endpoints.issu_info.get("verb") self.properties["response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - self.properties["result"] = self._handle_response(self.response, verb) - self.log_msg(f"{self.class_name}.refresh: result: {self.result}") - msg = f"REMOVE: {self.class_name}.refresh: " + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"response: {self.response}" + self.log_msg(msg) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"result: {self.result}" - # result for 404 response - # {'found': False, 'success': True} + self.log_msg(msg) + if self.result["success"] == False or self.result["found"] == False: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retriving switch " msg += "information from the controller" self.module.fail_json(msg) data = self.response.get("DATA").get("lastOperDataObject") + if data is None: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "The controller has no switch ISSU information." self.module.fail_json(msg) + if len(data) == 0: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "The controller has no switch ISSU information." self.module.fail_json(msg) @@ -125,6 +138,8 @@ def actions_in_progress(self): Return True if any actions are in progress Return False otherwise """ + self.method_name = inspect.stack()[0][3] + for action_key in self.properties["action_keys"]: if self._get(action_key) == "In-Progress": return True @@ -633,10 +648,12 @@ class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): def __init__(self, module): super().__init__(module) + self.method_name = inspect.stack()[0][3] self._init_properties() def _init_properties(self): super()._init_properties() + self.method_name = inspect.stack()[0][3] self.properties["ip_address"] = None def refresh(self): @@ -646,25 +663,30 @@ def refresh(self): Refresh ip_address current issu details from the controller """ super().refresh() + self.method_name = inspect.stack()[0][3] self.data_subclass = {} for switch in self.response_data: - msg = f"{self.class_name}.refresh: " - msg += f"switch {switch}" self.data_subclass[switch["ipAddress"]] = switch def _get(self, item): + self.method_name = inspect.stack()[0][3] + if self.ip_address is None: - msg = f"{self.class_name}._get: set instance.ip_address " - msg += f"before accessing property {item}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "set instance.ip_address before accessing " + msg += f"property {item}." self.module.fail_json(msg) + if self.data_subclass.get(self.ip_address) is None: - msg = f"{self.class_name}._get: {self.ip_address} does not " - msg += f"exist on the controller." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.ip_address} does not exist on the controller." self.module.fail_json(msg) + if self.data_subclass[self.ip_address].get(item) is None: - msg = f"{self.class_name}._get: {self.ip_address} unknown " - msg += f"property name: {item}." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.ip_address} unknown property name: {item}." self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.ip_address].get(item)) @property @@ -711,10 +733,12 @@ class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): def __init__(self, module): super().__init__(module) + self.method_name = inspect.stack()[0][3] self._init_properties() def _init_properties(self): super()._init_properties() + self.method_name = inspect.stack()[0][3] self.properties["serial_number"] = None def refresh(self): @@ -724,23 +748,32 @@ def refresh(self): Refresh serial_number current issu details from NDFC """ super().refresh() + self.method_name = inspect.stack()[0][3] + self.data_subclass = {} for switch in self.response_data: self.data_subclass[switch["serialNumber"]] = switch def _get(self, item): + self.method_name = inspect.stack()[0][3] + if self.serial_number is None: - msg = f"{self.class_name}._get: set instance.serial_number " - msg += f"before accessing property {item}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "set instance.serial_number before " + msg += f"accessing property {item}." self.module.fail_json(msg) + if self.data_subclass.get(self.serial_number) is None: - msg = f"{self.class_name}._get: {self.serial_number} does not " - msg += f"exist on the controller." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.serial_number} does not exist " + msg += "on the controller." self.module.fail_json(msg) + if self.data_subclass[self.serial_number].get(item) is None: - msg = f"{self.class_name}._get: {self.serial_number} unknown " - msg += f"property name: {item}." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.serial_number} unknown property name: {item}." self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.serial_number].get(item)) @property @@ -786,10 +819,12 @@ class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): def __init__(self, module): super().__init__(module) + self.method_name = inspect.stack()[0][3] self._init_properties() def _init_properties(self): super()._init_properties() + self.method_name = inspect.stack()[0][3] self.properties["device_name"] = None def refresh(self): @@ -799,23 +834,31 @@ def refresh(self): Refresh device_name current issu details from NDFC """ super().refresh() + self.method_name = inspect.stack()[0][3] self.data_subclass = {} for switch in self.response_data: self.data_subclass[switch["deviceName"]] = switch def _get(self, item): + self.method_name = inspect.stack()[0][3] + if self.device_name is None: - msg = f"{self.class_name}._get: set instance.device_name " - msg += f"before accessing property {item}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "set instance.device_name before " + msg += f"accessing property {item}." self.module.fail_json(msg) + if self.data_subclass.get(self.device_name) is None: - msg = f"{self.class_name}._get: {self.device_name} does not " - msg += f"exist on the controller." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.device_name} does not exist " + msg += f"on the controller." self.module.fail_json(msg) + if self.data_subclass[self.device_name].get(item) is None: - msg = f"{self.class_name}._get: {self.device_name} unknown " - msg += f"property name: {item}." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.device_name} unknown property name: {item}." self.module.fail_json(msg) + return self.make_none(self.data_subclass[self.device_name].get(item)) @property @@ -837,4 +880,4 @@ def device_name(self): @device_name.setter def device_name(self, value): - self.properties["device_name"] = value \ No newline at end of file + self.properties["device_name"] = value From 511dd24c97e630f4cb28f66180101a179c2bdde7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 10:00:51 -1000 Subject: [PATCH 027/300] cleanup log and fail_json messages --- .../module_utils/image_mgmt/image_stage.py | 148 ++++++++---------- .../test_image_upgrade_ImageStage.py | 4 +- 2 files changed, 66 insertions(+), 86 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 00d70a647..dff4adc0d 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -1,13 +1,19 @@ import copy +import inspect import json from time import sleep -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber -from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import ControllerVersion + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ + ControllerVersion +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send + class ImageStage(ImageUpgradeCommon): """ @@ -75,6 +81,7 @@ class ImageStage(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ + self.method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() self._init_properties() self.serial_numbers_done = set() @@ -85,6 +92,7 @@ def __init__(self, module): self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): + self.method_name = inspect.stack()[0][3] self.properties = {} self.properties["serial_numbers"] = None self.properties["response_data"] = None @@ -101,6 +109,7 @@ def _populate_controller_version(self): 1. This cannot go into ImageUpgradeCommon() due to circular imports resulting in RecursionError """ + self.method_name = inspect.stack()[0][3] instance = ControllerVersion(self.module) instance.refresh() self.controller_version = instance.version @@ -110,28 +119,33 @@ def prune_serial_numbers(self): If the image is already staged on a switch, remove that switch's serial number from the list of serial numbers to stage. """ + self.method_name = inspect.stack()[0][3] serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: self.issu_detail.serial_number = serial_number self.issu_detail.refresh() if self.issu_detail.image_staged == "Success": - msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " + self.serial_numbers.remove(serial_number) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += "image already staged for " msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " msg += f"{self.issu_detail.serial_number}." self.log_msg(msg) - self.serial_numbers.remove(serial_number) def validate_serial_numbers(self): """ Fail if the image_staged state for any serial_number is Failed. """ + self.method_name = inspect.stack()[0][3] for serial_number in self.serial_numbers: self.issu_detail.serial_number = serial_number self.issu_detail.refresh() + if self.issu_detail.image_staged == "Failed": + msg = f"{self.class_name}.{self.method_name}: " msg = "Image staging is failing for the following switch: " msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " @@ -139,22 +153,23 @@ def validate_serial_numbers(self): msg += f"Check the switch connectivity to the controller " msg += "and try again." self.module.fail_json(msg) - else: - self.log_msg(f"Image staging is not failing for the following switch: {self.issu_detail.serial_number}") def commit(self): """ Commit the image staging request to the controller and wait for the images to be staged. """ + self.method_name = inspect.stack()[0][3] + if self.serial_numbers is None: - msg = f"{self.class_name}.commit() call instance.serial_numbers " - msg += "before calling commit()." + msg = f"{self.class_name}.{self.method_name}: " + msg += "call instance.serial_numbers " + msg += "before calling commit." self.module.fail_json(msg) + if len(self.serial_numbers) == 0: - msg = f"REMOVE: {self.class_name}.commit() no serial numbers to stage." - self.log_msg(msg) return + self.prune_serial_numbers() self.validate_serial_numbers() self._wait_for_current_actions_to_complete() @@ -162,27 +177,34 @@ def commit(self): self.path = self.endpoints.image_stage.get("path") self.verb = self.endpoints.image_stage.get("verb") - self.log_msg(f"REMOVE: {self.class_name}.commit() path: {self.path}") - self.log_msg(f"REMOVE: {self.class_name}.commit() verb: {self.verb}") self.payload = {} self._populate_controller_version() + if self.controller_version == "12.1.2e": # Yes, version 12.1.2e wants serialNum to be misspelled self.payload["sereialNum"] = self.serial_numbers else: self.payload["serialNumbers"] = self.serial_numbers + self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) self.properties["result"] = self._handle_response(self.response, self.verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.response}" - ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"response: {self.response}" + self.log_msg(msg) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"result: {self.result}" + self.log_msg(msg) + if not self.result["success"]: - msg = f"{self.class_name}.commit() failed: {self.result}. " + msg = f"{self.class_name}.{self.method_name}: " + msg = f"failed: {self.result}. " msg += f"Controller response: {self.response}" self.module.fail_json(msg) + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_stage_to_complete() @@ -192,66 +214,53 @@ def _wait_for_current_actions_to_complete(self): progress. Wait for all actions to complete before staging image. Actions include image staging, image upgrade, and image validation. """ + self.method_name = inspect.stack()[0][3] + self.serial_numbers_done = set() serial_numbers_todo = set(copy.copy(self.serial_numbers)) timeout = self.check_timeout + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue + self.issu_detail.serial_number = serial_number self.issu_detail.refresh() - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} actions in progress: " - msg += f"{self.issu_detail.actions_in_progress}, " - msg += f"{timeout} seconds remaining." - self.log_msg(msg) + if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) self.serial_numbers_done.add(serial_number) + if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_current_actions_to_complete: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"Timed out waiting for actions to complete. " msg += f"serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += f"serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) self.module.fail_json(msg) def _wait_for_image_stage_to_complete(self): """ # Wait for image stage to complete """ - # We're promoting serial_numbers_done to a class-level attribute - # so that it can be used in unit test asserts. + self.method_name = inspect.stack()[0][3] + self.serial_numbers_done = set() timeout = self.check_timeout serial_numbers_todo = set(copy.copy(self.serial_numbers)) + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" - self.log_msg(msg) + for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue + self.issu_detail.serial_number = serial_number self.issu_detail.refresh() ip_address = self.issu_detail.ip_address @@ -259,54 +268,23 @@ def _wait_for_image_stage_to_complete(self): staged_percent = self.issu_detail.image_staged_percent staged_status = self.issu_detail.image_staged - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Seconds remaining {timeout}: " - msg += f"{device_name}, {serial_number}, {ip_address} " - msg += f"image staged percent: {staged_percent}" - self.log_msg(msg) - if staged_status == "Failed": - msg = f"Seconds remaining {timeout}: stage image failed " + msg = f"{self.class_name}.{self.method_name}: " + msg += f"Seconds remaining {timeout}: stage image failed " msg += f"for {device_name}, {serial_number}, {ip_address}. " msg += f"image staged percent: {staged_percent}" self.module.fail_json(msg) if staged_status == "Success": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Seconds remaining {timeout}: stage image complete for " - msg += f"for {device_name}, {serial_number}, {ip_address}. " - msg += f"image staged percent: {staged_percent}" - self.log_msg(msg) self.serial_numbers_done.add(serial_number) - # if staged_status == None: - # msg = f"REMOVE: {self.class_name}." - # msg += "_wait_for_image_stage_to_complete: " - # msg += f"Seconds remaining {timeout}: stage image " - # msg += "not started for " - # msg += f"{device_name}, {serial_number}, {ip_address}. " - # msg += f"image staged percent: {staged_percent}" - # self.log_msg(msg) - - # if staged_status == "In Progress": - # msg = f"REMOVE: {self.class_name}." - # msg += "_wait_for_image_stage_to_complete: " - # msg += f"Seconds remaining {timeout}: stage image " - # msg += f"{staged_status} for " - # msg += f"{device_name}, {serial_number}, {ip_address}. " - # msg += f"image staged percent: {staged_percent}" - # self.log_msg(msg) if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_image_stage_to_complete: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"Timed out waiting for image stage to complete. " msg += f"serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += f"serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) self.module.fail_json(msg) @property @@ -320,9 +298,11 @@ def serial_numbers(self): @serial_numbers.setter def serial_numbers(self, value): + self.method_name = inspect.stack()[0][3] if not isinstance(value, list): - msg = f"{self.__class__.__name__}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.serial_numbers must be a " + msg += "python list of switch serial numbers." self.module.fail_json(msg) self.properties["serial_numbers"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py index 7b84ede7a..02c2d2eb0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py @@ -210,8 +210,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: # test_commit_serial_numbers -match = r"ImageStage.commit\(\) call instance.serial_numbers " -match += r"before calling commit\(\)." +match = r"ImageStage.commit: call instance.serial_numbers " +match += r"before calling commit." @pytest.mark.parametrize( From 6cc6643d70007471a2ceee2d84ba334bdb5cd6f9 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 10:04:12 -1000 Subject: [PATCH 028/300] cleanup log messages --- .../module_utils/image_mgmt/switch_issu_details.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 9bbedee07..673fab081 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -71,8 +71,6 @@ def __init__(self, module): self.class_name = self.__class__.__name__ self.method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() - msg = f"REMOVE: {self.class_name}.{self.method_name}: entered" - self.log_msg(msg) self._init_properties() def _init_properties(self): @@ -101,14 +99,6 @@ def refresh(self) -> None: self.properties["response"] = dcnm_send(self.module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"response: {self.response}" - self.log_msg(msg) - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"result: {self.result}" - self.log_msg(msg) - if self.result["success"] == False or self.result["found"] == False: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retriving switch " @@ -130,7 +120,6 @@ def refresh(self) -> None: self.properties["response_data"] = self.response.get("DATA", {}).get( "lastOperDataObject", [] ) - self.log_msg(f"{self.class_name}.refresh: response: {self.response}") @property def actions_in_progress(self): From 08bb9ae8551a0c36fc8e0a386102dc3ac60f42fd Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 10:08:44 -1000 Subject: [PATCH 029/300] cleanup log and fail_json messages --- .../module_utils/image_mgmt/image_policies.py | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 2eff7af67..000708241 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -1,3 +1,5 @@ +import inspect + from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) @@ -31,16 +33,12 @@ class ImagePolicies(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.method_name = "__init__" + self.method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() self._init_properties() - msg = f"REMOVE: {self.class_name}.{self.method_name}: entered" - self.log_msg(msg) - - def _init_properties(self): - self.method_name = "_init_properties" + self.method_name = inspect.stack()[0][3] self.properties = {} self.properties["policy_name"] = None self.properties["response_data"] = None @@ -51,7 +49,7 @@ def refresh(self): """ Refresh self.image_policies with current image policies from the controller """ - self.method_name = "refresh" + self.method_name = inspect.stack()[0][3] path = self.endpoints.policies_info.get("path") verb = self.endpoints.policies_info.get("verb") @@ -59,14 +57,6 @@ def refresh(self): self.properties["response"] = dcnm_send(self.module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"response: {self.response}" - self.log_msg(msg) - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"result: {self.result}" - self.log_msg(msg) - if not self.result["success"]: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retriving image policy " @@ -90,14 +80,16 @@ def refresh(self): for policy in data: policy_name = policy.get("policyName") + if policy_name is None: msg = f"{self.class_name}.{self.method_name}: " msg += "Cannot parse policy information from the controller." self.module.fail_json(msg) + self.properties["response_data"][policy_name] = policy def _get(self, item): - self.method_name = "_get" + self.method_name = inspect.stack()[0][3] if self.policy_name is None: msg = f"{self.class_name}.{self.method_name}: " From 2fbb7c93cde79fe71b074ad88ed85d8ca4b73311 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 10:27:09 -1000 Subject: [PATCH 030/300] cleanup log/fail_json messages, minor code cleanup --- .../image_mgmt/image_policy_action.py | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index c57fbbda5..d42a4b4fb 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -1,3 +1,4 @@ +import inspect import json from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, @@ -39,7 +40,7 @@ class ImagePolicyAction(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.method_name = "__init__" + self.method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() self._init_properties() self.image_policies = ImagePolicies(self.module) @@ -47,7 +48,7 @@ def __init__(self, module): self.valid_actions = {"attach", "detach", "query"} def _init_properties(self): - self.method_name = "_init_properties" + self.method_name = inspect.stack()[0][3] self.properties = {} self.properties["action"] = None self.properties["response"] = None @@ -63,7 +64,7 @@ def build_attach_payload(self): caller _attach_policy() """ - self.method_name = "build_attach_payload" + self.method_name = inspect.stack()[0][3] self.payloads = [] self.switch_issu_details.refresh() @@ -91,7 +92,7 @@ def validate_request(self): """ validations prior to commit() should be added here. """ - self.method_name = "validate_request" + self.method_name = inspect.stack()[0][3] if self.action is None: msg = f"{self.class_name}.{self.method_name}: " @@ -105,24 +106,15 @@ def validate_request(self): msg += "calling commit()" self.module.fail_json(msg) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg = f"action {self.action}" - self.log_msg(msg) - if self.action == "query": return - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg = f"serial_numbers {self.serial_numbers}" - self.log_msg(msg) - if self.serial_numbers is None: msg = f"{self.class_name}.{self.method_name}: " msg += "instance.serial_numbers must be set before " msg += "calling commit()" self.module.fail_json(msg) - self.image_policies.refresh() self.switch_issu_details.refresh() @@ -139,7 +131,7 @@ def validate_request(self): self.module.fail_json(msg) def commit(self): - self.method_name = "commit" + self.method_name = inspect.stack()[0][3] self.validate_request() if self.action == "attach": @@ -162,16 +154,19 @@ def _attach_policy(self): are accessible via properties response and result, respectively. """ - self.method_name = "_attach_policy" + self.method_name = inspect.stack()[0][3] self.build_attach_payload() - path = self.endpoints.policy_attach.get("path") - verb = self.endpoints.policy_attach.get("verb") + + self.path = self.endpoints.policy_attach.get("path") + self.verb = self.endpoints.policy_attach.get("verb") + responses = [] results = [] + for payload in self.payloads: - response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) - result = self._handle_response(response, verb) + response = dcnm_send(self.module, self.verb, self.path, data=json.dumps(payload)) + result = self._handle_response(response, self.verb) if not result["success"]: msg = f"{self.class_name}.{self.method_name}: " @@ -192,18 +187,19 @@ def _detach_policy(self): endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy query_params: ?serialNumber=FDO211218GC,FDO21120U5D """ - self.method_name = "_detach_policy" + self.method_name = inspect.stack()[0][3] + + self.path = self.endpoints.policy_detach.get("path") + self.verb = self.endpoints.policy_detach.get("verb") - path = self.endpoints.policy_detach.get("path") - verb = self.endpoints.policy_detach.get("verb") query_params = ",".join(self.serial_numbers) - path += f"?serialNumber={query_params}" - response = dcnm_send(self.module, verb, path) - result = self._handle_response(response, verb) - if not result["success"]: - self._failure(response) - self.properties["response"] = response - self.properties["result"] = result + self.path += f"?serialNumber={query_params}" + + self.properties["response"] = dcnm_send(self.module, self.verb, self.path) + self.properties["result"] = self._handle_response(self.response, self.verb) + + if not self.result["success"]: + self._failure(self.response) def _query_policy(self): """ @@ -213,16 +209,18 @@ def _query_policy(self): """ self.method_name = "_query_policy" - path = self.endpoints.policy_info.get("path") - verb = self.endpoints.policy_info.get("verb") - path = path.replace("__POLICY_NAME__", self.policy_name) - response = dcnm_send(self.module, verb, path) - result = self._handle_response(response, verb) - if not result["success"]: - self._failure(response) - self.properties["query_result"] = response.get("DATA") - self.properties["response"] = response - self.properties["result"] = result + self.path = self.endpoints.policy_info.get("path") + self.verb = self.endpoints.policy_info.get("verb") + + self.path = self.path.replace("__POLICY_NAME__", self.policy_name) + + self.properties["response"] = dcnm_send(self.module, self.verb, self.path) + self.properties["result"] = self._handle_response(self.response, self.verb) + + if not self.result["success"]: + self._failure(self.response) + + self.properties["query_result"] = self.response.get("DATA") @property def query_result(self): From 612638e9535cc915d72325738d2ad616bda2504c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 11:06:27 -1000 Subject: [PATCH 031/300] cleanup log and fail_json messages --- .../image_mgmt/image_upgrade_common.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 6961a773b..3f30c4c2d 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -1,3 +1,5 @@ +import inspect + class ImageUpgradeCommon: """ Base class for the other image upgrade classes @@ -11,6 +13,9 @@ def __init__(self, module): """ def __init__(self, module): + self.class_name = __class__.__name__ + self.method_name = inspect.stack()[0][3] + self.module = module self.params = module.params self.debug = True @@ -19,6 +24,8 @@ def __init__(self, module): self.module = module def _handle_response(self, response, verb): + self.method_name = inspect.stack()[0][3] + if verb == "GET": return self._handle_get_response(response) if verb in {"POST", "PUT", "DELETE"}: @@ -26,7 +33,10 @@ def _handle_response(self, response, verb): return self._handle_unknown_request_verbs(response, verb) def _handle_unknown_request_verbs(self, response, verb): - msg = f"Unknown request verb ({verb}) in _handle_response() for response {response}." + self.method_name = inspect.stack()[0][3] + + msg = f"{self.class_name}.{self.method_name}: " + msg += f"Unknown request verb ({verb}) for response {response}." self.module.fail_json(msg) def _handle_get_response(self, response): @@ -42,6 +52,8 @@ def _handle_get_response(self, response): - False if RETURN_CODE != 200 or MESSAGE != "OK" - True otherwise """ + self.method_name = inspect.stack()[0][3] + result = {} success_return_codes = {200, 404} if ( @@ -77,6 +89,8 @@ def _handle_post_put_delete_response(self, response): - False if RETURN_CODE != 200 or MESSAGE != "OK" - True otherwise """ + self.method_name = inspect.stack()[0][3] + result = {} if response.get("ERROR") is not None: result["success"] = False @@ -93,7 +107,7 @@ def _handle_post_put_delete_response(self, response): def log_msg(self, msg): """ used for debugging. disable this when committing to main - by setting __init__().debug to False + by setting self.debug to False in __init__() """ if self.debug is False: return @@ -114,6 +128,8 @@ def make_boolean(self, value): Return value converted to boolean, if possible. Return value, if value cannot be converted. """ + self.method_name = inspect.stack()[0][3] + if isinstance(value, bool): return value if isinstance(value, str): @@ -129,6 +145,8 @@ def make_none(self, value): representation of a None type Return value otherwise """ + self.method_name = inspect.stack()[0][3] + if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: return None return value From de85e868e933ab2e4951f101c9d8d74bc56bfafc Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 11:26:27 -1000 Subject: [PATCH 032/300] Remove self.method_name from response handlers We cannot set self.method_name in the following handlers, since we want the calling method's name to appear in fail_json messages: _handle_response _handle_get_response _handle_post_put_delete_response --- .../module_utils/image_mgmt/image_upgrade_common.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 3f30c4c2d..841c14626 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -24,7 +24,9 @@ def __init__(self, module): self.module = module def _handle_response(self, response, verb): - self.method_name = inspect.stack()[0][3] + # don't add self.method_name to this method since + # it is called by other methods and we want their + # method_names in the log if verb == "GET": return self._handle_get_response(response) @@ -52,7 +54,9 @@ def _handle_get_response(self, response): - False if RETURN_CODE != 200 or MESSAGE != "OK" - True otherwise """ - self.method_name = inspect.stack()[0][3] + # don't add self.method_name to this method since + # it is called by other methods and we want their + # method_names in the log result = {} success_return_codes = {200, 404} @@ -89,7 +93,9 @@ def _handle_post_put_delete_response(self, response): - False if RETURN_CODE != 200 or MESSAGE != "OK" - True otherwise """ - self.method_name = inspect.stack()[0][3] + # don't add self.method_name to this method since + # it is called by other methods and we want their + # method_names in the log result = {} if response.get("ERROR") is not None: From 9236c78c4d13f98e88576f3110a5a124a25955f7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 14:28:38 -1000 Subject: [PATCH 033/300] cleanup log and fail_json messages, more... _wait_for_current_actions_to_complete - Use same logic as _wait_for_image_upgrade_to_complete --- .../module_utils/image_mgmt/image_upgrade.py | 306 +++++++++++------- 1 file changed, 182 insertions(+), 124 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index cca718bee..9fa915b6e 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -1,4 +1,5 @@ import copy +import inspect import json from time import sleep @@ -105,6 +106,8 @@ class ImageUpgrade(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ + self.method_name = inspect.stack()[0][3] + self.endpoints = ApiEndpoints() # Maximum number of modules/linecards in a switch self.max_module_number = 9 @@ -114,6 +117,8 @@ def __init__(self, module): self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) def _init_defaults(self): + self.method_name = inspect.stack()[0][3] + self.defaults = {} self.defaults["reboot"] = False self.defaults["stage"] = True @@ -136,6 +141,8 @@ def _init_defaults(self): self.defaults["options"]["package"]["uninstall"] = False def _init_properties(self): + self.method_name = inspect.stack()[0][3] + # self.ip_addresses is used in: # self._wait_for_current_actions_to_complete() # self._wait_for_image_upgrade_to_complete() @@ -210,12 +217,14 @@ def validate_devices(self): """ Fail if the upgrade state for any device is Failed. """ + self.method_name = inspect.stack()[0][3] + for device in self.devices: self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() if self.issu_detail.upgrade == "Failed": - msg = f"{self.class_name}.validate_devices: Image upgrade is " - msg += "failing for the following switch: " + msg = f"{self.class_name}.{self.method_name}: " + msg += "Image upgrade is failing for the following switch: " msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " msg += f"{self.issu_detail.serial_number}. " @@ -226,6 +235,8 @@ def validate_devices(self): self.ip_addresses.add(self.issu_detail.ip_address) def _merge_defaults_to_switch_config(self, config): + self.method_name = inspect.stack()[0][3] + if config.get("stage") is None: config["stage"] = self.defaults["stage"] if config.get("reboot") is None: @@ -270,14 +281,17 @@ def build_payload(self, device): """ Build the request payload to upgrade the switches. """ - msg = f"REMOVE: {self.class_name}.build_payload() PRE_DEFAULTS: " - msg += f"device: {device}" + self.method_name = inspect.stack()[0][3] + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f" PRE_DEFAULTS: device: {device}" self.log_msg(msg) + device = self._merge_defaults_to_switch_config(device) - msg = f"REMOVE: {self.class_name}.build_payload() POST_DEFAULTS: " - msg += f"device: {device}" - self.log_msg(msg) + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"POST_DEFAULTS: device: {device}" + self.log_msg(msg) # devices_to_upgrade must currently be a single device devices_to_upgrade = [] @@ -302,9 +316,11 @@ def build_payload(self, device): nxos_mode = device.get("options").get("nxos").get("mode") if nxos_mode not in self.valid_nxos_mode: - msg = f"{self.class_name}.build_payload() options.nxos.mode must " - msg += f"be one of {self.valid_nxos_mode}. Got {nxos_mode}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.nxos.mode must be one of " + msg += f"{self.valid_nxos_mode}. Got {nxos_mode}." self.module.fail_json(msg) + if nxos_mode == "non_disruptive": self.payload["issuUpgradeOptions1"]["nonDisruptive"] = True if nxos_mode == "disruptive": @@ -314,24 +330,32 @@ def build_payload(self, device): # biosForce corresponds to BIOS Force GUI option bios_force = device.get("options").get("nxos").get("bios_force") + if not isinstance(bios_force, bool): - msg = f"{self.class_name}.build_payload() options.nxos.bios_force " - msg += f"must be a boolean. Got {bios_force}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.nxos.bios_force must be a boolean. " + msg += f"Got {bios_force}." self.module.fail_json(msg) + self.payload["issuUpgradeOptions2"] = {} self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force # EPLD epld_module = device.get("options").get("epld").get("module") epld_golden = device.get("options").get("epld").get("golden") + if epld_module not in self.valid_epld_module: - msg = f"{self.class_name}.build_payload() options.epld.module must " - msg += f"be one of {self.valid_epld_module}. Got {epld_module}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.epld.module must be one of " + msg += f"{self.valid_epld_module}. Got {epld_module}." self.module.fail_json(msg) + if not isinstance(epld_golden, bool): - msg = f"{self.class_name}.build_payload() options.epld.golden " - msg += f"must be a boolean. Got {epld_golden}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.epld.golden must be a boolean. " + msg += f"Got {epld_golden}." self.module.fail_json(msg) + self.payload["epldUpgrade"] = device.get("upgrade").get("epld") self.payload["epldOptions"] = {} self.payload["epldOptions"]["moduleNumber"] = epld_module @@ -339,23 +363,30 @@ def build_payload(self, device): # Reboot reboot = device.get("reboot") + if not isinstance(reboot, bool): - msg = f"{self.class_name}.build_payload() reboot must " - msg += f"be a boolean. Got {reboot}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "reboot must be a boolean. " + msg += f"Got {reboot}." self.module.fail_json(msg) self.payload["reboot"] = reboot # Reboot options config_reload = device.get("options").get("reboot").get("config_reload") write_erase = device.get("options").get("reboot").get("write_erase") + if not isinstance(config_reload, bool): - msg = f"{self.class_name}.build_payload() options.reboot.config_reload " - msg += f"must be a boolean. Got {config_reload}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.reboot.config_reload must be a boolean. " + msg += f"Got {config_reload}." self.module.fail_json(msg) + if not isinstance(write_erase, bool): - msg = f"{self.class_name}.build_payload() options.reboot.write_erase " - msg += f"must be a boolean. Got {write_erase}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.reboot.write_erase must be a boolean. " + msg += f"Got {write_erase}." self.module.fail_json(msg) + self.payload["rebootOptions"] = {} self.payload["rebootOptions"]["configReload"] = config_reload self.payload["rebootOptions"]["writeErase"] = write_erase @@ -363,14 +394,19 @@ def build_payload(self, device): # Packages package_install = device.get("options").get("package").get("install") package_uninstall = device.get("options").get("package").get("uninstall") + if not isinstance(package_install, bool): - msg = f"{self.class_name}.build_payload() options.package.install " - msg += f"must be a boolean. Got {package_install}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.package.install must be a boolean. " + msg += f"Got {package_install}." self.module.fail_json(msg) + if not isinstance(package_uninstall, bool): - msg = f"{self.class_name}.build_payload() options.package.uninstall " - msg += f"must be a boolean. Got {package_uninstall}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.package.uninstall must be a boolean. " + msg += f"Got {package_uninstall}." self.module.fail_json(msg) + self.payload["pacakgeInstall"] = package_install self.payload["pacakgeUnInstall"] = package_uninstall @@ -379,34 +415,46 @@ def commit(self): Commit the image upgrade request to the controller and wait for the images to be upgraded. """ + self.method_name = inspect.stack()[0][3] + if self.devices is None: - msg = f"{self.class_name}.commit() call instance.devices " - msg += "before calling commit()." + msg = f"{self.class_name}.{self.method_name}: " + msg += "call instance.devices before calling commit." self.module.fail_json(msg) - if len(self.devices) == 0: - msg = f"REMOVE: {self.class_name}.commit() no devices to upgrade." - self.log_msg(msg) - return + #self.prune_devices() self.validate_devices() self._wait_for_current_actions_to_complete() - path = self.endpoints.image_upgrade.get("path") - verb = self.endpoints.image_upgrade.get("verb") + + self.path = self.endpoints.image_upgrade.get("path") + self.verb = self.endpoints.image_upgrade.get("verb") + for device in self.devices: self.build_payload(device) - self.log_msg(f"REMOVE: {self.class_name}.commit() upgrade payload: {self.payload}") + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"upgrade payload: {self.payload}" + self.log_msg(msg) + self.properties["response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) + self.module, self.verb, self.path, data=json.dumps(self.payload) ) - self.properties["result"] = self._handle_response(self.response, verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.response}" - ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + self.properties["result"] = self._handle_response(self.response, self.verb) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"response: {self.response}" + self.log_msg(msg) + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " + msg += f"result: {self.result}" + self.log_msg(msg) + if not self.result["success"]: - msg = f"{self.class_name}.commit() failed: {self.result}. " + msg = f"{self.class_name}.{self.method_name}: " + msg += f"failed: {self.result}. " msg += f"Controller response: {self.response}" self.module.fail_json(msg) + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_upgrade_to_complete() @@ -416,53 +464,65 @@ def _wait_for_current_actions_to_complete(self): in progress. Wait for all actions to complete before upgrading image. Actions include image staging, image upgrade, and image validation. """ - ipv4_todo = copy.copy(self.ip_addresses) + self.method_name = inspect.stack()[0][3] + + self.ipv4_todo = copy.copy(self.ip_addresses) + self.ipv4_done = set() timeout = self.check_timeout - while len(ipv4_todo) > 0 and timeout > 0: + + while self.ipv4_done != self.ipv4_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + for ipv4 in self.ip_addresses: - if ipv4 not in ipv4_todo: + if ipv4 not in self.ipv4_done: continue + self.issu_detail.ip_address = ipv4 self.issu_detail.refresh() + if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{ipv4} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) - ipv4_todo.remove(ipv4) + self.ipv4_done.add(ipv4) continue - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"{ipv4} actions in progress. " msg += f"Waiting. {timeout} seconds remaining." self.log_msg(msg) + if self.ipv4_done != self.ipv4_todo: + msg = f"{self.class_name}.{self.method_name}: " + msg += "Timed out while waiting for actions in progress " + msg += "to complete for the following device(s): " + msg += f"{self.ipv4_todo}. " + msg += "Try increasing issu timeout in the playbook, or check " + msg += "the device(s) to determine the cause " + msg += "(e.g. show install all status)." + self.module.fail_json(msg) + def _wait_for_image_upgrade_to_complete(self): """ Wait for image upgrade to complete """ - ipv4_done = set() + self.method_name = inspect.stack()[0][3] + + self.ipv4_todo = set(copy.copy(self.ip_addresses)) + self.ipv4_done = set() timeout = self.check_timeout - ipv4_todo = set(copy.copy(self.ip_addresses)) - while ipv4_done != ipv4_todo and timeout > 0: + + while self.ipv4_done != self.ipv4_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"seconds remaining {timeout}, " - msg += f"ipv4_todo: {sorted(list(ipv4_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " + + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"seconds remaining {timeout}, " - msg += f"ipv4_done: {sorted(list(ipv4_done))}" + msg += f"ipv4_todo: {sorted(list(self.ipv4_todo))}" self.log_msg(msg) + for ipv4 in self.ip_addresses: - if ipv4 in ipv4_done: + if ipv4 in self.ipv4_done: continue + self.issu_detail.ip_address = ipv4 self.issu_detail.refresh() ip_address = self.issu_detail.ip_address @@ -472,33 +532,31 @@ def _wait_for_image_upgrade_to_complete(self): serial_number = self.issu_detail.serial_number if upgrade_status == "Failed": - msg = f"{self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"Seconds remaining {timeout}: upgrade image " msg += f"{upgrade_status} for " msg += f"{device_name}, {serial_number}, {ip_address}" self.module.fail_json(msg) if upgrade_status == "Success": - ipv4_done.add(ipv4) + self.ipv4_done.add(ipv4) status = "succeeded" if upgrade_status == None: status = "not started" if upgrade_status == "In-Progress": status = "in progress" - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " + msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"Seconds remaining {timeout}, " msg += f"Percent complete {upgrade_percent}, " msg += f"Status {status}, " msg += f"{device_name}, {serial_number}, {ip_address}" self.log_msg(msg) - if ipv4_done != ipv4_todo: - msg = f"{self.class_name}._wait_for_image_upgrade_to_complete(): " + if self.ipv4_done != self.ipv4_todo: + msg = f"{self.class_name}.{self.method_name}: " msg += "The following device(s) did not complete upgrade: " - msg += f"{ipv4_todo.difference(ipv4_done)}. " + msg += f"{self.ipv4_todo.difference(self.ipv4_done)}. " msg += "Try increasing issu timeout in the playbook, or check " msg += "the device(s) to determine the cause " msg += "(e.g. show install all status)." @@ -516,12 +574,12 @@ def bios_force(self): @bios_force.setter def bios_force(self, value): - name = "bios_force" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.bios_force must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["bios_force"] = value @property def config_reload(self): @@ -534,12 +592,12 @@ def config_reload(self): @config_reload.setter def config_reload(self, value): - name = "config_reload" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.config_reload must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["config_reload"] = value @property def devices(self): @@ -558,12 +616,12 @@ def devices(self): @devices.setter def devices(self, value): - name = "devices" + self.method_name = inspect.stack()[0][3] if not isinstance(value, list): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a python list of dict." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.devices must be a python list of dict." self.module.fail_json(msg) - self.properties[name] = value + self.properties["devices"] = value @property def disruptive(self): @@ -576,12 +634,12 @@ def disruptive(self): @disruptive.setter def disruptive(self, value): - name = "disruptive" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.disruptive must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["disruptive"] = value @property def epld_golden(self): @@ -594,12 +652,12 @@ def epld_golden(self): @epld_golden.setter def epld_golden(self, value): - name = "epld_golden" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.epld_golden must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["epld_golden"] = value @property def epld_upgrade(self): @@ -612,12 +670,12 @@ def epld_upgrade(self): @epld_upgrade.setter def epld_upgrade(self, value): - name = "epld_upgrade" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.epld_upgrade must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["epld_upgrade"] = value @property def epld_module(self): @@ -632,16 +690,16 @@ def epld_module(self): @epld_module.setter def epld_module(self, value): - name = "epld_module" + self.method_name = inspect.stack()[0][3] try: value = value.upper() except AttributeError: pass if not isinstance(value, int) and value != "ALL": - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be an integer or 'ALL'" + msg = f"{self.class_name}.{self.method_name}: " + msg += f"instance.epld_module must be an integer or 'ALL'" self.module.fail_json(msg) - self.properties[name] = value + self.properties["epld_module"] = value @property def force_non_disruptive(self): @@ -654,12 +712,12 @@ def force_non_disruptive(self): @force_non_disruptive.setter def force_non_disruptive(self, value): - name = "force_non_disruptive" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.force_non_disruptivemust be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["force_non_disruptive"] = value @property def non_disruptive(self): @@ -672,12 +730,12 @@ def non_disruptive(self): @non_disruptive.setter def non_disruptive(self, value): - name = "non_disruptive" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}.setter: " + msg += "instance.non_disruptive must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["non_disruptive"] = value @property def package_install(self): @@ -690,12 +748,12 @@ def package_install(self): @package_install.setter def package_install(self, value): - name = "package_install" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.package_install must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["package_install"] = value @property def package_uninstall(self): @@ -708,12 +766,12 @@ def package_uninstall(self): @package_uninstall.setter def package_uninstall(self, value): - name = "package_uninstall" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.package_uninstall must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["package_uninstall"] = value @property def reboot(self): @@ -726,12 +784,12 @@ def reboot(self): @reboot.setter def reboot(self, value): - name = "reboot" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.reboot must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["reboot"] = value @property def write_erase(self): @@ -744,12 +802,12 @@ def write_erase(self): @write_erase.setter def write_erase(self, value): - name = "write_erase" + self.method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.write_erase must be a boolean." self.module.fail_json(msg) - self.properties[name] = value + self.properties["write_erase"] = value # getter properties From 4541c72ac0cf5aceb86099dccabbab913af69733 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 15:42:02 -1000 Subject: [PATCH 034/300] cleanup log and fail_json messages --- plugins/modules/dcnm_image_upgrade.py | 337 +++++++++----------------- 1 file changed, 120 insertions(+), 217 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 130570631..d49380ed3 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -25,43 +25,33 @@ from __future__ import absolute_import, division, print_function import copy +import inspect import json +from typing import Any, Dict from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ( - ApiEndpoints, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import ( - ImagePolicies, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import ( - ImagePolicyAction, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import ( - ImageStage, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ( - ImageUpgradeCommon, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import ( - ImageUpgrade, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import ( - ImageValidate, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import ( - ImageInstallOptions, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import ( - SwitchDetails, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import ( - SwitchIssuDetailsByIpAddress, -) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ + ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ + ImagePolicyAction +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ + ImageStage +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ + ImageUpgrade +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ + ImageValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ + ImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ + SwitchDetails +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsByIpAddress from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, - validate_list_of_dicts, -) + dcnm_send, validate_list_of_dicts) __metaclass__ = type __author__ = "Cisco Systems, Inc." @@ -455,23 +445,16 @@ class ImageUpgradeTask(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.method_name = "__init__" + self.method_name = inspect.stack()[0][3] self.params = self.module.params self.class_name = self.__class__.__name__ self.endpoints = ApiEndpoints() - msg = f"REMOVE: {self.class_name}.{self.method_name}: entered" - self.log_msg(msg) - # populated in self._build_policy_attach_payload() self.payloads = [] self.config = module.params.get("config") - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"self.config: {self.config}" - self.log_msg(msg) - if not isinstance(self.config, dict): msg = f"{self.class_name}.{self.method_name}: " msg += "expected dict type for self.config. " @@ -505,7 +488,7 @@ def __init__(self, module): for switch in self.config["switches"]: if not self.mandatory_switch_keys.issubset(switch): msg = f"{self.class_name}.{self.method_name}: " - msg += f"missing mandatory key(s) in playbook switch config. " + msg += "missing mandatory key(s) in playbook switch config. " msg += f"expected {self.mandatory_switch_keys}, " msg += f"got {switch.keys()}" self.module.fail_json(msg) @@ -513,35 +496,33 @@ def __init__(self, module): self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) - def get_have(self): + def get_have(self) -> None: """ Caller: main() Determine current switch ISSU state on NDFC """ - self.method_name = "get_have" + self.method_name = inspect.stack()[0][3] + self.have = SwitchIssuDetailsByIpAddress(self.module) self.have.refresh() - def get_want(self): + def get_want(self) -> None: """ Caller: main() Update self.want_create for all switches defined in the playbook """ - self.method_name = "get_want" + self.method_name = inspect.stack()[0][3] + self._merge_global_and_switch_configs(self.config) self._validate_switch_configs() if not self.switch_configs: return - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"self.switch_configs: {self.switch_configs}" - self.log_msg(msg) - self.want_create = self.switch_configs - def _get_idempotent_want(self, want): + def _build_idempotent_want(self, want) -> None: """ Return an itempotent want item based on the have item contents. @@ -578,11 +559,7 @@ def _get_idempotent_want(self, want): Caller: self.get_need_merged() """ - self.method_name = "_get_idempotent_want" - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"want: {want}" - self.log_msg(msg) + self.method_name = inspect.stack()[0][3] self.have.ip_address = want["ip_address"] @@ -590,68 +567,48 @@ def _get_idempotent_want(self, want): # The switch does not have an image policy attached. # Return the want item as-is with policy_changed = True if self.have.serial_number is None: - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"no serial_number. return want: {want}" - self.log_msg(msg) return want # The switch has an image policy attached which is # different from the want policy. # Return the want item as-is with policy_changed = True if want["policy"] != self.have.policy: - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"different policy attached. return want: {want}" - self.log_msg(msg) return want # start with a copy of the want item - idempotent_want = copy.deepcopy(want) + self.idempotent_want = copy.deepcopy(want) # Give an indication to the caller that the policy has not changed # We can use this later to determine if we need to do anything in # the case where the image is already staged and/or upgraded. - idempotent_want["policy_changed"] = False + self.idempotent_want["policy_changed"] = False # if the image is already staged, don't stage it again if self.have.image_staged == "Success": - idempotent_want["stage"] = False + self.idempotent_want["stage"] = False # if the image is already validated, don't validate it again if self.have.validated == "Success": - idempotent_want["validate"] = False + self.idempotent_want["validate"] = False # if the image is already upgraded, don't upgrade it again if ( self.have.status == "In-Sync" and self.have.reason == "Upgrade" and self.have.policy == want["policy"] ): - idempotent_want["upgrade"]["nxos"] = False + self.idempotent_want["upgrade"]["nxos"] = False - # Get relevant install options from NDFC based on the - # options in our want item + # Get relevant install options from the controller + # based on the options in our want item instance = ImageInstallOptions(self.module) instance.policy_name = want["policy"] - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"calling ImageInstallOptions.refresh() with " - msg += f"serial_number {self.have.serial_number} " - msg += f"ip_address {self.have.ip_address} " - msg += f"device_name {self.have.device_name}" - self.log_msg(msg) - instance.serial_number = self.have.serial_number instance.epld = want["upgrade"]["epld"] instance.issu = want["upgrade"]["nxos"] instance.refresh() if instance.epld_modules is None: - idempotent_want["upgrade"]["epld"] = False + self.idempotent_want["upgrade"]["epld"] = False - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f" return {idempotent_want}" - self.log_msg(msg) - - return idempotent_want - - def get_need_merged(self): + def get_need_merged(self) -> None: """ Caller: main() @@ -659,28 +616,28 @@ def get_need_merged(self): our want list that are not in our have list. These items will be sent to the controller. """ - self.method_name = "get_need_merged" + self.method_name = inspect.stack()[0][3] need = [] for want_create in self.want_create: self.have.ip_address = want_create["ip_address"] if self.have.serial_number is not None: - idempotent_want = self._get_idempotent_want(want_create) + self._build_idempotent_want(want_create) if ( - idempotent_want["policy_changed"] is False - and idempotent_want["stage"] is False - and idempotent_want["upgrade"]["nxos"] is False - and idempotent_want["upgrade"]["epld"] is False + self.idempotent_want["policy_changed"] is False + and self.idempotent_want["stage"] is False + and self.idempotent_want["upgrade"]["nxos"] is False + and self.idempotent_want["upgrade"]["epld"] is False ): continue - need.append(idempotent_want) + need.append(self.idempotent_want) self.need = need msg = f"REMOVE: {self.class_name}.{self.method_name}: " msg += f"need: {self.need}" self.log_msg(msg) - def get_need_deleted(self): + def get_need_deleted(self) -> None: """ Caller: main() @@ -688,7 +645,8 @@ def get_need_deleted(self): list that are not in our have list. These items will be sent to the controller. """ - self.method_name = "get_need_deleted" + self.method_name = inspect.stack()[0][3] + need = [] for want in self.want_create: self.have.ip_address = want["ip_address"] @@ -699,21 +657,22 @@ def get_need_deleted(self): need.append(want) self.need = need - def get_need_query(self): + def get_need_query(self) -> None: """ Caller: main() For query state, populate self.need list() with all items from our want list. These items will be sent to the controller. """ - self.method_name = "get_need_query" + self.method_name = inspect.stack()[0][3] + need = [] for want in self.want_create: need.append(want) self.need = need @staticmethod - def _build_params_spec_for_merged_state(): + def _build_params_spec_for_merged_state() -> Dict[str, Any]: """ Build the specs for the parameters expected when state == merged. @@ -813,13 +772,14 @@ def _build_params_spec_for_merged_state(): return copy.deepcopy(params_spec) - def validate_input(self): + def validate_input(self) -> None: """ Caller: main() Validate the playbook parameters """ - self.method_name = "validate_input" + self.method_name = inspect.stack()[0][3] + state = self.params["state"] if state not in ["merged", "deleted", "query"]: @@ -838,13 +798,13 @@ def validate_input(self): self._validate_input_for_query_state() return - def _validate_input_for_merged_state(self): + def _validate_input_for_merged_state(self) -> None: """ Caller: self.validate_input() Validate that self.config contains appropriate values for merged state """ - self.method_name = "_validate_input_for_merged_state" + self.method_name = inspect.stack()[0][3] if not self.config: msg = f"{self.class_name}.{self.method_name}: " @@ -853,14 +813,6 @@ def _validate_input_for_merged_state(self): params_spec = self._build_params_spec_for_merged_state() - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"params_spec: {params_spec}" - self.log_msg(msg) - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"self.config: {self.config}" - self.log_msg(msg) - valid_params, invalid_params = validate_list_of_dicts( self.config.get("switches"), params_spec, self.module ) @@ -874,7 +826,7 @@ def _validate_input_for_merged_state(self): msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) - def _validate_input_for_deleted_state(self): + def _validate_input_for_deleted_state(self) -> None: """ Caller: self.validate_input() @@ -884,7 +836,7 @@ def _validate_input_for_deleted_state(self): 1. This is currently identical to _validate_input_for_merged_state() 2. Adding in case there are differences in the future """ - self.method_name = "_validate_input_for_deleted_state" + self.method_name = inspect.stack()[0][3] params_spec = self._build_params_spec_for_merged_state() if not self.config: @@ -905,7 +857,7 @@ def _validate_input_for_deleted_state(self): msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) - def _validate_input_for_query_state(self): + def _validate_input_for_query_state(self) -> None: """ Caller: self.validate_input() @@ -915,7 +867,8 @@ def _validate_input_for_query_state(self): 1. This is currently identical to _validate_input_for_merged_state() 2. Adding in case there are differences in the future """ - self.method_name = "_validate_input_for_query_state" + self.method_name = inspect.stack()[0][3] + params_spec = self._build_params_spec_for_merged_state() if not self.config: @@ -936,7 +889,7 @@ def _validate_input_for_query_state(self): msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) - def _merge_global_and_switch_configs(self, config): + def _merge_global_and_switch_configs(self, config) -> None: """ Merge the global config with each switch config and return a dict of switch configs keyed on switch ip_address. @@ -951,17 +904,13 @@ def _merge_global_and_switch_configs(self, config): 5. If global_config and switch_config are both missing a mandatory parameter, fail. """ - self.method_name = "_merge_global_and_switch_configs" + self.method_name = inspect.stack()[0][3] if not config.get("switches"): msg = f"{self.class_name}.{self.method_name}: " msg += "playbook is missing list of switches" self.module.fail_json(msg) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"config: {config}" - self.log_msg(msg) - global_config = {} global_config["policy"] = config.get("policy") global_config["stage"] = config.get("stage") @@ -969,20 +918,12 @@ def _merge_global_and_switch_configs(self, config): global_config["options"] = config.get("options") global_config["validate"] = config.get("validate") - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"global_config: {global_config}" - self.log_msg(msg) - self.switch_configs = [] for switch in config["switches"]: switch_config = global_config.copy() | switch.copy() self.switch_configs.append(switch_config) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"merged switch_config: {switch_config}" - self.log_msg(msg) - - def _validate_switch_configs(self): + def _validate_switch_configs(self) -> None: """ Ensure mandatory parameters are present for each switch - fail_json if this isn't the case @@ -995,12 +936,9 @@ def _validate_switch_configs(self): Callers: - self.get_want """ - self.method_name = "_validate_switch_configs" - for switch in self.switch_configs: - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"switch: {switch}" - self.log_msg(msg) + self.method_name = inspect.stack()[0][3] + for switch in self.switch_configs: if not switch.get("ip_address"): msg = f"{self.class_name}.{self.method_name}: " msg = "playbook is missing ip_address for at least one switch" @@ -1018,7 +956,7 @@ def _validate_switch_configs(self): msg += "and global image policy is not defined." self.module.fail_json(msg) - def _build_policy_attach_payload(self): + def _build_policy_attach_payload(self) -> None: """ Build the payload for the policy attach request Verify that the image policy exists on the controller @@ -1027,13 +965,15 @@ def _build_policy_attach_payload(self): Callers: - self.handle_merged_state """ - self.method_name = "_build_policy_attach_payload" + self.method_name = inspect.stack()[0][3] + self.payloads = [] self.switch_details.refresh() self.image_policies.refresh() for switch in self.need: if switch.get("policy_changed") is False: continue + self.switch_details.ip_address = switch.get("ip_address") self.image_policies.policy_name = switch.get("policy") @@ -1073,30 +1013,35 @@ def _build_policy_attach_payload(self): msg += "Please verify that the switch is managed by " msg += "the controller." self.module.fail_json(msg) + self.payloads.append(payload) - def _send_policy_attach_payload(self): + def _send_policy_attach_payload(self) -> None: """ Send the policy attach payload to NDFC and handle the response Callers: - self.handle_merged_state """ - self.method_name = "_send_policy_attach_payload" + self.method_name = inspect.stack()[0][3] + if len(self.payloads) == 0: return - path = self.endpoints.policy_attach.get("path") - verb = self.endpoints.policy_attach.get("verb") + self.path = self.endpoints.policy_attach.get("path") + self.verb = self.endpoints.policy_attach.get("verb") + payload = {} payload["mappingList"] = self.payloads - response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) - result = self._handle_response(response, verb) + response = dcnm_send( + self.module, self.verb, self.path, data=json.dumps(payload) + ) + result = self._handle_response(response, self.verb) if not result["success"]: self._failure(response) - def _stage_images(self, serial_numbers): + def _stage_images(self, serial_numbers) -> None: """ Initiate image staging to the switch(es) associated with serial_numbers @@ -1104,19 +1049,21 @@ def _stage_images(self, serial_numbers): Callers: - handle_merged_state """ - self.method_name = "_stage_images" + self.method_name = inspect.stack()[0][3] + instance = ImageStage(self.module) instance.serial_numbers = serial_numbers instance.commit() - def _validate_images(self, serial_numbers): + def _validate_images(self, serial_numbers) -> None: """ Validate the image staged to the switch(es) Callers: - handle_merged_state """ - self.method_name = "_validate_images" + self.method_name = inspect.stack()[0][3] + instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers # TODO:2 Discuss with Mike/Shangxin - ImageValidate.non_disruptive @@ -1125,7 +1072,7 @@ def _validate_images(self, serial_numbers): # instance.non_disruptive = False instance.commit() - def _verify_install_options(self, devices): + def _verify_install_options(self, devices) -> None: """ Verify that the install options for the devices(es) are valid @@ -1158,61 +1105,58 @@ def _verify_install_options(self, devices): Callers: - self.handle_merged_state """ - self.method_name = "_verify_install_options" + self.method_name = inspect.stack()[0][3] + if len(devices) == 0: return + install_options = ImageInstallOptions(self.module) self.switch_details.refresh() - for device in devices: - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"device: {device}" - self.log_msg(msg) + for device in devices: self.switch_details.ip_address = device.get("ip_address") install_options.serial_number = self.switch_details.serial_number install_options.policy_name = device["policy"] install_options.epld = device["upgrade"]["epld"] install_options.issu = device["upgrade"]["nxos"] install_options.refresh() + if ( install_options.status not in ["Success", "Skipped"] and device["upgrade"]["nxos"] is True ): msg = f"{self.class_name}.{self.method_name}: " - msg += f"NXOS upgrade is set to True for switch " + msg += "NXOS upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " - msg += f"NX-OS image" + msg += "NX-OS image" self.module.fail_json(msg) + if ( install_options.epld_modules is None and device["upgrade"]["epld"] is True ): msg = f"{self.class_name}.{self.method_name}: " - msg = f"EPLD upgrade is set to True for switch " + msg += "EPLD upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " - msg += f"EPLD image." + msg += "EPLD image." self.module.fail_json(msg) - def _upgrade_images(self, devices): + def _upgrade_images(self, devices) -> None: """ Upgrade the switch(es) to the specified image Callers: - handle_merged_state """ - self.method_name = "_upgrade_images" - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"devices: {devices}" - self.log_msg(msg) + self.method_name = inspect.stack()[0][3] upgrade = ImageUpgrade(self.module) upgrade.devices = devices upgrade.commit() - def handle_merged_state(self): + def handle_merged_state(self) -> None: """ Update the switch policy if it has changed. Stage the image if requested. @@ -1221,38 +1165,25 @@ def handle_merged_state(self): Caller: main() """ - self.method_name = "handle_merged_state" - # TODO:1 Replace these with ImagePolicyAction - # See commented code below + self.method_name = inspect.stack()[0][3] + self._build_policy_attach_payload() self._send_policy_attach_payload() - # Use (or not) below for policy attach/detach - # instance = ImagePolicyAction(self.module) - # instance.policy_name = "NR3F" - # instance.action = "attach" # or detach - # instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] - # instance.commit() - # policy_attach_devices = [] - # policy_detach_devices = [] - stage_devices = [] validate_devices = [] upgrade_devices = [] self.switch_details.refresh() - for switch in self.need: - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"switch: {switch}" - self.log_msg(msg) + for switch in self.need: self.switch_details.ip_address = switch.get("ip_address") device = {} device["serial_number"] = self.switch_details.serial_number self.have.ip_address = self.switch_details.ip_address device["policy_name"] = switch.get("policy") device["ip_address"] = self.switch_details.ip_address + if switch.get("stage") is not False: stage_devices.append(device["serial_number"]) if switch.get("validate") is not False: @@ -1263,81 +1194,57 @@ def handle_merged_state(self): ): upgrade_devices.append(switch) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"stage_devices: {stage_devices}" - self.log_msg(msg) self._stage_images(stage_devices) - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"validate_devices: {validate_devices}" - self.log_msg(msg) self._validate_images(validate_devices) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"upgrade_devices: {upgrade_devices}" - self.log_msg(msg) - self._verify_install_options(upgrade_devices) self._upgrade_images(upgrade_devices) - def handle_deleted_state(self): + def handle_deleted_state(self) -> None: """ Delete the image policy from the switch(es) Caller: main() """ - self.method_name = "handle_deleted_state" - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"Entered with self.need {self.need}" - self.log_msg(msg) + self.method_name = inspect.stack()[0][3] detach_policy_devices = {} + self.switch_details.refresh() self.image_policies.refresh() + for switch in self.need: self.switch_details.ip_address = switch.get("ip_address") self.image_policies.policy_name = switch.get("policy") - # if self.image_policies.name is None: - # continue + if self.image_policies.name not in detach_policy_devices: detach_policy_devices[self.image_policies.policy_name] = [] detach_policy_devices[self.image_policies.policy_name].append( self.switch_details.serial_number ) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"detach_policy_devices: {detach_policy_devices}" - self.log_msg(msg) if len(detach_policy_devices) == 0: self.result = dict(changed=False, diff=[], response=[]) return + instance = ImagePolicyAction(self.module) for policy_name in detach_policy_devices: - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"detach policy_name: {policy_name}" - msg += f" from devices: {detach_policy_devices[policy_name]}" - self.log_msg(msg) - instance.policy_name = policy_name instance.action = "detach" instance.serial_numbers = detach_policy_devices[policy_name] instance.commit() - def handle_query_state(self): + def handle_query_state(self) -> None: """ Return the ISSU state of the switch(es) listed in the playbook Caller: main() """ - self.method_name = "handle_query_state" + self.method_name = inspect.stack()[0][3] + instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"Entered. self.need {self.need}" - self.log_msg(msg) - query_devices = [] for switch in self.need: instance.ip_address = switch.get("ip_address") @@ -1345,15 +1252,11 @@ def handle_query_state(self): continue query_devices.append(instance.filtered_data) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"query_policies: {query_devices}" - self.log_msg(msg) - self.result["response"] = query_devices self.result["diff"] = [] self.result["changed"] = False - def _failure(self, resp): + def _failure(self, resp) -> None: """ Caller: self.attach_policies() From 4b6cac2ad3c469193fecc07d7a4d4782a9730b23 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 17:38:50 -1000 Subject: [PATCH 035/300] cleanup log and fail_json messages + black/isort --- .../module_utils/image_mgmt/image_upgrade.py | 98 ++++++++----------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 9fa915b6e..7821240e2 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -2,13 +2,17 @@ import inspect import json from time import sleep +from typing import Any, Dict + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsByIpAddress +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsByIpAddress class ImageUpgrade(ImageUpgradeCommon): """ @@ -33,7 +37,7 @@ class ImageUpgrade(ImageUpgradeCommon): 'stage': False, 'validate': True, 'upgrade': { - 'nxos': True, + 'nxos': True, 'epld': False }, 'options': { @@ -180,7 +184,6 @@ def _init_properties(self): self.valid_nxos_mode.add("non_disruptive") self.valid_nxos_mode.add("force_non_disruptive") - # def prune_devices(self): # """ # If the image is already upgraded on a device, remove that device @@ -213,7 +216,7 @@ def _init_properties(self): # if device.get("ip_address") not in pruned_devices # ] - def validate_devices(self): + def validate_devices(self) -> None: """ Fail if the upgrade state for any device is Failed. """ @@ -222,6 +225,7 @@ def validate_devices(self): for device in self.devices: self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() + if self.issu_detail.upgrade == "Failed": msg = f"{self.class_name}.{self.method_name}: " msg += "Image upgrade is failing for the following switch: " @@ -231,10 +235,11 @@ def validate_devices(self): msg += "Please check the switch " msg += "to determine the cause and try again." self.module.fail_json(msg) + # used in self._wait_for_current_actions_to_complete() self.ip_addresses.add(self.issu_detail.ip_address) - def _merge_defaults_to_switch_config(self, config): + def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: self.method_name = inspect.stack()[0][3] if config.get("stage") is None: @@ -256,43 +261,49 @@ def _merge_defaults_to_switch_config(self, config): if config["options"]["nxos"].get("mode") is None: config["options"]["nxos"]["mode"] = self.defaults["options"]["nxos"]["mode"] if config["options"]["nxos"].get("bios_force") is None: - config["options"]["nxos"]["bios_force"] = self.defaults["options"]["nxos"]["bios_force"] + config["options"]["nxos"]["bios_force"] = self.defaults["options"]["nxos"][ + "bios_force" + ] if config["options"].get("epld") is None: config["options"]["epld"] = self.defaults["options"]["epld"] if config["options"]["epld"].get("module") is None: - config["options"]["epld"]["module"] = self.defaults["options"]["epld"]["module"] + config["options"]["epld"]["module"] = self.defaults["options"]["epld"][ + "module" + ] if config["options"]["epld"].get("golden") is None: - config["options"]["epld"]["golden"] = self.defaults["options"]["epld"]["golden"] + config["options"]["epld"]["golden"] = self.defaults["options"]["epld"][ + "golden" + ] if config["options"].get("reboot") is None: config["options"]["reboot"] = self.defaults["options"]["reboot"] if config["options"]["reboot"].get("config_reload") is None: - config["options"]["reboot"]["config_reload"] = self.defaults["options"]["reboot"]["config_reload"] + config["options"]["reboot"]["config_reload"] = self.defaults["options"][ + "reboot" + ]["config_reload"] if config["options"]["reboot"].get("write_erase") is None: - config["options"]["reboot"]["write_erase"] = self.defaults["options"]["reboot"]["write_erase"] + config["options"]["reboot"]["write_erase"] = self.defaults["options"][ + "reboot" + ]["write_erase"] if config["options"].get("package") is None: config["options"]["package"] = self.defaults["options"]["package"] if config["options"]["package"].get("install") is None: - config["options"]["package"]["install"] = self.defaults["options"]["package"]["install"] + config["options"]["package"]["install"] = self.defaults["options"][ + "package" + ]["install"] if config["options"]["package"].get("uninstall") is None: - config["options"]["package"]["uninstall"] = self.defaults["options"]["package"]["uninstall"] + config["options"]["package"]["uninstall"] = self.defaults["options"][ + "package" + ]["uninstall"] return config - def build_payload(self, device): + def build_payload(self, device) -> None: """ Build the request payload to upgrade the switches. """ self.method_name = inspect.stack()[0][3] - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f" PRE_DEFAULTS: device: {device}" - self.log_msg(msg) - device = self._merge_defaults_to_switch_config(device) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"POST_DEFAULTS: device: {device}" - self.log_msg(msg) - # devices_to_upgrade must currently be a single device devices_to_upgrade = [] self.issu_detail.ip_address = device.get("ip_address") @@ -410,7 +421,7 @@ def build_payload(self, device): self.payload["pacakgeInstall"] = package_install self.payload["pacakgeUnInstall"] = package_uninstall - def commit(self): + def commit(self) -> None: """ Commit the image upgrade request to the controller and wait for the images to be upgraded. @@ -422,7 +433,6 @@ def commit(self): msg += "call instance.devices before calling commit." self.module.fail_json(msg) - #self.prune_devices() self.validate_devices() self._wait_for_current_actions_to_complete() @@ -432,23 +442,11 @@ def commit(self): for device in self.devices: self.build_payload(device) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"upgrade payload: {self.payload}" - self.log_msg(msg) - self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) self.properties["result"] = self._handle_response(self.response, self.verb) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"response: {self.response}" - self.log_msg(msg) - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"result: {self.result}" - self.log_msg(msg) - if not self.result["success"]: msg = f"{self.class_name}.{self.method_name}: " msg += f"failed: {self.result}. " @@ -475,7 +473,7 @@ def _wait_for_current_actions_to_complete(self): timeout -= self.check_interval for ipv4 in self.ip_addresses: - if ipv4 not in self.ipv4_done: + if ipv4 in self.ipv4_done: continue self.issu_detail.ip_address = ipv4 @@ -485,11 +483,6 @@ def _wait_for_current_actions_to_complete(self): self.ipv4_done.add(ipv4) continue - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"{ipv4} actions in progress. " - msg += f"Waiting. {timeout} seconds remaining." - self.log_msg(msg) - if self.ipv4_done != self.ipv4_todo: msg = f"{self.class_name}.{self.method_name}: " msg += "Timed out while waiting for actions in progress " @@ -514,11 +507,6 @@ def _wait_for_image_upgrade_to_complete(self): sleep(self.check_interval) timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"seconds remaining {timeout}, " - msg += f"ipv4_todo: {sorted(list(self.ipv4_todo))}" - self.log_msg(msg) - for ipv4 in self.ip_addresses: if ipv4 in self.ipv4_done: continue @@ -546,13 +534,6 @@ def _wait_for_image_upgrade_to_complete(self): if upgrade_status == "In-Progress": status = "in progress" - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"Seconds remaining {timeout}, " - msg += f"Percent complete {upgrade_percent}, " - msg += f"Status {status}, " - msg += f"{device_name}, {serial_number}, {ip_address}" - self.log_msg(msg) - if self.ipv4_done != self.ipv4_todo: msg = f"{self.class_name}.{self.method_name}: " msg += "The following device(s) did not complete upgrade: " @@ -809,7 +790,6 @@ def write_erase(self, value): self.module.fail_json(msg) self.properties["write_erase"] = value - # getter properties @property def check_interval(self): From 021c88efbb904c84ac09d210d7b7a8265a12d743 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 2 Nov 2023 18:03:21 -1000 Subject: [PATCH 036/300] cleanup log and fail_json messages --- .../module_utils/image_mgmt/image_validate.py | 203 ++++++++---------- 1 file changed, 94 insertions(+), 109 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index ca52e62de..596efeada 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -1,12 +1,18 @@ import copy +import inspect import json from time import sleep -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber +from typing import Any, Dict + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send + class ImageValidate(ImageUpgradeCommon): """ @@ -44,11 +50,15 @@ class ImageValidate(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ + self.method_name = inspect.stack()[0][3] + self.endpoints = ApiEndpoints() self._init_properties() self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) - def _init_properties(self): + def _init_properties(self) -> None: + self.method_name = inspect.stack()[0][3] + self.properties = {} self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds @@ -58,169 +68,155 @@ def _init_properties(self): self.properties["non_disruptive"] = False self.properties["serial_numbers"] = None - def prune_serial_numbers(self): + def prune_serial_numbers(self) -> None: """ If the image is already validated on a switch, remove that switch's serial number from the list of serial numbers to validate. """ + self.method_name = inspect.stack()[0][3] + serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: self.issu_detail.serial_number = serial_number self.issu_detail.refresh() if self.issu_detail.validated == "Success": - msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " - msg += "image already validated for " - msg += f"{self.issu_detail.serial_number}, " - msg += f"{self.issu_detail.ip_address}" - self.log_msg(msg) self.serial_numbers.remove(self.issu_detail.serial_number) - def validate_serial_numbers(self): + def validate_serial_numbers(self) -> None: """ Log a warning if the validated state for any serial_number is Failed. - TODO:1 Need a way to compare current image_policy with the image policy in the response - TODO:3 If validate == Failed, it may have been from the last operation. - TODO:3 We can't fail here based on this until we can verify the failure is happening for the current image_policy. - TODO:3 Change this to a log message and update the unit test if we can't verify the failure is happening for the current image_policy. + TODO:1 Need a way to compare current image_policy with the image + policy in the response + TODO:3 If validate == Failed, it may have been from the last operation. + TODO:3 We can't fail here based on this until we can verify the failure + is happening for the current image_policy. + TODO:3 Change this to a log message and update the unit test if we can't + verify the failure is happening for the current image_policy. """ + self.method_name = inspect.stack()[0][3] + for serial_number in self.serial_numbers: self.issu_detail.serial_number = serial_number self.issu_detail.refresh() if self.issu_detail.validated == "Failed": - msg = f"{self.class_name}.validate_serial_numbers: " + msg = f"{self.class_name}.{self.method_name}: " msg += "image validation is failing for the following switch: " msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " msg += f"{self.issu_detail.serial_number}. " msg += "If this persists, check the switch connectivity to " msg += "the controller and try again." - #self.log_msg(msg) self.module.fail_json(msg) - def build_payload(self): + def build_payload(self) -> None: + self.method_name = inspect.stack()[0][3] + self.payload = {} self.payload["serialNum"] = self.serial_numbers self.payload["nonDisruptive"] = self.non_disruptive - def commit(self): + def commit(self) -> None: """ Commit the image validation request to the controller and wait for the images to be validated. """ + self.method_name = inspect.stack()[0][3] + if self.serial_numbers is None: - msg = f"{self.class_name}.commit() call instance.serial_numbers " - msg += "before calling commit()." + msg = f"{self.class_name}.{self.method_name}: " + msg += "call instance.serial_numbers before " + msg += "calling commit." self.module.fail_json(msg) + if len(self.serial_numbers) == 0: - msg = f"REMOVE: {self.class_name}.commit() no serial numbers " - msg += "to validate." - self.log_msg(msg) return + self.prune_serial_numbers() self.validate_serial_numbers() self._wait_for_current_actions_to_complete() - path = self.endpoints.image_validate.get("path") - verb = self.endpoints.image_validate.get("verb") + + self.path = self.endpoints.image_validate.get("path") + self.verb = self.endpoints.image_validate.get("verb") + self.build_payload() self.properties["response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) - ) - self.properties["result"] = self._handle_response(self.response, verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.response}" + self.module, self.verb, self.path, data=json.dumps(self.payload) ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") + self.properties["result"] = self._handle_response(self.response, self.verb) + if not self.result["success"]: - msg = f"{self.class_name}.commit() failed: {self.result}. " + msg = f"{self.class_name}.{self.method_name}: " + msg = f"failed: {self.result}. " msg += f"Controller response: {self.response}" self.module.fail_json(msg) + self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_validate_to_complete() - def _wait_for_current_actions_to_complete(self): + def _wait_for_current_actions_to_complete(self) -> None: """ The controller will not validate an image if there are any actions in progress. Wait for all actions to complete before validating image. Actions include image staging, image upgrade, and image validation. """ + self.method_name = inspect.stack()[0][3] + self.serial_numbers_done = set() serial_numbers_todo = set(copy.copy(self.serial_numbers)) timeout = self.check_timeout + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue self.issu_detail.serial_number = serial_number self.issu_detail.refresh() - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} actions in progress: " - msg += f"{self.issu_detail.actions_in_progress}, " - msg += f"{timeout} seconds remaining." - self.log_msg(msg) + if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) self.serial_numbers_done.add(serial_number) + if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_current_actions_to_complete: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"Timed out waiting for actions to complete. " msg += f"serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += f"serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) self.module.fail_json(msg) - def _wait_for_image_validate_to_complete(self): + def _wait_for_image_validate_to_complete(self) -> None: """ Wait for image validation to complete """ - # We're promiting serial_numbers_done to a class-level attribute - # so that it can be used in unit test asserts. + self.method_name = inspect.stack()[0][3] + self.serial_numbers_done = set() timeout = self.check_timeout serial_numbers_todo = set(copy.copy(self.serial_numbers)) + while self.serial_numbers_done != serial_numbers_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" - self.log_msg(msg) + for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue + self.issu_detail.serial_number = serial_number self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name validated_percent = self.issu_detail.validated_percent validated_status = self.issu_detail.validated - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"validated_percent: {validated_percent} " - msg += f"validated_state: {validated_status}" - self.log_msg(msg) - if validated_status == "Failed": + msg = f"{self.class_name}.{self.method_name}: " msg = f"Seconds remaining {timeout}: validate image " msg += f"{validated_status} for " msg += f"{device_name}, {ip_address}, {serial_number}, " @@ -233,41 +229,15 @@ def _wait_for_image_validate_to_complete(self): self.module.fail_json(msg) if validated_status == "Success": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += f"{validated_status} for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) self.serial_numbers_done.add(serial_number) - if validated_status == None: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += "not started for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) - - if validated_status == "In Progress": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += f"{validated_status} for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_image_validate_to_complete: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"Timed out waiting for image validation to complete. " msg += f"serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += f"serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) self.module.fail_json(msg) @property @@ -281,10 +251,15 @@ def serial_numbers(self): @serial_numbers.setter def serial_numbers(self, value): + self.method_name = inspect.stack()[0][3] + if not isinstance(value, list): - msg = f"{self.__class__.__name__}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.serial_numbers must be a " + msg += "python list of switch serial numbers. " + msg += f"Got {value}." self.module.fail_json(msg) + self.properties["serial_numbers"] = value @property @@ -296,12 +271,15 @@ def non_disruptive(self): @non_disruptive.setter def non_disruptive(self, value): + self.method_name = inspect.stack()[0][3] + value = self.make_boolean(value) if not isinstance(value, bool): - msg = f"{self.class_name}.non_disruptive: " - msg += "instance.non_disruptive must " - msg += f"be a boolean. Got {value}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.non_disruptive must be a boolean. " + msg += f"Got {value}." self.module.fail_json(msg) + self.properties["non_disruptive"] = value @property @@ -337,10 +315,14 @@ def check_interval(self): @check_interval.setter def check_interval(self, value): + self.method_name = inspect.stack()[0][3] + if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_interval must " - msg += f"be an integer." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.check_interval must be an integer. " + msg += f"Got {value}." self.module.fail_json(msg) + self.properties["check_interval"] = value @property @@ -352,9 +334,12 @@ def check_timeout(self): @check_timeout.setter def check_timeout(self, value): + self.method_name = inspect.stack()[0][3] + if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_timeout must " - msg += f"be an integer." + msg = f"{self.class_name}.{self.method_name}: " + msg += "instance.check_timeout must be an integer. " + msg += f"Got {value}." self.module.fail_json(msg) - self.properties["check_timeout"] = value + self.properties["check_timeout"] = value From da6e366c5d58989481280d5b1a4004b9c8449bfa Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 3 Nov 2023 06:10:53 -1000 Subject: [PATCH 037/300] _get() Run value through make_boolean() and make_none() --- .../module_utils/image_mgmt/install_options.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 705b5c381..fec3a6f89 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -197,7 +197,11 @@ def _build_payload(self): self.payload["packageInstall"] = self.package_install def _get(self, item): - return self.data.get(item) + return self.make_boolean( + self.make_none( + self.data.get(item) + ) + ) # Mandatory properties @property @@ -307,10 +311,10 @@ def epld_modules(self): if it exists. Return None otherwise - epldModules will be "null" if self.epld is False, so - make_none() will convert to NoneType in this case. + epldModules will be "null" if self.epld is False. + _get will convert to NoneType in this case. """ - return self.make_none(self._get("epldModules")) + return self._get("epldModules") @property def err_message(self): @@ -318,9 +322,6 @@ def err_message(self): Return the errMessage of the install-options response, if it exists. Return None otherwise - - epldModules will be "null" if self.epld is False, so - make_none() will convert to NoneType in this case. """ return self._get("errMessage") @@ -345,7 +346,7 @@ def install_packages(self): 12.1.2e 12.1.3b """ - return self.make_none(self._get("installPacakges")) + return self._get("installPacakges") @property def ip_address(self): From c507ca38e2f4da6c934251102a62fa961beede7a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 3 Nov 2023 13:01:16 -1000 Subject: [PATCH 038/300] Add type hints/annotations and update test expectations --- .../module_utils/image_mgmt/image_validate.py | 20 +++++++++---------- .../test_image_upgrade_ImageValidate.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 596efeada..1a3464d93 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -2,7 +2,7 @@ import inspect import json from time import sleep -from typing import Any, Dict +from typing import Any, Dict, List, Set from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -59,14 +59,14 @@ def __init__(self, module): def _init_properties(self) -> None: self.method_name = inspect.stack()[0][3] - self.properties = {} + self.properties:Dict[str, Any] = {} self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds - self.properties["response_data"] = None - self.properties["result"] = None - self.properties["response"] = None + self.properties["response_data"] = {} + self.properties["result"] = {} + self.properties["response"] = {} self.properties["non_disruptive"] = False - self.properties["serial_numbers"] = None + self.properties["serial_numbers"] = [] def prune_serial_numbers(self) -> None: """ @@ -163,7 +163,7 @@ def _wait_for_current_actions_to_complete(self) -> None: """ self.method_name = inspect.stack()[0][3] - self.serial_numbers_done = set() + self.serial_numbers_done: Set[str] = set() serial_numbers_todo = set(copy.copy(self.serial_numbers)) timeout = self.check_timeout @@ -241,16 +241,16 @@ def _wait_for_image_validate_to_complete(self) -> None: self.module.fail_json(msg) @property - def serial_numbers(self): + def serial_numbers(self) -> List[str]: """ Set the serial numbers of the switches to stage. This must be set before calling instance.commit() """ - return self.properties.get("serial_numbers") + return self.properties.get("serial_numbers", []) @serial_numbers.setter - def serial_numbers(self, value): + def serial_numbers(self, value: List[str]): self.method_name = inspect.stack()[0][3] if not isinstance(value, list): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index 63c7e04ba..2dd5b365f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -53,11 +53,11 @@ def test_init_properties(module) -> None: assert isinstance(module.properties, dict) assert module.properties.get("check_interval") == 10 assert module.properties.get("check_timeout") == 1800 - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None + assert module.properties.get("response_data") == {} + assert module.properties.get("response") == {} + assert module.properties.get("result") == {} assert module.properties.get("non_disruptive") == False - assert module.properties.get("serial_numbers") == None + assert module.properties.get("serial_numbers") == [] def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: From 5e935ce92a0f49a04e4187603471eed70a83d627 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 3 Nov 2023 13:02:54 -1000 Subject: [PATCH 039/300] use inspect to retrieve method names for fail_json messages --- .../module_utils/image_mgmt/switch_details.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index c95392709..2f9ac791b 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -1,3 +1,5 @@ +import inspect + from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) @@ -26,11 +28,15 @@ class SwitchDetails(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) + self.method_name = inspect.stack()[0][3] + self.class_name = self.__class__.__name__ self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): + self.method_name = inspect.stack()[0][3] + self.properties = {} self.properties["ip_address"] = None self.properties["response_data"] = None @@ -44,19 +50,17 @@ def refresh(self): Refresh switch_details with current switch details from the controller. """ + self.method_name = inspect.stack()[0][3] + path = self.endpoints.switches_info.get("path") verb = self.endpoints.switches_info.get("verb") - self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") - self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") + self.properties["response"] = dcnm_send(self.module, verb, path) - msg = f"REMOVE: {self.class_name}.refresh: self.response {self.response}" - self.log_msg(msg) self.properties["result"] = self._handle_response(self.response, verb) - msg = f"REMOVE: {self.class_name}.refresh: self.result {self.result}" - self.log_msg(msg) if self.response["RETURN_CODE"] != 200: - msg = "Unable to retrieve switch information from the controller. " + msg = f"{self.class_name}.{self.method_name}: " + msg += "Unable to retrieve switch information from the controller. " msg += f"Got response {self.response}" self.module.fail_json(msg) @@ -65,22 +69,26 @@ def refresh(self): for switch in data: self.properties["response_data"][switch["ipAddress"]] = switch - msg = f"REMOVE: {self.class_name}.refresh: self.response_data {self.response_data}" - self.log_msg(msg) def _get(self, item): + self.method_name = inspect.stack()[0][3] + if self.ip_address is None: - msg = f"{self.class_name}._get: set instance.ip_address " - msg += f"before accessing property {item}." + msg = f"{self.class_name}.{self.method_name}: " + msg += "set instance.ip_address before accessing " + msg += f"property {item}." self.module.fail_json(msg) + if self.properties["response_data"].get(self.ip_address) is None: - msg = f"{self.class_name}._get: {self.ip_address} does not exist " - msg += f"on the controller." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.ip_address} does not exist on the controller." self.module.fail_json(msg) + if self.properties["response_data"][self.ip_address].get(item) is None: - msg = f"{self.class_name}._get: {self.ip_address} does not have a key" - msg += f" named {item}." + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.ip_address} does not have a key named {item}." self.module.fail_json(msg) + return self.make_boolean( self.make_none( self.properties["response_data"][self.ip_address].get(item) From 1f81fb6159d41930170d6f3089d5854654659d93 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 3 Nov 2023 13:04:46 -1000 Subject: [PATCH 040/300] Add type hints/annotations and use inspect for method names --- .../image_mgmt/install_options.py | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index fec3a6f89..e93ae8a1f 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -1,5 +1,8 @@ -from time import sleep +import inspect import json +from time import sleep +from typing import Dict, Any + from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) @@ -113,7 +116,7 @@ class ImageInstallOptions(ImageUpgradeCommon): } """ - def __init__(self, module): + def __init__(self, module) -> None: super().__init__(module) self.class_name = self.__class__.__name__ self.endpoints = ApiEndpoints() @@ -131,48 +134,45 @@ def _init_properties(self): self.properties["serial_number"] = None self.properties["epld_modules"] = None - def refresh(self): + def refresh(self) -> None: """ - Refresh self.data with current install-options from the controller + Refresh self.response_data with current install-options from the controller """ + self.method_name = inspect.stack()[0][3] + if self.policy_name is None: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "instance.policy_name must be set before " msg += "calling refresh()" self.module.fail_json(msg) + if self.serial_number is None: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += f"instance.serial_number must be set before " msg += f"calling refresh()" self.module.fail_json(msg) - path = self.endpoints.install_options.get("path") - verb = self.endpoints.install_options.get("verb") + self.path = self.endpoints.install_options.get("path") + self.verb = self.endpoints.install_options.get("verb") + self._build_payload() - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"payload: {self.payload}" - self.log_msg(msg) self.properties["response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) + self.module, self.verb, self.path, data=json.dumps(self.payload) ) - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"response: {self.response}" - self.log_msg(msg) - self.properties["result"] = self._handle_response(self.response, verb) + self.properties["result"] = self._handle_response(self.response, self.verb) + if self.result["success"] is False: - msg = f"{self.class_name}.refresh: " + msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retrieving install-options from " msg += f"the controller. Controller response: {self.response}" self.module.fail_json(msg) - self.properties["response_data"] = self.response.get("DATA") - self.data = self.properties["response_data"] - if self.data.get("compatibilityStatusList") is None: - self.compatibility_status = {} - else: - self.compatibility_status = self.data.get("compatibilityStatusList")[0] + self.properties["response_data"] = self.response.get("DATA", {}) + + self.compatibility_status = self.response_data.get("compatibilityStatusList", [{}])[0] - def _build_payload(self): + + def _build_payload(self) -> None: """ { "devices": [ @@ -186,7 +186,7 @@ def _build_payload(self): "packageInstall": false } """ - self.payload = {} + self.payload: Dict[str, Any] = {} self.payload["devices"] = [] devices = {} devices["serialNumber"] = self.serial_number @@ -197,11 +197,7 @@ def _build_payload(self): self.payload["packageInstall"] = self.package_install def _get(self, item): - return self.make_boolean( - self.make_none( - self.data.get(item) - ) - ) + self.response_data.get(item) # Mandatory properties @property @@ -358,25 +354,28 @@ def ip_address(self): return self.compatibility_status.get("ipAddress") @property - def response_data(self): + def response_data(self) -> Dict[str, Any]: """ Return the DATA portion of the controller response. + Return empty dict otherwise """ - return self.properties.get("response_data") + return self.properties.get("response_data", {}) @property - def response(self): + def response(self) -> Dict[str, Any]: """ Return the controller response. + Return empty dict otherwise """ - return self.properties.get("response") + return self.properties.get("response", {}) @property def result(self): """ Return the query result. + Return empty dict otherwise """ - return self.properties.get("result") + return self.properties.get("result", {}) @property def os_type(self): @@ -408,13 +407,15 @@ def pre_issu_link(self): def raw_data(self): """ Return the raw data of the install-options response, if it exists. + Alias for self.response_data """ - return self.data + return self.response_data @property def raw_response(self): """ Return the raw response, if it exists. + Alias for self.response """ return self.response From 5b0eaa575b79f05964f24661d16b667668cefc38 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 3 Nov 2023 13:06:08 -1000 Subject: [PATCH 041/300] add type annotations/hints --- .../module_utils/image_mgmt/image_upgrade.py | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 7821240e2..baf4374cb 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -2,7 +2,7 @@ import inspect import json from time import sleep -from typing import Any, Dict +from typing import Any, Dict, List, Set, Union from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -120,10 +120,10 @@ def __init__(self, module): self._init_properties() self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) - def _init_defaults(self): + def _init_defaults(self) -> None: self.method_name = inspect.stack()[0][3] - self.defaults = {} + self.defaults: Dict[str, Any] = {} self.defaults["reboot"] = False self.defaults["stage"] = True self.defaults["validate"] = True @@ -144,17 +144,17 @@ def _init_defaults(self): self.defaults["options"]["package"]["install"] = False self.defaults["options"]["package"]["uninstall"] = False - def _init_properties(self): + def _init_properties(self) -> None: self.method_name = inspect.stack()[0][3] # self.ip_addresses is used in: # self._wait_for_current_actions_to_complete() # self._wait_for_image_upgrade_to_complete() - self.ip_addresses = set() + self.ip_addresses: Set[str] = set() # TODO:1 Review these properties since we are no longer # calling this class per-switch given the payload structure # is not amenable to that. - self.properties = {} + self.properties: Dict[str, Any] = {} self.properties["bios_force"] = False self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds @@ -174,12 +174,12 @@ def _init_properties(self): self.properties["reboot"] = False self.properties["write_erase"] = False - self.valid_epld_module = set() + self.valid_epld_module: Set[Union[str, int]] = set() self.valid_epld_module.add("ALL") for module in range(1, self.max_module_number + 1): self.valid_epld_module.add(str(module)) - self.valid_nxos_mode = set() + self.valid_nxos_mode: Set[str] = set() self.valid_nxos_mode.add("disruptive") self.valid_nxos_mode.add("non_disruptive") self.valid_nxos_mode.add("force_non_disruptive") @@ -237,7 +237,7 @@ def validate_devices(self) -> None: self.module.fail_json(msg) # used in self._wait_for_current_actions_to_complete() - self.ip_addresses.add(self.issu_detail.ip_address) + self.ip_addresses.add(str(self.issu_detail.ip_address)) def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: self.method_name = inspect.stack()[0][3] @@ -304,16 +304,18 @@ def build_payload(self, device) -> None: device = self._merge_defaults_to_switch_config(device) - # devices_to_upgrade must currently be a single device - devices_to_upgrade = [] self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() - payload_device = {} + + # devices_to_upgrade must currently be a single device + devices_to_upgrade: List[dict] = [] + + payload_device: Dict[str, Any] = {} payload_device["serialNumber"] = self.issu_detail.serial_number payload_device["policyName"] = device.get("policy") devices_to_upgrade.append(payload_device) - self.payload = {} + self.payload: Dict[str, Any] = {} self.payload["devices"] = devices_to_upgrade self.payload["issuUpgrade"] = device.get("upgrade").get("nxos") @@ -436,8 +438,8 @@ def commit(self) -> None: self.validate_devices() self._wait_for_current_actions_to_complete() - self.path = self.endpoints.image_upgrade.get("path") - self.verb = self.endpoints.image_upgrade.get("verb") + self.path: str = self.endpoints.image_upgrade.get("path") + self.verb: str = self.endpoints.image_upgrade.get("verb") for device in self.devices: self.build_payload(device) @@ -581,22 +583,23 @@ def config_reload(self, value): self.properties["config_reload"] = value @property - def devices(self): + def devices(self) -> List[Dict]: """ Set the devices to upgrade. list() of dict() with the following structure: - { - "serial_number": "FDO211218HH", - "policy_name": "NR1F" - } - + [ + { + "serial_number": "FDO211218HH", + "policy_name": "NR1F" + } + ] Must be set before calling instance.commit() """ - return self.properties.get("devices") + return self.properties.get("devices", [{}]) @devices.setter - def devices(self, value): + def devices(self, value: List[Dict]): self.method_name = inspect.stack()[0][3] if not isinstance(value, list): msg = f"{self.class_name}.{self.method_name}: " From 9b80f6a88b45c5a58f46ee1d8d3339669d32634c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 3 Nov 2023 13:12:17 -1000 Subject: [PATCH 042/300] Remove unused vars, more... FIx fail_json message construction. _build_idempotent_want() - Fix docstring Add type annotations for some items --- plugins/modules/dcnm_image_upgrade.py | 50 +++++++++++++-------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index d49380ed3..76360e680 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -27,7 +27,7 @@ import copy import inspect import json -from typing import Any, Dict +from typing import Any, Dict, List, Union from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -445,29 +445,29 @@ class ImageUpgradeTask(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) + self.class_name = self.__class__.__name__ self.method_name = inspect.stack()[0][3] + self.params = self.module.params - self.class_name = self.__class__.__name__ self.endpoints = ApiEndpoints() # populated in self._build_policy_attach_payload() self.payloads = [] - self.config = module.params.get("config") + self.config = module.params.get("config", {}) if not isinstance(self.config, dict): msg = f"{self.class_name}.{self.method_name}: " msg += "expected dict type for self.config. " - msg = +f"got {type(self.config).__name__}" + msg += f"got {type(self.config).__name__}" self.module.fail_json(msg) self.check_mode = False - self.validated = [] - self.have_create = [] + + self.validated = {} self.want_create = [] self.need = [] - self.diff_save = {} - self.query = [] + self.result = dict(changed=False, diff=[], response=[]) self.mandatory_global_keys = {"switches"} @@ -524,7 +524,7 @@ def get_want(self) -> None: def _build_idempotent_want(self, want) -> None: """ - Return an itempotent want item based on the have item contents. + Build an itempotent want item based on the have item contents. The have item is obtained from an instance of SwitchIssuDetails created in self.get_have(). @@ -565,15 +565,17 @@ def _build_idempotent_want(self, want) -> None: want["policy_changed"] = True # The switch does not have an image policy attached. - # Return the want item as-is with policy_changed = True + # idempotent_want == want with policy_changed = True if self.have.serial_number is None: - return want + self.idempotent_want = copy.deepcopy(want) + return # The switch has an image policy attached which is # different from the want policy. - # Return the want item as-is with policy_changed = True + # idempotent_want == want with policy_changed = True if want["policy"] != self.have.policy: - return want + self.idempotent_want = copy.deepcopy(want) + return # start with a copy of the want item self.idempotent_want = copy.deepcopy(want) @@ -617,7 +619,7 @@ def get_need_merged(self) -> None: be sent to the controller. """ self.method_name = inspect.stack()[0][3] - need = [] + need: List[Dict] = [] for want_create in self.want_create: self.have.ip_address = want_create["ip_address"] @@ -633,10 +635,6 @@ def get_need_merged(self) -> None: need.append(self.idempotent_want) self.need = need - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"need: {self.need}" - self.log_msg(msg) - def get_need_deleted(self) -> None: """ Caller: main() @@ -680,7 +678,7 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: Return: params_spec, a dictionary containing the set of playbook parameter specifications. """ - params_spec = {} + params_spec: Dict[str, Any] = {} params_spec["policy"] = {} params_spec["policy"]["required"] = False params_spec["policy"]["type"] = "str" @@ -995,7 +993,7 @@ def _build_policy_attach_payload(self) -> None: msg += f"{self.image_policies.platform}" self.module.fail_json(msg) - payload = {} + payload: Dict[str, Any] = {} payload["policyName"] = self.image_policies.name # switch_details.host_name is always None in 12.1.2e # so we're using logical_name instead @@ -1031,7 +1029,7 @@ def _send_policy_attach_payload(self) -> None: self.path = self.endpoints.policy_attach.get("path") self.verb = self.endpoints.policy_attach.get("verb") - payload = {} + payload: Dict[str, Any] = {} payload["mappingList"] = self.payloads response = dcnm_send( self.module, self.verb, self.path, data=json.dumps(payload) @@ -1170,9 +1168,9 @@ def handle_merged_state(self) -> None: self._build_policy_attach_payload() self._send_policy_attach_payload() - stage_devices = [] - validate_devices = [] - upgrade_devices = [] + stage_devices: List[str] = [] + validate_devices: List[str] = [] + upgrade_devices: List[Dict[str, Any]] = [] self.switch_details.refresh() @@ -1208,7 +1206,7 @@ def handle_deleted_state(self) -> None: """ self.method_name = inspect.stack()[0][3] - detach_policy_devices = {} + detach_policy_devices: Dict[str, Any] = {} self.switch_details.refresh() self.image_policies.refresh() @@ -1245,7 +1243,7 @@ def handle_query_state(self) -> None: instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() - query_devices = [] + query_devices: List[Dict[str, Any]] = [] for switch in self.need: instance.ip_address = switch.get("ip_address") if instance.filtered_data is None: From 466a273e1015c589892dfa79ddd157b2762d92bc Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 3 Nov 2023 13:13:55 -1000 Subject: [PATCH 043/300] Update test expectations, rename error_message to match. --- .../test_image_upgrade_ImageInstallOptions.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index 2bdcd3143..caa6be694 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -43,10 +43,10 @@ def test_policy_name_not_defined(module) -> None: fail_json() is called if policy_name is not set when refresh() is called. """ module.serial_number = "FOO" - error_message = "ImageInstallOptions.refresh: " - error_message += "instance.policy_name must be set before " - error_message += r"calling refresh\(\)" - with pytest.raises(AnsibleFailJson, match=error_message): + match = "ImageInstallOptions.refresh: " + match += "instance.policy_name must be set before " + match += r"calling refresh\(\)" + with pytest.raises(AnsibleFailJson, match=match): module.refresh() @@ -55,10 +55,10 @@ def test_serial_number_not_defined(module) -> None: fail_json() is called if serial_number is not set when refresh() is called. """ module.policy_name = "FOO" - error_message = "ImageInstallOptions.refresh: " - error_message += "instance.serial_number must be set before " - error_message += r"calling refresh\(\)" - with pytest.raises(AnsibleFailJson, match=error_message): + match = "ImageInstallOptions.refresh: " + match += "instance.serial_number must be set before " + match += r"calling refresh\(\)" + with pytest.raises(AnsibleFailJson, match=match): module.refresh() @@ -79,7 +79,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: module.refresh() assert isinstance(module.response, dict) assert module.device_name == "cvd-1314-leaf" - assert module.err_message == "" + assert module.err_message is None assert module.epld_modules is None assert module.install_option == "disruptive" assert module.install_packages is None @@ -105,10 +105,10 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: module.policy_name = "KRM5" module.serial_number = "BAR" - error_message = "ImageInstallOptions.refresh: " - error_message += "Bad result when retrieving install-options from " - error_message += "the controller. Controller response:" - with pytest.raises(AnsibleFailJson, match=rf"{error_message}"): + match = "ImageInstallOptions.refresh: " + match += "Bad result when retrieving install-options from " + match += "the controller. Controller response:" + with pytest.raises(AnsibleFailJson, match=rf"{match}"): module.refresh() @@ -149,9 +149,9 @@ def test_invalid_value_issu(module) -> None: """ fail_json() is called if issu is not a boolean. """ - error_message = "ImageInstallOptions.issu.setter: issu must be a " - error_message += "boolean value" - with pytest.raises(AnsibleFailJson, match=error_message): + match = "ImageInstallOptions.issu.setter: issu must be a " + match += "boolean value" + with pytest.raises(AnsibleFailJson, match=match): module.issu = "FOO" @@ -159,9 +159,9 @@ def test_invalid_value_epld(module) -> None: """ fail_json() is called if epld is not a boolean. """ - error_message = "ImageInstallOptions.epld.setter: epld must be a " - error_message += "boolean value" - with pytest.raises(AnsibleFailJson, match=error_message): + match = "ImageInstallOptions.epld.setter: epld must be a " + match += "boolean value" + with pytest.raises(AnsibleFailJson, match=match): module.epld = "FOO" @@ -169,7 +169,7 @@ def test_invalid_value_package_install(module) -> None: """ fail_json() is called if package_install is not a boolean. """ - error_message = "ImageInstallOptions.package_install.setter: " - error_message += "package_install must be a boolean value" - with pytest.raises(AnsibleFailJson, match=error_message): + match = "ImageInstallOptions.package_install.setter: " + match += "package_install must be a boolean value" + with pytest.raises(AnsibleFailJson, match=match): module.package_install = "FOO" From 92b0242a9ad88cf3efd7f5982e1ae87d2f7f44c8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 3 Nov 2023 18:57:17 -1000 Subject: [PATCH 044/300] Add more unit tests for ImagePolicyAction --- ...image_upgrade_responses_ImagePolicies.json | 52 +++ ...e_upgrade_responses_ImagePolicyAction.json | 156 ++++++++ ...image_upgrade_responses_SwitchDetails.json | 350 ++++++++++++++++++ ...e_upgrade_responses_SwitchIssuDetails.json | 102 +++++ .../test_image_upgrade_ImagePolicyAction.py | 298 +++++++++++---- 5 files changed, 896 insertions(+), 62 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json index 399d77222..98865e96f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json @@ -93,5 +93,57 @@ ], "message": "" } + }, + "ImagePolicyAction_test_validate_request_1": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "ImagePolicyAction_test_commit_all": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json new file mode 100644 index 000000000..5891db54a --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json @@ -0,0 +1,156 @@ +{ + "policymgnt_policies_get_return_code_200": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + }, + { + "policyName": "OR1F", + "policyType": "PLATFORM", + "nxosVersion": "10.4.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "OR1F EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.4.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.4.1.F.bin", + "agnostic": "false", + "ref_count": 0 + } + ], + "message": "" + } + }, + "policymgnt_policies_get_return_code_200_empty_DATA": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": {} + }, + "policymgnt_policies_get_return_code_200_controller_has_no_defined_image_policies": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "policymgnt_policies_get_return_code_404": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policiess", + "MESSAGE": "Not Found", + "DATA": { + "timestamp": "2023-10-22T21:01:17.375+00:00", + "status": 404, + "error": "Not Found", + "path": "/rest/policymgnt/policiess" + } + }, + "policymgnt_policies_get_return_code_200_policyName_missing_in_response": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "ImagePolicyAction_test_validate_request_1": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "ImagePolicyAction_test_commit_all": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "ImagePolicyAction_detach_policy_200": { + "RETURN_CODE": 200, + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy?serialNumber=FDO21120U5D,FDO211218GC,FOX2109PGD0", + "MESSAGE": "OK", + "DATA": "Successfully detach the policy from device." + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json index 0a9af9d5e..474a3337d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json @@ -248,5 +248,355 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "MESSAGE": "Internal Server Error", "DATA": {} + }, + "ImagePolicyAction_test_commit_all": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [ + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.110", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1111-bgw", + "switchDbID": 158560, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "15 days, 03:37:10", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO211218GC", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-1111-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1400, + "swUUID": "DCNM-UUID-1400", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.111", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1112-bgw", + "switchDbID": 160170, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "15 days, 03:36:55", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FDO21120U5D", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-1112-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1200, + "swUUID": "DCNM-UUID-1200", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Healthy", + "intentedpeerName": "" + }, + { + "switchRoleEnum": "BorderGateway", + "vrf": "management", + "fabricTechnology": "VLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 4, + "name": "null", + "domainID": 0, + "wwn": "null", + "membership": "null", + "ports": 0, + "model": "N9K-C9504", + "version": "null", + "upTime": 0, + "ipAddress": "172.22.150.113", + "mgmtAddress": "null", + "vendor": "Cisco", + "displayHdrs": "null", + "displayValues": "null", + "colDBId": 0, + "fid": 0, + "isLan": "false", + "is_smlic_enabled": "false", + "present": "true", + "licenseViolation": "false", + "managable": "true", + "mds": "false", + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "easy", + "modelType": 0, + "logicalName": "cvd-1111-bgw", + "switchDbID": 160170, + "uid": 0, + "release": "10.2(5)", + "location": "null", + "contact": "null", + "upTimeStr": "15 days, 03:36:55", + "upTimeNumber": 0, + "network": "null", + "nonMdsModel": "null", + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": "null", + "vsanWwnName": "null", + "swWwn": "null", + "swWwnName": "null", + "serialNumber": "FOX2109PGD0", + "domain": "null", + "principal": "null", + "status": "ok", + "index": 0, + "licenseDetail": "null", + "isPmCollect": "false", + "sanAnalyticsCapable": "false", + "vdcId": 0, + "vdcName": "", + "vdcMac": "null", + "fcoeEnabled": "false", + "cpuUsage": 0, + "memoryUsage": 0, + "scope": "null", + "fex": "false", + "health": -1, + "npvEnabled": "false", + "linkName": "null", + "username": "null", + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": "false", + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": "false", + "switchRole": "border gateway", + "mode": "Normal", + "hostName": "cvd-1112-bgw", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": "null", + "operMode": "null", + "modules": "null", + "fexMap": {}, + "isVpcConfigured": "false", + "vpcDomain": 0, + "role": "null", + "peer": "null", + "peerSerialNumber": "null", + "peerSwitchDbId": 0, + "peerlinkState": "null", + "keepAliveState": "null", + "consistencyState": "false", + "sendIntf": "null", + "recvIntf": "null", + "interfaces": "null", + "elementType": "null", + "monitorMode": "null", + "freezeMode": "null", + "cfsSyslogStatus": 1, + "isNonNexus": "false", + "swUUIDId": 1200, + "swUUID": "DCNM-UUID-1200", + "swType": "null", + "ccStatus": "Out-of-Sync", + "operStatus": "Healthy", + "intentedpeerName": "" + } + ] } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 19d840c14..bd19d6f89 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -2597,6 +2597,108 @@ "message": "" } }, + "ImagePolicyAction_test_validate_request_1": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "none", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "TEST_UNKNOWN_PLATFORM", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "ImagePolicyAction_test_commit_all": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, "ImageUpgrade_test_validate_devices_success": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index 2415f1e02..bf1af6d8e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -11,6 +11,10 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ ImagePolicyAction +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ + ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ + SwitchDetails from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber @@ -25,13 +29,33 @@ def does_not_raise(): patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." -dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +dcnm_send_image_policies = patch_image_mgmt + "image_policies.dcnm_send" +dcnm_send_image_policy_action = patch_image_mgmt + "image_policy_action.dcnm_send" +dcnm_send_switch_details = patch_image_mgmt + "switch_details.dcnm_send" +dcnm_send_switch_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +def responses_image_policies(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ImagePolicies" + response = load_fixture(response_file).get(key) + print(f"responses_image_policies: {key} : {response}") + return response + +def responses_image_policy_action(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ImagePolicyAction" + response = load_fixture(response_file).get(key) + print(f"responses_image_policy_action: {key} : {response}") + return response -def responses_issu_details(key: str) -> Dict[str, str]: +def responses_switch_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchDetails" + response = load_fixture(response_file).get(key) + print(f"responses_switch_details: {key} : {response}") + return response + +def responses_switch_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) - print(f"responses_issu_details: {key} : {response}") + print(f"responses_switch_issu_details: {key} : {response}") return response @@ -43,68 +67,71 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture -def module(): +def mock_image_policy_action(): return ImagePolicyAction(MockAnsibleModule) - @pytest.fixture def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) +@pytest.fixture +def mock_image_policies() -> ImagePolicies: + return ImagePolicies(MockAnsibleModule) + # test_init -def test_init(module) -> None: - module.__init__(MockAnsibleModule) - assert isinstance(module, ImagePolicyAction) - assert isinstance(module.switch_issu_details, SwitchIssuDetailsBySerialNumber) - assert module.valid_actions == {"attach", "detach", "query"} +def test_init(mock_image_policy_action) -> None: + mock_image_policy_action.__init__(MockAnsibleModule) + assert isinstance(mock_image_policy_action, ImagePolicyAction) + assert isinstance(mock_image_policy_action.switch_issu_details, SwitchIssuDetailsBySerialNumber) + assert mock_image_policy_action.valid_actions == {"attach", "detach", "query"} # test_init_properties -def test_init_properties(module) -> None: +def test_init_properties(mock_image_policy_action) -> None: """ Properties are initialized to expected values """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("action") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None - assert module.properties.get("policy_name") == None - assert module.properties.get("query_result") == None - assert module.properties.get("serial_numbers") == None + mock_image_policy_action._init_properties() + assert isinstance(mock_image_policy_action.properties, dict) + assert mock_image_policy_action.properties.get("action") == None + assert mock_image_policy_action.properties.get("response") == None + assert mock_image_policy_action.properties.get("result") == None + assert mock_image_policy_action.properties.get("policy_name") == None + assert mock_image_policy_action.properties.get("query_result") == None + assert mock_image_policy_action.properties.get("serial_numbers") == None # test_build_attach_payload -def test_build_attach_payload(monkeypatch, module, mock_issu_details) -> None: +def test_build_attach_payload(monkeypatch, mock_image_policy_action, mock_issu_details) -> None: """ build_attach_payload builds the payload to send in the POST request to attach policies to devices Expectations: - 1. module.payload should be a list() + 1. mock_image_policy_action.payload should be a list() Expected results: - 1. module.fail_json should not be called - 2. module.payloads should be a list() - 3. module.payloads should have length 5 + 1. mock_image_policy_action.fail_json should not be called + 2. mock_image_policy_action.payloads should be a list() + 3. mock_image_policy_action.payloads should have length 5 """ - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "ImagePolicyAction_test_build_attach_payload" - return responses_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) - module.switch_issu_details = mock_issu_details - module.policy_name = "KR5M" - module.serial_numbers = [ + mock_image_policy_action.switch_issu_details = mock_issu_details + mock_image_policy_action.policy_name = "KR5M" + mock_image_policy_action.serial_numbers = [ "FDO2112189M", "FDO211218AX", "FDO211218B5", @@ -112,33 +139,33 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: "FDO211218GC", ] with does_not_raise(): - module.build_attach_payload() - assert isinstance(module.payloads, list) - assert len(module.payloads) == 5 + mock_image_policy_action.build_attach_payload() + assert isinstance(mock_image_policy_action.payloads, list) + assert len(mock_image_policy_action.payloads) == 5 # test_build_attach_payload_fail_json -def test_build_attach_payload_fail_json(monkeypatch, module, mock_issu_details) -> None: +def test_build_attach_payload_fail_json(monkeypatch, mock_image_policy_action, mock_issu_details) -> None: """ build_attach_payload builds the payload to send in the POST request to attach policies to devices. If any key in the payload has a value of None, the function calls fail_json. Expected results: - 1. module.fail_json should be called because deviceName is None in the issu_details response + 1. fail_json should be called because deviceName is None in the issu_details response """ - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "ImagePolicyAction_test_build_attach_payload_fail_json" - return responses_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) - module.switch_issu_details = mock_issu_details - module.policy_name = "KR5M" - module.serial_numbers = [ + mock_image_policy_action.switch_issu_details = mock_issu_details + mock_image_policy_action.policy_name = "KR5M" + mock_image_policy_action.serial_numbers = [ "FDO2112189M", ] error_message = "Unable to determine hostName for switch " @@ -146,31 +173,31 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: error_message += "Please verify that the switch is managed by " error_message += "the controller." with pytest.raises(AnsibleFailJson, match=error_message): - module.build_attach_payload() + mock_image_policy_action.build_attach_payload() # test_validate_request_policy_name_none -def test_validate_request_action_none(module, mock_issu_details) -> None: +def test_validate_request_action_none(mock_image_policy_action, mock_issu_details) -> None: """ validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a validation-specific error message. Expected results: - 1. module.fail_json should be called because module.action is None + 1. fail_json should be called because action is None """ - module.switch_issu_details = mock_issu_details - module.policy_name = "KR5M" - module.serial_numbers = [ + mock_image_policy_action.switch_issu_details = mock_issu_details + mock_image_policy_action.policy_name = "KR5M" + mock_image_policy_action.serial_numbers = [ "FDO2112189M", ] match = "ImagePolicyAction.validate_request: " match += "instance.action must be set before calling commit()" with pytest.raises(AnsibleFailJson, match=match): - module.validate_request() + mock_image_policy_action.validate_request() # test_validate_request_policy_name_none @@ -188,7 +215,7 @@ def test_validate_request_action_none(module, mock_issu_details) -> None: ], ) def test_validate_request_policy_name_none( - action, expected, module, mock_issu_details + action, expected, mock_image_policy_action, mock_issu_details ) -> None: """ validate_request performs a number of validations prior to calling commit @@ -196,17 +223,17 @@ def test_validate_request_policy_name_none( validation-specific error message. Expected results: - 1. module.fail_json should be called because module.policy_name is None + 1. fail_json should be called because policy_name is None """ - module.switch_issu_details = mock_issu_details - module.action = action - module.serial_numbers = [ + mock_image_policy_action.switch_issu_details = mock_issu_details + mock_image_policy_action.action = action + mock_image_policy_action.serial_numbers = [ "FDO2112189M", ] with expected: - module.validate_request() + mock_image_policy_action.validate_request() # test_validate_request_serial_numbers_none @@ -224,7 +251,7 @@ def test_validate_request_policy_name_none( ], ) def test_validate_request_serial_numbers_none( - action, expected, module, mock_issu_details + action, expected, mock_image_policy_action, mock_issu_details ) -> None: """ validate_request performs a number of validations prior to calling commit @@ -232,14 +259,161 @@ def test_validate_request_serial_numbers_none( validation-specific error message. Expected results: - 1. action == attach module.fail_json should be called - 2. action == detach module.fail_json should be called - 3. action == query module.fail_json should NOT be called + 1. action == attach fail_json should be called + 2. action == detach fail_json should be called + 3. action == query fail_json should NOT be called """ - module.switch_issu_details = mock_issu_details - module.action = action - module.policy_name = "KR5M" + mock_image_policy_action.switch_issu_details = mock_issu_details + mock_image_policy_action.action = action + mock_image_policy_action.policy_name = "KR5M" with expected: - module.validate_request() + mock_image_policy_action.validate_request() + + +# test_validate_request_image_policy_does_not_support_switch_platform + +def test_validate_request_image_policy_does_not_support_switch_platform(monkeypatch, mock_image_policy_action, mock_issu_details, mock_image_policies +) -> None: + """ + validate_request performs a number of validations prior to calling commit + If any of these validations fail, the function calls fail_json with a + validation-specific error message. + + Expected results: + 1. fail_json should be called since the policy KR5M + supports "N9K switch platform and the response from ImagePolicies + contains platform == "TEST_UNKNOWN_PLATFORM" + """ + + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_validate_request_1" + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_validate_request_1" + return responses_image_policies(key) + + monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + mock_image_policy_action.switch_issu_details = mock_issu_details + mock_image_policy_action.image_policies = mock_image_policies + mock_image_policy_action.action = "attach" + mock_image_policy_action.policy_name = "KR5M" + mock_image_policy_action.serial_numbers = ["FDO2112189M"] + + + match = "ImagePolicyAction.validate_request: " + match += "policy KR5M does not support platform TEST_UNKNOWN_PLATFORM. " + match += r"KR5M supports the following platform\(s\): N9K/N3K" + + with pytest.raises(AnsibleFailJson, match=match): + mock_image_policy_action.validate_request() + + +# test_commit_unknown_action + +def test_commit_action_unknown(monkeypatch, mock_image_policy_action) -> None: + """ + Verify that fail_json is called if action is unknown. + + commit calls validate_request() and then calls one of the following + functions based on the value of action: + action == "attach" : _attach_policy + action == "detach" : _detach_policy + action == "query" : _query_policy + + If action is not one of [attach, detach, query], commit() calls fail_json. + + This test mocks valid_actions to include "FOO" so that action.setter + will accept it (effectively bypassing the check in the setter). + It also mocks validate_request() to remove it from consideration. + + Since action == "FOO" is not covered in commit()'s if clauses, + the else clause is taken and fail_json is called. + + Expected results: + 1. action is unknown, so module.fail_json is called + """ + + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_commit_all" + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_commit_all" + return responses_image_policies(key) + def mock_validate_request(*args, **kwargs) -> None: + pass + + monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + + monkeypatch.setattr(mock_image_policy_action, "validate_request", mock_validate_request) + monkeypatch.setattr(mock_image_policy_action, "valid_actions", {"attach", "detach", "query", "FOO"}) + + mock_image_policy_action.policy_name = "KR5M" + mock_image_policy_action.serial_numbers = ["FDO2112189M"] + mock_image_policy_action.action = "FOO" + + match = "ImagePolicyAction.commit: Unknown action FOO." + + with pytest.raises(AnsibleFailJson, match=match): + mock_image_policy_action.commit() + + +def test_commit_action_detach_success(monkeypatch, mock_image_policy_action) -> None: + """ + Verify that commit is successful for action == "detach" given a + 200 response from the controller in ImagePolicyAction._detach_policy + + commit calls validate_request() and then calls one of the following + functions based on the value of action: + action == "attach" : _attach_policy + action == "detach" : _detach_policy + action == "query" : _query_policy + + Expected results: + 1. action is "detach", so ImagePolicyAction_detach_policy is called + 2. ImagePolicyAction.response contains RESULT_CODE 200 + 3. commit is successful + """ + + + def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_commit_all" + return responses_image_policies(key) + + def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_commit_all" + return responses_switch_details(key) + + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_test_commit_all" + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_policy_action(*args, **kwargs) -> Dict[str, Any]: + key = "ImagePolicyAction_detach_policy_200" + return responses_image_policy_action(key) + + monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(dcnm_send_image_policy_action, mock_dcnm_send_image_policy_action) + monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) + + mock_image_policy_action.policy_name = "KR5M" + mock_image_policy_action.serial_numbers = ["FDO2112189M"] + # mock_image_policy_action.serial_numbers = ["FDO21120U5D","FDO211218GC","FOX2109PGD0"] + mock_image_policy_action.action = "detach" + + mock_image_policy_action.commit() + assert isinstance(mock_image_policy_action.response, dict) + assert mock_image_policy_action.response.get("RETURN_CODE") == 200 + assert mock_image_policy_action.response.get("METHOD") == "DELETE" + assert mock_image_policy_action.response.get("MESSAGE") == "OK" + assert mock_image_policy_action.response.get("DATA") == "Successfully detach the policy from device." + assert mock_image_policy_action.result.get("success") == True + assert mock_image_policy_action.result.get("changed") == True + From 2dde0d127e56455f9851e6d6f49e96fbfd2e7f0c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 5 Nov 2023 07:54:19 -1000 Subject: [PATCH 045/300] Consistent naming for test cases While there is value in a testcase name indicating what the test does, this information can be added to the input JSON if need be. IMHO, for organizing input data, there's more value is naming the input data consistently with its correponding testcase. Also, we've duplicated input data such that each testcase has one, and only one, corresponding input entry per response file. This way, if we ever remove a testcase, we can know with certainty that removing its corresponding input data will not impact other testcases i.e. let's not share input data across testcasees. --- ...e_upgrade_responses_ControllerVersion.json | 140 ++++++++++--- .../test_image_upgrade_ApiEndpoints.py | 32 +-- .../test_image_upgrade_ControllerVersion.py | 192 +++++++++--------- 3 files changed, 227 insertions(+), 137 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json index b1bc22c1a..205b8c409 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json @@ -1,5 +1,5 @@ { - "ControllerVersion_get_return_code_200": { + "test_common_version_00009a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -15,21 +15,21 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_get_return_code_404": { + "test_common_version_00010a": { "RETURN_CODE": 404, "METHOD": "GET", "REQUEST_PATH": "https://foo/noop", "MESSAGE": "Not Found", "DATA": {} }, - "ControllerVersion_get_return_code_500": { + "test_common_version_00011a": { "RETURN_CODE": 500, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", "MESSAGE": "Internal Server Error", "DATA": {} }, - "ControllerVersion_dev_false": { + "test_common_version_00002a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -45,7 +45,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_dev_true": { + "test_common_version_00002b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -61,7 +61,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_dev_none": { + "test_common_version_00002c": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -76,7 +76,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_install_EASYFABRIC": { + "test_common_version_00003a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -92,7 +92,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_install_none": { + "test_common_version_00003b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -107,7 +107,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_is_ha_enabled_false": { + "test_common_version_00004b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -123,7 +123,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_is_ha_enabled_true": { + "test_common_version_00004a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -139,7 +139,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_is_ha_enabled_none": { + "test_common_version_00004c": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -154,7 +154,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_is_upgrade_inprogress_false": { + "test_common_version_00006b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -170,7 +170,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_is_upgrade_inprogress_true": { + "test_common_version_00006a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -186,7 +186,7 @@ "is_upgrade_inprogress": "true" } }, - "ControllerVersion_is_upgrade_inprogress_none": { + "test_common_version_00006c": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -201,7 +201,7 @@ "uuid": "" } }, - "ControllerVersion_is_media_controller_false": { + "test_common_version_00005b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -217,7 +217,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_is_media_controller_true": { + "test_common_version_00005a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -233,7 +233,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_is_media_controller_none": { + "test_common_version_00005c": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -248,7 +248,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_DATA_present": { + "test_common_version_00007a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -264,13 +264,13 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_DATA_not_present": { + "test_common_version_00008a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", "MESSAGE": "OK" }, - "ControllerVersion_mode_LAN": { + "test_common_version_00012a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -286,7 +286,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_mode_none": { + "test_common_version_00012b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -300,7 +300,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_uuid_UUID": { + "test_common_version_00013a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -316,7 +316,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_uuid_none": { + "test_common_version_00013b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -330,7 +330,7 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_version_12.1.3b": { + "test_common_version_00014a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -346,7 +346,97 @@ "is_upgrade_inprogress": "false" } }, - "ControllerVersion_version_none": { + "test_common_version_00014b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00015a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00015b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00016a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00016b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00017a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00017b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py index f36827006..4dc33bd0e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py @@ -7,7 +7,7 @@ ApiEndpoints -def test_dcnm_image_upgrade_endpoints_init() -> None: +def test_image_mgmt_api_00001() -> None: """ Endpoints.__init__ """ @@ -38,7 +38,7 @@ def test_dcnm_image_upgrade_endpoints_init() -> None: ) -def test_dcnm_image_upgrade_endpoints_bootflash_info() -> None: +def test_image_mgmt_api_00002() -> None: """ Endpoints.bootflash_info """ @@ -50,7 +50,7 @@ def test_dcnm_image_upgrade_endpoints_bootflash_info() -> None: ) -def test_dcnm_image_upgrade_endpoints_install_options() -> None: +def test_image_mgmt_api_00003() -> None: """ Endpoints.install_options """ @@ -62,7 +62,7 @@ def test_dcnm_image_upgrade_endpoints_install_options() -> None: ) -def test_dcnm_image_upgrade_endpoints_image_stage() -> None: +def test_image_mgmt_api_00004() -> None: """ Endpoints.image_stage """ @@ -74,7 +74,7 @@ def test_dcnm_image_upgrade_endpoints_image_stage() -> None: ) -def test_dcnm_image_upgrade_endpoints_image_upgrade() -> None: +def test_image_mgmt_api_00005() -> None: """ Endpoints.image_upgrade """ @@ -86,7 +86,7 @@ def test_dcnm_image_upgrade_endpoints_image_upgrade() -> None: ) -def test_dcnm_image_upgrade_endpoints_image_validate() -> None: +def test_image_mgmt_api_00006() -> None: """ Endpoints.image_validate """ @@ -98,7 +98,7 @@ def test_dcnm_image_upgrade_endpoints_image_validate() -> None: ) -def test_dcnm_image_upgrade_endpoints_issu_info() -> None: +def test_image_mgmt_api_00007() -> None: """ Endpoints.issu_info """ @@ -110,7 +110,7 @@ def test_dcnm_image_upgrade_endpoints_issu_info() -> None: ) -def test_dcnm_image_upgrade_endpoints_controller_version() -> None: +def test_image_mgmt_api_00008() -> None: """ Endpoints.controller_version """ @@ -122,7 +122,7 @@ def test_dcnm_image_upgrade_endpoints_controller_version() -> None: ) -def test_dcnm_image_upgrade_endpoints_policies_attached_info() -> None: +def test_image_mgmt_api_00009() -> None: """ Endpoints.policies_attached_info """ @@ -134,7 +134,7 @@ def test_dcnm_image_upgrade_endpoints_policies_attached_info() -> None: ) -def test_dcnm_image_upgrade_endpoints_policies_info() -> None: +def test_image_mgmt_api_00010() -> None: """ Endpoints.policies_info """ @@ -146,7 +146,7 @@ def test_dcnm_image_upgrade_endpoints_policies_info() -> None: ) -def test_dcnm_image_upgrade_endpoints_policy_attach() -> None: +def test_image_mgmt_api_00011() -> None: """ Endpoints.policy_attach """ @@ -158,7 +158,7 @@ def test_dcnm_image_upgrade_endpoints_policy_attach() -> None: ) -def test_dcnm_image_upgrade_endpoints_policy_create() -> None: +def test_image_mgmt_api_00012() -> None: """ Endpoints.policy_create """ @@ -170,7 +170,7 @@ def test_dcnm_image_upgrade_endpoints_policy_create() -> None: ) -def test_dcnm_image_upgrade_endpoints_policy_detach() -> None: +def test_image_mgmt_api_00013() -> None: """ Endpoints.policy_detach """ @@ -182,7 +182,7 @@ def test_dcnm_image_upgrade_endpoints_policy_detach() -> None: ) -def test_dcnm_image_upgrade_endpoints_policy_info() -> None: +def test_image_mgmt_api_00014() -> None: """ Endpoints.policy_info """ @@ -194,7 +194,7 @@ def test_dcnm_image_upgrade_endpoints_policy_info() -> None: ) -def test_dcnm_image_upgrade_endpoints_stage_info() -> None: +def test_image_mgmt_api_00015() -> None: """ Endpoints.stage_info """ @@ -206,7 +206,7 @@ def test_dcnm_image_upgrade_endpoints_stage_info() -> None: ) -def test_dcnm_image_upgrade_endpoints_switches_info() -> None: +def test_image_mgmt_api_00016() -> None: """ Endpoints.switches_info """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py index 70392223a..3eb9c1532 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py @@ -43,7 +43,7 @@ def mock_controller_version() -> ControllerVersion: return ControllerVersion(MockAnsibleModule) -def test_init_properties(module) -> None: +def test_common_version_00001(module) -> None: """ Properties are initialized to expected values """ @@ -57,18 +57,18 @@ def test_init_properties(module) -> None: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_dev_false", False), - ("ControllerVersion_dev_true", True), - ("ControllerVersion_dev_none", None), + ("test_common_version_00002a", False), + ("test_common_version_00002b", True), + ("test_common_version_00002c", None), ], ) -def test_dev(monkeypatch, module, key, expected) -> None: +def test_common_version_00002(monkeypatch, module, key, expected) -> None: """ Function description: ControllerVersion.dev returns: - - True if NDFC is a development version - - False if NDFC is not a development version + - True if the controller is a development version + - False if the controller is not a development version - None otherwise Expectations: @@ -77,9 +77,9 @@ def test_dev(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_dev_false == False - 2. ControllerVersion_dev_true == True - 3. ControllerVersion_dev_none == None + 1. test_common_version_00002a == False + 2. test_common_version_00002b == True + 3. test_common_version_00002c == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -94,17 +94,17 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_install_EASYFABRIC", "EASYFABRIC"), - ("ControllerVersion_install_none", None), + ("test_common_version_00003a", "EASYFABRIC"), + ("test_common_version_00003b", None), ], ) -def test_install(monkeypatch, module, key, expected) -> None: +def test_common_version_00003(monkeypatch, module, key, expected) -> None: """ Description: ControllerVersion.install returns: - - Value of NDFC response "install" key, if present - - None, if NDFC response "install" key is missing + - Value of controller response "install" key, if present + - None, if controller response "install" key is missing Expectations: @@ -113,8 +113,8 @@ def test_install(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_install_EASYFABRIC == "EASYFABRIC" - 2. ControllerVersion_install_none == None + 1. test_common_version_00003a == "EASYFABRIC" + 2. test_common_version_00003b == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -129,19 +129,19 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_is_ha_enabled_true", True), - ("ControllerVersion_is_ha_enabled_false", False), - ("ControllerVersion_is_ha_enabled_none", None), + ("test_common_version_00004a", True), + ("test_common_version_00004b", False), + ("test_common_version_00004c", None), ], ) -def test_is_ha_enabled(monkeypatch, module, key, expected) -> None: +def test_common_version_00004(monkeypatch, module, key, expected) -> None: """ Function description: ControllerVersion.is_ha_enabled returns: - - True, if NDFC response "isHaEnabled" key == "true" - - False, if NDFC response "isHaEnabled" key == "false" - - None, if NDFC response "isHaEnabled" key is missing + - True, if controller response "isHaEnabled" key == "true" + - False, if controller response "isHaEnabled" key == "false" + - None, if controller response "isHaEnabled" key is missing Expectations: @@ -149,9 +149,9 @@ def test_is_ha_enabled(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_is_ha_enabled_true == True - 2. ControllerVersion_is_ha_enabled_false == False - 3. ControllerVersion_is_ha_enabled_none == None + 1. test_common_version_00004a == True + 2. test_common_version_00004b == False + 3. test_common_version_00004c == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -166,19 +166,19 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_is_media_controller_true", True), - ("ControllerVersion_is_media_controller_false", False), - ("ControllerVersion_is_media_controller_none", None), + ("test_common_version_00005a", True), + ("test_common_version_00005b", False), + ("test_common_version_00005c", None), ], ) -def test_is_media_controller(monkeypatch, module, key, expected) -> None: +def test_common_version_00005(monkeypatch, module, key, expected) -> None: """ Function description: ControllerVersion.is_media_controller returns: - - True, if NDFC response "isMediaController" key == "true" - - False, if NDFC response "isMediaController" key == "false" - - None, if NDFC response "isMediaController" key is missing + - True, if controller response "isMediaController" key == "true" + - False, if controller response "isMediaController" key == "false" + - None, if controller response "isMediaController" key is missing Expectations: @@ -187,9 +187,9 @@ def test_is_media_controller(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_is_media_controller_true == True - 2. ControllerVersion_is_media_controller_false == False - 3. ControllerVersion_is_media_controller_none == None + 1. test_common_version_00005a == True + 2. test_common_version_00005b == False + 3. test_common_version_00005c == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -204,12 +204,12 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_is_upgrade_inprogress_true", True), - ("ControllerVersion_is_upgrade_inprogress_false", False), - ("ControllerVersion_is_upgrade_inprogress_none", None), + ("test_common_version_00006a", True), + ("test_common_version_00006b", False), + ("test_common_version_00006c", None), ], ) -def test_is_upgrade_inprogress(monkeypatch, module, key, expected) -> None: +def test_common_version_00006(monkeypatch, module, key, expected) -> None: """ Function description: @@ -225,9 +225,9 @@ def test_is_upgrade_inprogress(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_is_upgrade_inprogress_true == True - 2. ControllerVersion_is_upgrade_inprogress_false == False - 3. ControllerVersion_is_upgrade_inprogress_none == None + 1. test_common_version_00006a == True + 2. test_common_version_00006b == False + 3. test_common_version_00006c == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -239,7 +239,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: assert module.is_upgrade_inprogress == expected -def test_response_data_present(monkeypatch, module) -> None: +def test_common_version_00007(monkeypatch, module) -> None: """ Function description: @@ -254,11 +254,11 @@ def test_response_data_present(monkeypatch, module) -> None: Expected results: - 1. ControllerVersion_DATA_present, ControllerVersion.response_data == type(dict) + 1. test_common_version_00007a, ControllerVersion.response_data == type(dict) """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "ControllerVersion_DATA_present" + key = "test_common_version_00007a" return responses_controller_version(key) monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) @@ -267,7 +267,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: assert isinstance(module.response_data, dict) -def test_response_data_not_present(monkeypatch, module) -> None: +def test_common_version_00008(monkeypatch, module) -> None: """ Function description: @@ -283,7 +283,7 @@ def test_response_data_not_present(monkeypatch, module) -> None: """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "ControllerVersion_DATA_not_present" + key = "test_common_version_00008a" return responses_controller_version(key) monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) @@ -292,7 +292,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: module.refresh() -def test_result_200(monkeypatch, module) -> None: +def test_common_version_00009(monkeypatch, module) -> None: """ Function description: @@ -310,7 +310,7 @@ def test_result_200(monkeypatch, module) -> None: """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "ControllerVersion_get_return_code_200" + key = "test_common_version_00009a" return responses_controller_version(key) monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) @@ -319,7 +319,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: assert module.result == {"found": True, "success": True} -def test_result_404(monkeypatch, module) -> None: +def test_common_version_00010(monkeypatch, module) -> None: """ Function description: @@ -337,7 +337,7 @@ def test_result_404(monkeypatch, module) -> None: """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "ControllerVersion_get_return_code_404" + key = "test_common_version_00010a" return responses_controller_version(key) monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) @@ -346,7 +346,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: module.refresh() -def test_result_500(monkeypatch, module) -> None: +def test_common_version_00011(monkeypatch, module) -> None: """ Function description: @@ -364,7 +364,7 @@ def test_result_500(monkeypatch, module) -> None: """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "ControllerVersion_get_return_code_500" + key = "test_common_version_00011a" return responses_controller_version(key) monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) @@ -375,15 +375,15 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", - [("ControllerVersion_mode_LAN", "LAN"), ("ControllerVersion_mode_none", None)], + [("test_common_version_00012a", "LAN"), ("test_common_version_00012b", None)], ) -def test_mode(monkeypatch, module, key, expected) -> None: +def test_common_version_00012(monkeypatch, module, key, expected) -> None: """ Function description: ControllerVersion.mode returns: - - If NDFC response "mode" key is present, its value - - If NDFC response "mode" key is not present, None + - If controller response "mode" key is present, its value + - If controller response "mode" key is not present, None Expectations: @@ -392,8 +392,8 @@ def test_mode(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_mode_LAN == "LAN" - 2. ControllerVersion_mode_none == None + 1. test_common_version_00012a == "LAN" + 2. test_common_version_00012b == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -408,17 +408,17 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_uuid_UUID", "foo-uuid"), - ("ControllerVersion_uuid_none", None), + ("test_common_version_00013a", "foo-uuid"), + ("test_common_version_00013b", None), ], ) -def test_uuid(monkeypatch, module, key, expected) -> None: +def test_common_version_00013(monkeypatch, module, key, expected) -> None: """ Function description: ControllerVersion.uuid returns: - - If NDFC response "uuid" key is present, its value - - If NDFC response "uuid" key is not present, None + - If controller response "uuid" key is present, its value + - If controller response "uuid" key is not present, None Expectations: @@ -427,8 +427,8 @@ def test_uuid(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_uuid_UUID == "foo-uuid" - 2. ControllerVersion_uuid_none == None + 1. test_common_version_00013a == "foo-uuid" + 2. test_common_version_00013b == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -443,17 +443,17 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_version_12.1.3b", "12.1.3b"), - ("ControllerVersion_version_none", None), + ("test_common_version_00014a", "12.1.3b"), + ("test_common_version_00014b", None), ], ) -def test_version(monkeypatch, module, key, expected) -> None: +def test_common_version_00014(monkeypatch, module, key, expected) -> None: """ Function description: ControllerVersion.version returns: - - If NDFC response "version" key is present, its value - - If NDFC response "version" key is not present, None + - If controller response "version" key is present, its value + - If controller response "version" key is not present, None Expectations: @@ -462,8 +462,8 @@ def test_version(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_version_12.1.3b == "12.1.3b" - 2. ControllerVersion_version_none == None + 1. test_common_version_00014a == "12.1.3b" + 2. test_common_version_00014b == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -478,11 +478,11 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_version_12.1.3b", "12"), - ("ControllerVersion_version_none", None), + ("test_common_version_00015a", "12"), + ("test_common_version_00015b", None), ], ) -def test_version_major(monkeypatch, module, key, expected) -> None: +def test_common_version_00015(monkeypatch, module, key, expected) -> None: """ Function description: @@ -491,8 +491,8 @@ def test_version_major(monkeypatch, module, key, expected) -> None: by splitting the string on "." and returning the first element ControllerVersion.version_major returns: - - If NDFC response "version" key is present, the major version - - If NDFC response "version" key is not present, None + - If controller response "version" key is present, the major version + - If controller response "version" key is not present, None Expectations: @@ -501,8 +501,8 @@ def test_version_major(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_version_12.1.3b == "12" - 2. ControllerVersion_version_none == None + 1. test_common_version_00015a == "12" + 2. test_common_version_00015b == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -517,11 +517,11 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_version_12.1.3b", "1"), - ("ControllerVersion_version_none", None), + ("test_common_version_00016a", "1"), + ("test_common_version_00016b", None), ], ) -def test_version_minor(monkeypatch, module, key, expected) -> None: +def test_common_version_00016(monkeypatch, module, key, expected) -> None: """ Function description: @@ -530,8 +530,8 @@ def test_version_minor(monkeypatch, module, key, expected) -> None: by splitting the string on "." and returning the second element ControllerVersion.version_minor returns: - - If NDFC response "version" key is present, the minor version - - If NDFC response "version" key is not present, None + - If controller response "version" key is present, the minor version + - If controller response "version" key is not present, None Expectations: @@ -540,8 +540,8 @@ def test_version_minor(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_version_12.1.3b == "1" - 2. ControllerVersion_version_none == None + 1. test_common_version_00016a == "1" + 2. test_common_version_00016b == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @@ -556,11 +556,11 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key, expected", [ - ("ControllerVersion_version_12.1.3b", "3b"), - ("ControllerVersion_version_none", None), + ("test_common_version_00017a", "3b"), + ("test_common_version_00017b", None), ], ) -def test_version_patch(monkeypatch, module, key, expected) -> None: +def test_common_version_00017(monkeypatch, module, key, expected) -> None: """ Function description: @@ -569,8 +569,8 @@ def test_version_patch(monkeypatch, module, key, expected) -> None: by splitting the string on "." and returning the third element ControllerVersion.version_patch returns: - - If NDFC response "version" key is present, the patch version - - If NDFC response "version" key is not present, None + - If controller response "version" key is present, the patch version + - If controller response "version" key is not present, None Expectations: @@ -579,8 +579,8 @@ def test_version_patch(monkeypatch, module, key, expected) -> None: Expected results: - 1. ControllerVersion_version_12.1.3b == "3b" - 2. ControllerVersion_version_none == None + 1. test_common_version_00017a == "3b" + 2. test_common_version_00017b == None """ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: From dd6143ec475ddf82937f6af41dae13dfb66daa54 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 5 Nov 2023 09:54:41 -1000 Subject: [PATCH 046/300] Standardize test names, 1:1 input to test case --- ...e_upgrade_responses_ControllerVersion.json | 39 +++- .../image_upgrade_responses_ImageStage.json | 47 +++++ ...e_upgrade_responses_SwitchIssuDetails.json | 185 ++++++++++++++++-- .../test_image_upgrade_ImageStage.py | 156 ++++++++------- 4 files changed, 342 insertions(+), 85 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json index 205b8c409..049321107 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json @@ -450,7 +450,7 @@ "is_upgrade_inprogress": "false" } }, - "ImageStage_12_1_2e": { + "test_image_mgmt_stage_00003a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -466,7 +466,42 @@ "is_upgrade_inprogress": "false" } }, - "ImageStage_12_1_3b": { + "test_image_mgmt_stage_00003b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_image_mgmt_stage_00006a": { + "TEST_NOTES": [ + "Needed only for the 200 return code" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_image_mgmt_stage_00007a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json new file mode 100644 index 000000000..703d0794d --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json @@ -0,0 +1,47 @@ +{ + "test_image_mgmt_stage_00006a": { + "TEST_NOTES": [ + "Needed only for the 200 return code" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-info", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } + }, + "test_image_mgmt_stage_00007a": { + "TEST_NOTES": [ + "Needed only for the 200 return code" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-info", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } + }, + "test_image_mgmt_stage_00008a": { + "TEST_NOTES": [ + "Needed only for the 200 return code" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-info", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index bd19d6f89..98915306b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -755,7 +755,7 @@ "message": "" } }, - "ImageStage_test_prune_serial_numbers": { + "test_image_mgmt_stage_00004a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -962,7 +962,7 @@ "message": "" } }, - "ImageStage_test_validate_serial_numbers": { + "test_image_mgmt_stage_00005a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1052,7 +1052,7 @@ "message": "" } }, - "ImageStage_test_wait_for_image_stage_to_complete": { + "test_image_mgmt_stage_00006a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1106,7 +1106,7 @@ "policy": "KR5M", "status": "In-Sync", "reason": "Upgrade", - "imageStaged": "Success", + "imageStaged": "Failed", "validated": "Success", "upgrade": "Success", "upgGroups": "null", @@ -1142,7 +1142,7 @@ "message": "" } }, - "ImageStage_test_wait_for_image_stage_to_complete_fail_json": { + "test_image_mgmt_stage_00007a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1211,7 +1211,7 @@ "ipAddress": "172.22.150.108", "issuAllowed": "", "statusPercent": 100, - "imageStagedPercent": 90, + "imageStagedPercent": 100, "validatedPercent": 100, "upgradePercent": 100, "modelType": 0, @@ -1232,7 +1232,61 @@ "message": "" } }, - "ImageStage_test_wait_for_image_stage_to_complete_timeout": { + "test_image_mgmt_stage_00008a": { + "TEST_NOTES": [ + "Needed only for the 200 return code" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_mgmt_stage_00009a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1286,7 +1340,7 @@ "policy": "KR5M", "status": "In-Sync", "reason": "Upgrade", - "imageStaged": "In-Progress", + "imageStaged": "Success", "validated": "Success", "upgrade": "Success", "upgGroups": "null", @@ -1301,7 +1355,7 @@ "ipAddress": "172.22.150.108", "issuAllowed": "", "statusPercent": 100, - "imageStagedPercent": 50, + "imageStagedPercent": 100, "validatedPercent": 100, "upgradePercent": 100, "modelType": 0, @@ -1322,7 +1376,7 @@ "message": "" } }, - "ImageStage_test_wait_for_current_actions_to_complete": { + "test_image_mgmt_stage_00010a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1376,7 +1430,7 @@ "policy": "KR5M", "status": "In-Sync", "reason": "Upgrade", - "imageStaged": "Success", + "imageStaged": "Failed", "validated": "Success", "upgrade": "Success", "upgGroups": "null", @@ -1391,7 +1445,7 @@ "ipAddress": "172.22.150.108", "issuAllowed": "", "statusPercent": 100, - "imageStagedPercent": 100, + "imageStagedPercent": 90, "validatedPercent": 100, "upgradePercent": 100, "modelType": 0, @@ -1412,7 +1466,11 @@ "message": "" } }, - "ImageStage_test_wait_for_current_actions_to_complete_timeout": { + "test_image_mgmt_stage_00011a": { + "TEST_NOTES": [ + "FDO21120U5D imageStaged: Success", + "FDO2112189M imageStaged: In-Progress" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1502,7 +1560,11 @@ "message": "" } }, - "ImageStage_test_commit_payload_serial_number_key_name": { + "test_image_mgmt_stage_00012a": { + "TEST_NOTES": [ + "FDO21120U5D validated, upgrade, imageStaged: Success", + "FDO2112189M validated, upgrade, imageStaged: Success" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1592,6 +1654,101 @@ "message": "" } }, + "test_image_mgmt_stage_00013a": { + "TEST_NOTES": [ + "FDO21120U5D imageStaged: Success", + "FDO2112189M imageStaged: In-Progress", + "FDO2112189M imageStagedPercent: 50" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "In-Progress", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 50, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, "ImageValidate_test_prune_serial_numbers": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py index 02c2d2eb0..e53f8f1f0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py @@ -33,17 +33,22 @@ def does_not_raise(): dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" -def responses_issu_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchIssuDetails" +def responses_controller_version(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ControllerVersion" response = load_fixture(response_file).get(key) - print(f"responses_issu_details: {key} : {response}") + print(f"responses_controller_version: {key} : {response}") return response +def responses_image_stage(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ImageStage" + response = load_fixture(response_file).get(key) + print(f"responses_image_stage: {key} : {response}") + return response -def responses_controller_version(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ControllerVersion" +def responses_issu_details(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) - print(f"responses_controller_version: {key} : {response}") + print(f"responses_issu_details: {key} : {response}") return response @@ -64,10 +69,11 @@ def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) -# test_init +# test_image_mgmt_stage_00001 +# test_init (former names) -def test_init(module) -> None: +def test_image_mgmt_stage_00001(module) -> None: """ class attributes are initialized to expected values """ @@ -83,11 +89,11 @@ class attributes are initialized to expected values assert isinstance(module.issu_detail, SwitchIssuDetailsBySerialNumber) assert isinstance(module.endpoints, ApiEndpoints) +# test_image_mgmt_stage_00002 +# test_init_properties (former name) -# test_init_properties - -def test_init_properties(module) -> None: +def test_image_mgmt_stage_00002(module) -> None: """ Properties are initialized to expected values """ @@ -101,29 +107,30 @@ def test_init_properties(module) -> None: assert module.properties.get("check_timeout") == 1800 -# test_populate_controller_version +# test_image_mgmt_stage_00003 +# test_populate_controller_version (former name) @pytest.mark.parametrize( "key, expected", [ - ("ImageStage_12_1_2e", "12.1.2e"), - ("ImageStage_12_1_3b", "12.1.3b"), + ("test_image_mgmt_stage_00003a", "12.1.2e"), + ("test_image_mgmt_stage_00003b", "12.1.3b"), ], ) -def test_populate_controller_version(monkeypatch, module, key, expected) -> None: +def test_image_mgmt_stage_00003(monkeypatch, module, key, expected) -> None: """ - _populate_controller_version retrieves the controller version from NDFC. - This is used in commit() to populate the payload with either a misspelled - "sereialNum" key/value (12.1.2e) or a correctly-spelled "serialNumbers" - key/value (12.1.3b). + _populate_controller_version retrieves the controller version from + the controller. This is used in commit() to populate the payload + with either a misspelled "sereialNum" key/value (12.1.2e) or a + correctly-spelled "serialNumbers" key/value (12.1.3b). Expectations: 1. module.controller_version should be set Expected results: - 1. ImageStage_12_1_2e -> module.controller_version == "12.1.2e" - 2. ImageStage_12_1_3b -> module.controller_version == "12.1.3b" + 1. test_image_mgmt_stage_00003a -> module.controller_version == "12.1.2e" + 2. test_image_mgmt_stage_00003b -> module.controller_version == "12.1.3b" """ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: @@ -135,10 +142,11 @@ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: assert module.controller_version == expected -# test_prune_serial_numbers +# test_image_mgmt_stage_00004 +# test_prune_serial_numbers (former name) -def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_stage_00004(monkeypatch, module, mock_issu_details) -> None: """ prune_serial_numbers removes serial numbers from the list for which imageStaged == "Success" (TODO: AND policy == ) @@ -155,7 +163,7 @@ def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_prune_serial_numbers" + key = "test_image_mgmt_stage_00004a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -178,10 +186,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO211218GC" not in module.serial_numbers -# test_validate_serial_numbers_failed +# test_image_mgmt_stage_00005 +# test_validate_serial_numbers_failed (former name) -def test_validate_serial_numbers_failed(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_stage_00005(monkeypatch, module, mock_issu_details) -> None: """ fail_json is called when imageStaged == "Failed". @@ -192,7 +201,7 @@ def test_validate_serial_numbers_failed(monkeypatch, module, mock_issu_details) """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_validate_serial_numbers" + key = "test_image_mgmt_stage_00005a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -221,7 +230,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: (False, pytest.raises(AnsibleFailJson, match=match)), ], ) -def test_commit_serial_numbers( +def test_image_mgmt_stage_00006( monkeypatch, module, serial_numbers_is_set, expected ) -> None: """ @@ -235,20 +244,20 @@ def test_commit_serial_numbers( """ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: - key = "ControllerVersion_get_return_code_200" + key = "test_image_mgmt_stage_00006a" return responses_controller_version(key) def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_validate_serial_numbers" - return responses_issu_details(key) + key = "test_image_mgmt_stage_00006a" + return responses_image_stage(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_validate_serial_numbers" + key = "test_image_mgmt_stage_00006a" return responses_issu_details(key) + monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) if serial_numbers_is_set: module.serial_numbers = ["FDO21120U5D"] @@ -256,10 +265,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.commit() -# test_commit_path_verb +# test_image_mgmt_stage_00007 +# test_commit_path_verb (former name) -def test_commit_path_verb(monkeypatch, module) -> None: +def test_image_mgmt_stage_00007(monkeypatch, module) -> None: """ ImageStage.path should be set to: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image @@ -273,20 +283,21 @@ def test_commit_path_verb(monkeypatch, module) -> None: """ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: - key = "ControllerVersion_get_return_code_200" + key = "test_image_mgmt_stage_00007a" return responses_controller_version(key) + # Needed only for the 200 return code def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_validate_serial_numbers" - return responses_issu_details(key) + key = "test_image_mgmt_stage_00007a" + return responses_image_stage(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_validate_serial_numbers" + key = "test_image_mgmt_stage_00007a" return responses_issu_details(key) + monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) module.serial_numbers = ["FDO21120U5D"] module.commit() @@ -297,7 +308,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert module.verb == "POST" -# test_commit_payload_serial_number_key_name +# test_image_mgmt_stage_00008 +# test_commit_payload_serial_number_key_name (former name) @pytest.mark.parametrize( @@ -307,7 +319,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: ("12.1.3b", "serialNumbers"), ], ) -def test_commit_payload_serial_number_key_name( +def test_image_mgmt_stage_00008( monkeypatch, module, controller_version, expected_serial_number_key ) -> None: """ @@ -330,12 +342,14 @@ def mock_controller_version(*args, **kwargs) -> None: controller_version_patch += "ImageStage._populate_controller_version" monkeypatch.setattr(controller_version_patch, mock_controller_version) + # Needed only for the 200 return code def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_validate_serial_numbers" - return responses_issu_details(key) + key = "test_image_mgmt_stage_00008a" + return responses_image_stage(key) + # Needed only for the 200 response def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_commit_payload_serial_number_key_name" + key = "test_image_mgmt_stage_00008a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) @@ -346,10 +360,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert expected_serial_number_key in module.payload.keys() -# test_wait_for_image_stage_to_complete +# test_image_mgmt_stage_00009 +# test_wait_for_image_stage_to_complete (former name) -def test_wait_for_image_stage_to_complete( +def test_image_mgmt_stage_00009( monkeypatch, module, mock_issu_details ) -> None: """ @@ -366,7 +381,7 @@ def test_wait_for_image_stage_to_complete( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_wait_for_image_stage_to_complete" + key = "test_image_mgmt_stage_00009a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -384,10 +399,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" in module.serial_numbers_done -# test_wait_for_image_stage_to_complete_stage_failed +# test_image_mgmt_stage_00010 +# test_wait_for_image_stage_to_complete_stage_failed (former name) -def test_wait_for_image_stage_to_complete_stage_failed( +def test_image_mgmt_stage_00010( monkeypatch, module, mock_issu_details ) -> None: """ @@ -404,7 +420,7 @@ def test_wait_for_image_stage_to_complete_stage_failed( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_wait_for_image_stage_to_complete_fail_json" + key = "test_image_mgmt_stage_00010a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -415,10 +431,10 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: "FDO2112189M", ] module.check_interval = 0 - error_message = "Seconds remaining 1800: stage image failed for " - error_message += "cvd-2313-leaf, FDO2112189M, 172.22.150.108. image " - error_message += "staged percent: 90" - with pytest.raises(AnsibleFailJson, match=error_message): + match = "Seconds remaining 1800: stage image failed for " + match += "cvd-2313-leaf, FDO2112189M, 172.22.150.108. image " + match += "staged percent: 90" + with pytest.raises(AnsibleFailJson, match=match): module._wait_for_image_stage_to_complete() assert isinstance(module.serial_numbers_done, set) assert len(module.serial_numbers_done) == 1 @@ -426,10 +442,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" not in module.serial_numbers_done +# test_image_mgmt_stage_00011 # test_wait_for_image_stage_to_complete_timout -def test_wait_for_image_stage_to_complete_timout( +def test_image_mgmt_stage_00011( monkeypatch, module, mock_issu_details ) -> None: """ @@ -444,7 +461,7 @@ def test_wait_for_image_stage_to_complete_timout( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_wait_for_image_stage_to_complete_timeout" + key = "test_image_mgmt_stage_00011a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -457,11 +474,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.check_interval = 1 module.check_timeout = 1 - error_message = "ImageStage._wait_for_image_stage_to_complete: " - error_message += "Timed out waiting for image stage to complete. " - error_message += "serial_numbers_done: FDO21120U5D, " - error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" - with pytest.raises(AnsibleFailJson, match=error_message): + match = "ImageStage._wait_for_image_stage_to_complete: " + match += "Timed out waiting for image stage to complete. " + match += "serial_numbers_done: FDO21120U5D, " + match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + with pytest.raises(AnsibleFailJson, match=match): module._wait_for_image_stage_to_complete() assert isinstance(module.serial_numbers_done, set) assert len(module.serial_numbers_done) == 1 @@ -469,10 +486,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" not in module.serial_numbers_done -# test_wait_for_current_actions_to_complete +# test_image_mgmt_stage_00012 +# test_wait_for_current_actions_to_complete (former name) -def test_wait_for_current_actions_to_complete( +def test_image_mgmt_stage_00012( monkeypatch, module, mock_issu_details ) -> None: """ @@ -493,7 +511,7 @@ def test_wait_for_current_actions_to_complete( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_wait_for_current_actions_to_complete" + key = "test_image_mgmt_stage_00012a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -510,11 +528,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO21120U5D" in module.serial_numbers_done assert "FDO2112189M" in module.serial_numbers_done - -# test_wait_for_current_actions_to_complete_timout +# test_image_mgmt_stage_00013 +# test_wait_for_current_actions_to_complete_timout (former name) -def test_wait_for_current_actions_to_complete_timout( +def test_image_mgmt_stage_00013( monkeypatch, module, mock_issu_details ) -> None: """ @@ -529,7 +547,7 @@ def test_wait_for_current_actions_to_complete_timout( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageStage_test_wait_for_current_actions_to_complete_timeout" + key = "test_image_mgmt_stage_00013a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) From 9b4e6b8cbeabc6cea664d184db57302baf1d91d9 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 5 Nov 2023 16:11:00 -1000 Subject: [PATCH 047/300] Consistent naming for test cases, more... Add test_image_mgmt_validate_00001 for ImageValidate.__init__ --- ...e_upgrade_responses_SwitchIssuDetails.json | 48 ++++++++-- .../test_image_upgrade_ImageValidate.py | 93 +++++++++++++------ 2 files changed, 108 insertions(+), 33 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 98915306b..c8e9900c3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -1749,7 +1749,14 @@ "message": "" } }, - "ImageValidate_test_prune_serial_numbers": { + "test_image_mgmt_validate_00003a": { + "TEST_NOTES": [ + "FDO2112189M validated: none", + "FDO211218AX validated: none", + "FDO211218B5 validated: none", + "FDO211218FV validated: Success", + "FDO211218GC validated: Success" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1956,7 +1963,11 @@ "message": "" } }, - "ImageValidate_test_validate_serial_numbers": { + "test_image_mgmt_validate_00004a": { + "TEST_NOTES": [ + "FDO21120U5D validated: Success", + "FDO2112189M validated: Failed" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2046,7 +2057,11 @@ "message": "" } }, - "ImageValidate_test_wait_for_image_validate_to_complete": { + "test_image_mgmt_validate_00005a": { + "TEST_NOTES": [ + "FDO21120U5D validated: Success", + "FDO2112189M validated: Success" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2136,7 +2151,11 @@ "message": "" } }, - "ImageValidate_test_wait_for_image_validate_to_complete_fail_json": { + "test_image_mgmt_validate_00006a": { + "TEST_NOTES": [ + "FDO21120U5D validated: Success", + "FDO2112189M validated: Failed" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2226,7 +2245,13 @@ "message": "" } }, - "ImageValidate_test_wait_for_image_validate_to_complete_timeout": { + "test_image_mgmt_validate_00007a": { + "TEST_NOTES": [ + "FDO21120U5D validated: Success", + "FDO21120U5D imageStagedPercent: 100", + "FDO2112189M validated: In-Progress", + "FDO2112189M imageStagedPercent: 50" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2316,7 +2341,11 @@ "message": "" } }, - "ImageValidate_test_wait_for_current_actions_to_complete": { + "test_image_mgmt_validate_00008a": { + "TEST_NOTES": [ + "FDO21120U5D imageStaged, upgrade, validated: Success", + "FDO2112189M imageStaged, upgrade, validated: Success" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2406,7 +2435,12 @@ "message": "" } }, - "ImageValidate_test_wait_for_current_actions_to_complete_timeout": { + "test_image_mgmt_validate_00009a": { + "TEST_NOTES": [ + "FDO21120U5D imageStaged, upgrade, validated: Success", + "FDO2112189M imageStaged, upgrade: Success", + "FDO2112189M validated: In-Progress" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index 2dd5b365f..6041fb522 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -8,6 +8,8 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ ImageValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ @@ -45,7 +47,24 @@ def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) -def test_init_properties(module) -> None: +# test_image_mgmt_validate_00001 + + +def test_image_mgmt_validate_00001(module) -> None: + """ + __init__ + """ + module.__init__(MockAnsibleModule) + assert module.class_name == "ImageValidate" + assert isinstance(module.endpoints, ApiEndpoints) + assert isinstance(module.issu_detail, SwitchIssuDetailsBySerialNumber) + + +# test_image_mgmt_validate_00002 +# test_init_properties (former name) + + +def test_image_mgmt_validate_00002(module) -> None: """ Properties are initialized to expected values """ @@ -60,7 +79,11 @@ def test_init_properties(module) -> None: assert module.properties.get("serial_numbers") == [] -def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: +# test_image_mgmt_validate_00003 +# test_prune_serial_numbers (former name) + + +def test_image_mgmt_validate_00003(monkeypatch, module, mock_issu_details) -> None: """ prune_serial_numbers removes serial numbers from the list for which "validated" == "Success" (TODO: AND policy == ) @@ -77,7 +100,7 @@ def test_prune_serial_numbers(monkeypatch, module, mock_issu_details) -> None: """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageValidate_test_prune_serial_numbers" + key = "test_image_mgmt_validate_00003a" return response_data_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -100,7 +123,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO211218GC" not in module.serial_numbers -def test_validate_serial_numbers_failed(monkeypatch, module, mock_issu_details) -> None: +# test_image_mgmt_validate_00004 +# test_validate_serial_numbers_failed (former name) + + +def test_image_mgmt_validate_00004(monkeypatch, module, mock_issu_details) -> None: """ fail_json is called when imageStaged == "Failed". @@ -111,7 +138,7 @@ def test_validate_serial_numbers_failed(monkeypatch, module, mock_issu_details) """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageValidate_test_validate_serial_numbers" + key = "test_image_mgmt_validate_00004a" return response_data_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -128,9 +155,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.validate_serial_numbers() -def test_wait_for_image_validate_to_complete( - monkeypatch, module, mock_issu_details -) -> None: +# test_image_mgmt_validate_00005 +# test_wait_for_image_validate_to_complete (former name) + + +def test_image_mgmt_validate_00005(monkeypatch, module, mock_issu_details) -> None: """ _wait_for_image_validate_to_complete looks at the "validated" status for each serial number and waits for it to be "Success" or "Failed". @@ -146,7 +175,7 @@ def test_wait_for_image_validate_to_complete( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageValidate_test_wait_for_image_validate_to_complete" + key = "test_image_mgmt_validate_00005a" return response_data_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -164,11 +193,13 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" in module.serial_numbers_done -def test_wait_for_image_validate_to_complete_validate_failed( - monkeypatch, module, mock_issu_details -) -> None: +# test_image_mgmt_validate_00006 +# test_wait_for_image_validate_to_complete_validate_failed (former name) + + +def test_image_mgmt_validate_00006(monkeypatch, module, mock_issu_details) -> None: """ - _wait_for_image_validate_to_complete looks at the "validate" status for each + _wait_for_image_validate_to_complete looks at the "validated" status for each serial number and waits for it to be "Success" or "Failed". In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. @@ -181,7 +212,7 @@ def test_wait_for_image_validate_to_complete_validate_failed( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageValidate_test_wait_for_image_validate_to_complete_fail_json" + key = "test_image_mgmt_validate_00006a" return response_data_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -207,12 +238,16 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" not in module.serial_numbers_done -def test_wait_for_image_validate_to_complete_timeout( - monkeypatch, module, mock_issu_details -) -> None: +# test_image_mgmt_validate_00007 +# test_wait_for_image_validate_to_complete_timeout (former name) + + +def test_image_mgmt_validate_00007(monkeypatch, module, mock_issu_details) -> None: """ See test_wait_for_image_stage_to_complete for functional details. + Since FDO2112189M validated == "In-Progress" the function should timeout + Expectations: 1. module.serial_numbers_done should be a set() 2. module.serial_numbers_done should be length 1 @@ -222,7 +257,7 @@ def test_wait_for_image_validate_to_complete_timeout( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageValidate_test_wait_for_image_validate_to_complete_timeout" + key = "test_image_mgmt_validate_00007a" return response_data_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -247,9 +282,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" not in module.serial_numbers_done -def test_wait_for_current_actions_to_complete( - monkeypatch, module, mock_issu_details -) -> None: +# test_image_mgmt_validate_00008 +# test_wait_for_current_actions_to_complete (former name) + + +def test_image_mgmt_validate_00008(monkeypatch, module, mock_issu_details) -> None: """ _wait_for_current_actions_to_complete waits until staging, validation, and upgrade actions are complete for all serial numbers. It calls @@ -268,7 +305,7 @@ def test_wait_for_current_actions_to_complete( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageValidate_test_wait_for_current_actions_to_complete" + key = "test_image_mgmt_validate_00008a" return response_data_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) @@ -286,12 +323,16 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" in module.serial_numbers_done -def test_wait_for_current_actions_to_complete_timeout( - monkeypatch, module, mock_issu_details -) -> None: +# test_image_mgmt_validate_00009 +# test_wait_for_current_actions_to_complete_timeout (former name) + + +def test_image_mgmt_validate_00009(monkeypatch, module, mock_issu_details) -> None: """ See test_wait_for_current_actions_to_complete for functional details. + Since FDO2112189M validated == "In-Progress" the function should timeout + Expectations: 1. module.serial_numbers_done should be a set() 2. module.serial_numbers_done should be length 1 @@ -301,7 +342,7 @@ def test_wait_for_current_actions_to_complete_timeout( """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageValidate_test_wait_for_current_actions_to_complete_timeout" + key = "test_image_mgmt_validate_00009a" return response_data_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) From d7e7fea37709dc2e4eb63d62fb231d6a0d057e68 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 08:54:20 -1000 Subject: [PATCH 048/300] Standardize test names --- ...upgrade_responses_ImageInstallOptions.json | 13 ++- .../test_image_upgrade_ImageInstallOptions.py | 83 ++++++++++++++++--- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json index e41654f2d..4a4d03b28 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json @@ -1,5 +1,11 @@ { - "imageupgrade_install_options_post_return_code_200": { + "test_image_mgmt_install_options_00005a": { + "TEST_NOTES": [ + "All attributes in compatibilityStatusList are tested", + "epldModules is tested", + "installPacakges (yes, misspelled) is tested", + "errMessage is tested" + ], "RETURN_CODE": 200, "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", @@ -26,7 +32,10 @@ "errMessage": "" } }, - "imageupgrade_install_options_post_return_code_500": { + "test_image_mgmt_install_options_00006a": { + "TEST_NOTES": [ + "RETURN_CODE 500 is tested" + ], "RETURN_CODE": 500, "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index caa6be694..33337930a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -8,6 +8,8 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ ImageInstallOptions @@ -38,7 +40,37 @@ def module(): return ImageInstallOptions(MockAnsibleModule) -def test_policy_name_not_defined(module) -> None: +def test_image_mgmt_install_options_00001(module) -> None: + """ + Verify attributes set in __init__ + """ + module.__init__(MockAnsibleModule) + assert module.module == MockAnsibleModule + assert module.class_name == "ImageInstallOptions" + assert isinstance(module.endpoints, ApiEndpoints) + + +def test_image_mgmt_install_options_00002(module) -> None: + """ + Properties are initialized to expected values + """ + module._init_properties() + assert isinstance(module.properties, dict) + assert module.properties.get("epld") == False + assert module.properties.get("epld_modules") == None + assert module.properties.get("issu") == True + assert module.properties.get("package_install") == False + assert module.properties.get("policy_name") == None + assert module.properties.get("response") == None + assert module.properties.get("response_data") == None + assert module.properties.get("result") == None + assert module.properties.get("serial_number") == None + + +# test_image_mgmt_install_options_00003 +# test_policy_name_not_defined (former name) + +def test_image_mgmt_install_options_00003(module) -> None: """ fail_json() is called if policy_name is not set when refresh() is called. """ @@ -49,8 +81,10 @@ def test_policy_name_not_defined(module) -> None: with pytest.raises(AnsibleFailJson, match=match): module.refresh() +# test_image_mgmt_install_options_00004 +# test_serial_number_not_defined (former name) -def test_serial_number_not_defined(module) -> None: +def test_image_mgmt_install_options_00004(module) -> None: """ fail_json() is called if serial_number is not set when refresh() is called. """ @@ -62,14 +96,18 @@ def test_serial_number_not_defined(module) -> None: module.refresh() -def test_refresh_return_code_200(monkeypatch, module) -> None: +# test_image_mgmt_install_options_00005 +# test_refresh_return_code_200 (former name) + + +def test_image_mgmt_install_options_00005(monkeypatch, module) -> None: """ Properties are updated based on 200 response from endpoint. endpoint: install-options """ - key = "imageupgrade_install_options_post_return_code_200" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00005a" return responses_image_install_options(key) monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) @@ -92,13 +130,16 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert module.result.get("success") == True -def test_refresh_return_code_500(monkeypatch, module) -> None: +# test_image_mgmt_install_options_00006 +# test_refresh_return_code_500 (former name) + +def test_image_mgmt_install_options_00006(monkeypatch, module) -> None: """ fail_json() should be called if the response RETURN_CODE != 200 """ - key = "imageupgrade_install_options_post_return_code_500" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00006a" return responses_image_install_options(key) monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) @@ -112,7 +153,11 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: module.refresh() -def test_build_payload_defaults(module) -> None: +# test_image_mgmt_install_options_00007 +# test_build_payload_defaults (former name) + + +def test_image_mgmt_install_options_00007(module) -> None: """ Payload contains defaults if not specified by the user. Defaults for issu, epld, and package_install are applied. @@ -127,10 +172,14 @@ def test_build_payload_defaults(module) -> None: assert module.payload.get("packageInstall") == False -def test_build_payload_user_changed_defaults(module) -> None: +# test_image_mgmt_install_options_00008 +# test_build_payload_user_changed_defaults (former name) + + +def test_image_mgmt_install_options_00008(module) -> None: """ Payload contains user-specified values if the user sets them. - Defaults for issu, epld, and package_install overridden by user values. + Defaults for issu, epld, and package_install are overridden by user values. """ module.policy_name = "KRM5" module.serial_number = "BAR" @@ -145,7 +194,11 @@ def test_build_payload_user_changed_defaults(module) -> None: assert module.payload.get("packageInstall") == True -def test_invalid_value_issu(module) -> None: +# test_image_mgmt_install_options_00009 +# test_invalid_value_issu (former name) + + +def test_image_mgmt_install_options_00009(module) -> None: """ fail_json() is called if issu is not a boolean. """ @@ -155,7 +208,11 @@ def test_invalid_value_issu(module) -> None: module.issu = "FOO" -def test_invalid_value_epld(module) -> None: +# test_image_mgmt_install_options_00010 +# test_invalid_value_epld (former name) + + +def test_image_mgmt_install_options_00010(module) -> None: """ fail_json() is called if epld is not a boolean. """ @@ -165,6 +222,10 @@ def test_invalid_value_epld(module) -> None: module.epld = "FOO" +# test_image_mgmt_install_options_00011 +# test_invalid_value_package_install (former name) + + def test_invalid_value_package_install(module) -> None: """ fail_json() is called if package_install is not a boolean. From 0311501f0f9ab7c74748d07a8d8c5c910b4f8ba5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 13:23:22 -1000 Subject: [PATCH 049/300] Use install-options to abort upgrade in case of incongruent options. --- .../module_utils/image_mgmt/image_upgrade.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index baf4374cb..9458f1195 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -8,6 +8,8 @@ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ + ImageInstallOptions from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsByIpAddress from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ @@ -119,6 +121,7 @@ def __init__(self, module): self._init_defaults() self._init_properties() self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) + self.install_options = ImageInstallOptions(self.module) def _init_defaults(self) -> None: self.method_name = inspect.stack()[0][3] @@ -307,6 +310,20 @@ def build_payload(self, device) -> None: self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() + self.install_options.serial_number = self.issu_detail.serial_number + self.install_options.policy_name = device.get("policy") + self.install_options.epld = device.get("upgrade").get("epld") + self.install_options.nxos = device.get("upgrade").get("nxos") + self.install_options.package_install = device.get("options").get("package").get("install") + self.install_options.refresh() + # ImageInstallOptions may fail_json in the refresh() above, which is + # fine since it will contain more detailed information. + if self.install_options.result["success"] is False: + msg = f"{self.class_name}.{self.method_name}: " + msg += f"failed: {self.install_options.result}. " + msg += f"Controller response: {self.install_options.response}" + self.module.fail_json(msg) + # devices_to_upgrade must currently be a single device devices_to_upgrade: List[dict] = [] @@ -485,6 +502,14 @@ def _wait_for_current_actions_to_complete(self): self.ipv4_done.add(ipv4) continue + msg = f"{self.class_name}.{self.method_name}: " + msg += f"Seconds remaining {timeout}. Waiting " + msg += f" on ipv4 {ipv4} actions to complete. " + msg += f"staged_percent {self.issu_detail.image_staged_percent}, " + msg += f"validated_percent {self.issu_detail.validated_percent}, " + msg += f"upgrade_percent {self.issu_detail.upgrade_percent}" + self.log_msg(msg) + if self.ipv4_done != self.ipv4_todo: msg = f"{self.class_name}.{self.method_name}: " msg += "Timed out while waiting for actions in progress " @@ -525,7 +550,8 @@ def _wait_for_image_upgrade_to_complete(self): msg = f"{self.class_name}.{self.method_name}: " msg += f"Seconds remaining {timeout}: upgrade image " msg += f"{upgrade_status} for " - msg += f"{device_name}, {serial_number}, {ip_address}" + msg += f"{device_name}, {serial_number}, {ip_address}, " + msg += f"upgrade_percent {upgrade_percent}." self.module.fail_json(msg) if upgrade_status == "Success": From c14cf4d4bb95bee3f4ed4e5ba646c7887e134f90 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 13:25:11 -1000 Subject: [PATCH 050/300] Better error when user sets package_install incorrectly --- .../module_utils/image_mgmt/install_options.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index e93ae8a1f..cb3d9394c 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -159,17 +159,25 @@ def refresh(self) -> None: self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) + self.properties["response_data"] = self.response.get("DATA", {}) self.properties["result"] = self._handle_response(self.response, self.verb) if self.result["success"] is False: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retrieving install-options from " - msg += f"the controller. Controller response: {self.response}" + msg += f"the controller. Controller response: {self.response}. " + if "does not have package to continue" in self.response_data.get("error", ""): + msg += f"Possible cause: Image policy {self.policy_name} does not have " + msg += "a package defined, and package_install is set to " + msg += "True in the playbook." self.module.fail_json(msg) self.properties["response_data"] = self.response.get("DATA", {}) - self.compatibility_status = self.response_data.get("compatibilityStatusList", [{}])[0] + if self.response_data.get("compatibilityStatusList") is None: + self.compatibility_status = {} + else: + self.compatibility_status = self.response_data.get("compatibilityStatusList", [{}])[0] def _build_payload(self) -> None: @@ -197,7 +205,11 @@ def _build_payload(self) -> None: self.payload["packageInstall"] = self.package_install def _get(self, item): - self.response_data.get(item) + return self.make_boolean( + self.make_none( + self.response_data.get(item) + ) + ) # Mandatory properties @property From a8389b48def3ea53be354acfe84d7dd4448dc445 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 13:26:06 -1000 Subject: [PATCH 051/300] More robust handling of response_data --- plugins/module_utils/image_mgmt/switch_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index 2f9ac791b..47a0bfffb 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -79,12 +79,12 @@ def _get(self, item): msg += f"property {item}." self.module.fail_json(msg) - if self.properties["response_data"].get(self.ip_address) is None: + if self.ip_address not in self.properties["response_data"]: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.ip_address} does not exist on the controller." self.module.fail_json(msg) - if self.properties["response_data"][self.ip_address].get(item) is None: + if item not in self.properties["response_data"][self.ip_address]: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.ip_address} does not have a key named {item}." self.module.fail_json(msg) From 103415d7245055800b2f24d9f590001aaae2d5b3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 13:32:16 -1000 Subject: [PATCH 052/300] Let the user know which device is failing. --- plugins/module_utils/image_mgmt/install_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index cb3d9394c..fcb4197c4 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -169,7 +169,7 @@ def refresh(self) -> None: if "does not have package to continue" in self.response_data.get("error", ""): msg += f"Possible cause: Image policy {self.policy_name} does not have " msg += "a package defined, and package_install is set to " - msg += "True in the playbook." + msg += f"True in the playbook for device {self.serial_number}." self.module.fail_json(msg) self.properties["response_data"] = self.response.get("DATA", {}) From c95b5e0445e730be01d4353b78843a6dc064f028 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 14:06:36 -1000 Subject: [PATCH 053/300] More unit tests --- ...upgrade_responses_ImageInstallOptions.json | 166 ++++++++++++++ .../test_image_upgrade_ImageInstallOptions.py | 207 ++++++++++++++++-- 2 files changed, 358 insertions(+), 15 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json index 4a4d03b28..029971fc5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json @@ -43,5 +43,171 @@ "DATA": { "error": "null" } + }, + "test_image_mgmt_install_options_00007a": { + "TEST_NOTES": [ + "POST REQUEST contents: issu == true, epld == false, packageInstall false", + "Device has no policy attached", + "NDFC version: 12.1.3b", + "All attributes in compatibilityStatusList are tested", + "epldModules is tested", + "installPacakges (yes, misspelled) is tested", + "errMessage is tested" + ], + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "MESSAGE": "OK", + "DATA": { + "compatibilityStatusList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "policyName": "KR5M", + "platform": "N9K/N3K", + "version": "10.2.5", + "osType": "64bit", + "status": "Skipped", + "installOption": "NA", + "compDisp": "Compatibility status skipped.", + "versionCheck": "Compatibility status skipped.", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "timestamp": "NA" + } + ], + "epldModules": null, + "installPacakges": null, + "errMessage": "" + } + }, + "test_image_mgmt_install_options_00008a": { + "TEST_NOTES": [ + "POST REQUEST contents: issu == true, epld == true, packageInstall false", + "Device has no policy attached", + "NDFC version: 12.1.3b", + "All attributes in compatibilityStatusList are tested", + "epldModules is tested to be a dict", + "epldModules.moduleList length is tested to be 2", + "installPacakges (yes, misspelled) is tested", + "errMessage is tested" + ], + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "MESSAGE": "OK", + "DATA": { + "compatibilityStatusList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "policyName": "KR5M", + "platform": "N9K/N3K", + "version": "10.2.5", + "osType": "64bit", + "status": "Skipped", + "installOption": "NA", + "compDisp": "Compatibility status skipped.", + "versionCheck": "Compatibility status skipped.", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "timestamp": "NA" + } + ], + "epldModules": { + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "IO FPGA", + "oldVersion": "0x15", + "newVersion": "0x15" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "MI FPGA", + "oldVersion": "0x4", + "newVersion": "0x04" + } + ], + "bException": false, + "exceptionReason": null + }, + "installPacakges": null, + "errMessage": "" + } + }, + "test_image_mgmt_install_options_00009a": { + "TEST_NOTES": [ + "POST REQUEST contents: issu == false, epld == true, packageInstall false", + "Device has no policy attached", + "NDFC version: 12.1.3b", + "compatibilityStatusList is tested to be None", + "epldModules is tested to be a dict", + "epldModules.moduleList length is tested to be 2", + "installPacakges (yes, misspelled) is tested", + "errMessage is tested" + ], + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "MESSAGE": "OK", + "DATA": { + "compatibilityStatusList": null, + "epldModules": { + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "IO FPGA", + "oldVersion": "0x15", + "newVersion": "0x15" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "policyName": "KR5M", + "module": 1, + "name": null, + "modelName": "N9K-C93180YC-EX", + "moduleType": "MI FPGA", + "oldVersion": "0x4", + "newVersion": "0x04" + } + ], + "bException": false, + "exceptionReason": null + }, + "installPacakges": null, + "errMessage": "" + } + }, + "test_image_mgmt_install_options_00010a": { + "TEST_NOTES": [ + "POST REQUEST contents: issu == true, epld == true, packageInstall true", + "RETURN_CODE is 500 due to packageInstall is true, but policy contains no packages", + "Device has KR5M policy attached", + "NDFC version: 12.1.3b" + ], + "RETURN_CODE": 500, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "MESSAGE": "Internal Server Error", + "DATA": { + "error": "Selected policy KR5M does not have package to continue." + } } } diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index 33337930a..58e0480b9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -121,9 +121,18 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert module.epld_modules is None assert module.install_option == "disruptive" assert module.install_packages is None + assert module.ip_address == "172.22.150.105" assert module.os_type == "64bit" assert module.platform == "N9K/N3K" + assert module.pre_issu_link == "Not Applicable" + assert isinstance(module.raw_data, dict) + assert isinstance(module.raw_response, dict) + assert "compatibilityStatusList" in module.raw_data + print("module.raw_data: ", module.raw_data) + assert module.rep_status == "skipped" assert module.serial_number == "BAR" + assert module.status == "Success" + assert module.timestamp == "NA" assert module.version == "10.2.5" comp_disp = "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin" assert module.comp_disp == comp_disp @@ -153,11 +162,183 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: module.refresh() -# test_image_mgmt_install_options_00007 -# test_build_payload_defaults (former name) +def test_image_mgmt_install_options_00007(monkeypatch, module) -> None: + """ + Properties are updated based on: + - 200 response from endpoint + - Device has no policy attached + - POST REQUEST + - issu == True + - epld == False + - package_install == False + + endpoint: install-options + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00007a" + return responses_image_install_options(key) + + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + + module.policy_name = "KRM5" + module.serial_number = "FDO21120U5D" + module.refresh() + assert isinstance(module.response, dict) + assert module.device_name == "leaf1" + assert module.err_message is None + assert module.epld_modules is None + assert module.install_option == "NA" + assert module.install_packages is None + assert module.ip_address == "172.22.150.102" + assert module.os_type == "64bit" + assert module.platform == "N9K/N3K" + assert module.pre_issu_link == "Not Applicable" + assert isinstance(module.raw_data, dict) + assert isinstance(module.raw_response, dict) + assert "compatibilityStatusList" in module.raw_data + print("module.raw_data: ", module.raw_data) + assert module.rep_status == "skipped" + assert module.serial_number == "FDO21120U5D" + assert module.status == "Skipped" + assert module.timestamp == "NA" + assert module.version == "10.2.5" + assert module.version_check == "Compatibility status skipped." + assert module.comp_disp == "Compatibility status skipped." + assert module.result.get("success") == True + + +def test_image_mgmt_install_options_00008(monkeypatch, module) -> None: + """ + Properties are updated based on: + - 200 response from endpoint + - Device has no policy attached + - POST REQUEST + - issu == True + - epld == True + - package_install == False + + endpoint: install-options + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00008a" + return responses_image_install_options(key) + + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + + module.policy_name = "KRM5" + module.serial_number = "FDO21120U5D" + module.epld = True + module.issu = True + module.package_install = False + module.refresh() + assert isinstance(module.response, dict) + assert module.device_name == "leaf1" + assert module.err_message is None + assert isinstance(module.epld_modules, dict) + assert len(module.epld_modules.get("moduleList")) == 2 + assert module.install_option == "NA" + assert module.install_packages is None + assert module.ip_address == "172.22.150.102" + assert module.os_type == "64bit" + assert module.platform == "N9K/N3K" + assert module.pre_issu_link == "Not Applicable" + assert isinstance(module.raw_data, dict) + assert isinstance(module.raw_response, dict) + assert "compatibilityStatusList" in module.raw_data + assert module.rep_status == "skipped" + assert module.serial_number == "FDO21120U5D" + assert module.status == "Skipped" + assert module.timestamp == "NA" + assert module.version == "10.2.5" + assert module.version_check == "Compatibility status skipped." + assert module.comp_disp == "Compatibility status skipped." + assert module.result.get("success") == True + + +def test_image_mgmt_install_options_00009(monkeypatch, module) -> None: + """ + Properties are updated based on: + - 200 response from endpoint + - Device has no policy attached + - POST REQUEST + - issu == False + - epld == True + - package_install == False + + endpoint: install-options + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00009a" + return responses_image_install_options(key) + + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + + module.policy_name = "KRM5" + module.serial_number = "FDO21120U5D" + module.epld = True + module.issu = False + module.package_install = False + module.refresh() + assert isinstance(module.response, dict) + assert module.device_name is None + assert module.err_message is None + assert isinstance(module.epld_modules, dict) + assert len(module.epld_modules.get("moduleList")) == 2 + assert module.install_option == None + assert module.install_packages is None + assert module.ip_address == None + assert module.os_type == None + assert module.platform == None + assert module.pre_issu_link == None + assert isinstance(module.raw_data, dict) + assert isinstance(module.raw_response, dict) + assert "compatibilityStatusList" in module.raw_data + assert module.rep_status == None + assert module.serial_number == "FDO21120U5D" + assert module.status == None + assert module.timestamp == None + assert module.version == None + assert module.version_check == None + assert module.comp_disp == None + assert module.result.get("success") == True + +def test_image_mgmt_install_options_00010(monkeypatch, module) -> None: + """ + Properties are updated based on: + - 500 response from endpoint due to KR5M policy has no packages defined + and package_install set to True + - KR5M policy is attached to the device + - POST REQUEST contains + - issu == False + - epld == True + - package_install == True (this causes the expected error) + + endpoint: install-options + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00010a" + return responses_image_install_options(key) + + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + + module.policy_name = "KRM5" + module.serial_number = "FDO21120U5D" + module.epld = True + module.issu = True + module.package_install = True + match = "Selected policy KR5M does not have package to continue." + with pytest.raises(AnsibleFailJson, match=match): + module.refresh() + + +# test_image_mgmt_install_options_00020 -def test_image_mgmt_install_options_00007(module) -> None: +def test_image_mgmt_install_options_00020(module) -> None: """ Payload contains defaults if not specified by the user. Defaults for issu, epld, and package_install are applied. @@ -172,11 +353,10 @@ def test_image_mgmt_install_options_00007(module) -> None: assert module.payload.get("packageInstall") == False -# test_image_mgmt_install_options_00008 -# test_build_payload_user_changed_defaults (former name) +# test_image_mgmt_install_options_00021 -def test_image_mgmt_install_options_00008(module) -> None: +def test_image_mgmt_install_options_00021(module) -> None: """ Payload contains user-specified values if the user sets them. Defaults for issu, epld, and package_install are overridden by user values. @@ -194,11 +374,10 @@ def test_image_mgmt_install_options_00008(module) -> None: assert module.payload.get("packageInstall") == True -# test_image_mgmt_install_options_00009 -# test_invalid_value_issu (former name) +# test_image_mgmt_install_options_00022 -def test_image_mgmt_install_options_00009(module) -> None: +def test_image_mgmt_install_options_00022(module) -> None: """ fail_json() is called if issu is not a boolean. """ @@ -208,11 +387,10 @@ def test_image_mgmt_install_options_00009(module) -> None: module.issu = "FOO" -# test_image_mgmt_install_options_00010 -# test_invalid_value_epld (former name) +# test_image_mgmt_install_options_00023 -def test_image_mgmt_install_options_00010(module) -> None: +def test_image_mgmt_install_options_00023(module) -> None: """ fail_json() is called if epld is not a boolean. """ @@ -222,11 +400,10 @@ def test_image_mgmt_install_options_00010(module) -> None: module.epld = "FOO" -# test_image_mgmt_install_options_00011 -# test_invalid_value_package_install (former name) +# test_image_mgmt_install_options_00024 -def test_invalid_value_package_install(module) -> None: +def test_image_mgmt_install_options_00024(module) -> None: """ fail_json() is called if package_install is not a boolean. """ From 13fd3b2273fee6bf58551819a9162175719a8f07 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 14:10:00 -1000 Subject: [PATCH 054/300] run thru black/isort --- .../image_mgmt/install_options.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index fcb4197c4..04204209d 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -1,13 +1,15 @@ import inspect import json from time import sleep -from typing import Dict, Any +from typing import Any, Dict + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints class ImageInstallOptions(ImageUpgradeCommon): """ @@ -156,9 +158,11 @@ def refresh(self) -> None: self.verb = self.endpoints.install_options.get("verb") self._build_payload() + self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) + self.properties["response_data"] = self.response.get("DATA", {}) self.properties["result"] = self._handle_response(self.response, self.verb) @@ -166,7 +170,9 @@ def refresh(self) -> None: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retrieving install-options from " msg += f"the controller. Controller response: {self.response}. " - if "does not have package to continue" in self.response_data.get("error", ""): + if "does not have package to continue" in self.response_data.get( + "error", "" + ): msg += f"Possible cause: Image policy {self.policy_name} does not have " msg += "a package defined, and package_install is set to " msg += f"True in the playbook for device {self.serial_number}." @@ -177,8 +183,9 @@ def refresh(self) -> None: if self.response_data.get("compatibilityStatusList") is None: self.compatibility_status = {} else: - self.compatibility_status = self.response_data.get("compatibilityStatusList", [{}])[0] - + self.compatibility_status = self.response_data.get( + "compatibilityStatusList", [{}] + )[0] def _build_payload(self) -> None: """ @@ -205,11 +212,7 @@ def _build_payload(self) -> None: self.payload["packageInstall"] = self.package_install def _get(self, item): - return self.make_boolean( - self.make_none( - self.response_data.get(item) - ) - ) + return self.make_boolean(self.make_none(self.response_data.get(item))) # Mandatory properties @property From 5fc19149c883a9782245a6c7c4247eedced55055 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 15:29:09 -1000 Subject: [PATCH 055/300] Convert user input to boolean if it's boolean-like --- plugins/module_utils/image_mgmt/install_options.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 04204209d..77b4313a7 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -158,6 +158,7 @@ def refresh(self) -> None: self.verb = self.endpoints.install_options.get("verb") self._build_payload() + self.method_name = inspect.stack()[0][3] self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) @@ -201,6 +202,8 @@ def _build_payload(self) -> None: "packageInstall": false } """ + self.method_name = inspect.stack()[0][3] + self.payload: Dict[str, Any] = {} self.payload["devices"] = [] devices = {} @@ -212,6 +215,7 @@ def _build_payload(self) -> None: self.payload["packageInstall"] = self.package_install def _get(self, item): + self.method_name = inspect.stack()[0][3] return self.make_boolean(self.make_none(self.response_data.get(item))) # Mandatory properties @@ -251,9 +255,10 @@ def issu(self): @issu.setter def issu(self, value): + value = self.make_boolean(value) if not isinstance(value, bool): msg = f"{self.class_name}.issu.setter: " - msg += "issu must be a boolean value" + msg += f"issu must be a boolean value. Got {value}." self.module.fail_json(msg) self.properties["issu"] = value @@ -271,9 +276,10 @@ def epld(self): @epld.setter def epld(self, value): + value = self.make_boolean(value) if not isinstance(value, bool): msg = f"{self.class_name}.epld.setter: " - msg += "epld must be a boolean value" + msg += f"epld must be a boolean value. Got {value}." self.module.fail_json(msg) self.properties["epld"] = value @@ -290,9 +296,11 @@ def package_install(self): @package_install.setter def package_install(self, value): + value = self.make_boolean(value) if not isinstance(value, bool): msg = f"{self.class_name}.package_install.setter: " - msg += "package_install must be a boolean value" + msg += "package_install must be a boolean value. " + msg += f"Got {value}." self.module.fail_json(msg) self.properties["package_install"] = value From 6458c21cea9e136e83f16a994a9df62d428b832b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 15:31:27 -1000 Subject: [PATCH 056/300] Add testcase, standardize testcase names, more... Run thru black/isort --- .../test_image_upgrade_ImageUpgrade.py | 92 +++++++++++++------ 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 69a65de20..03c80e66a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -24,6 +24,7 @@ def does_not_raise(): dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" + def responses_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -43,16 +44,19 @@ def module(): return ImageUpgrade(MockAnsibleModule) -def test_init(module) -> None: +def test_image_mgmt_upgrade_00001(module) -> None: + """ + ImageUpgrade.__init__ initializes class attributes to expected values + """ module.__init__(MockAnsibleModule) assert isinstance(module, ImageUpgrade) assert module.class_name == "ImageUpgrade" assert module.max_module_number == 9 -def test_init_defaults(module) -> None: +def test_image_mgmt_upgrade_00002(module) -> None: """ - Defaults are initialized to expected values + ImageUpgrade._init_defaults initializes attributes to expected values """ module._init_defaults() assert isinstance(module.defaults, dict) @@ -71,9 +75,9 @@ def test_init_defaults(module) -> None: assert module.defaults["options"]["package"]["uninstall"] == False -def test_init_properties(module) -> None: +def test_image_mgmt_upgrade_00003(module) -> None: """ - Properties are initialized to expected values + ImageUpgrade._init_properties initializes properties to expected values """ module._init_properties() assert isinstance(module.properties, dict) @@ -114,7 +118,8 @@ def test_init_properties(module) -> None: "force_non_disruptive", } -def test_validate_devices_success(monkeypatch, module) -> None: + +def test_image_mgmt_upgrade_00004(monkeypatch, module) -> None: """ Function description: @@ -129,19 +134,12 @@ def test_validate_devices_success(monkeypatch, module) -> None: """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageUpgrade_test_validate_devices_success" + key = "test_image_mgmt_upgrade_00004a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - devices = [ - { - "ip_address": "172.22.150.102" - }, - { - "ip_address": "172.22.150.108" - } - ] + devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] module.devices = devices module.validate_devices() @@ -150,34 +148,29 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.102" in module.ip_addresses assert "172.22.150.108" in module.ip_addresses -def test_validate_devices_failed(monkeypatch, module) -> None: + +def test_image_mgmt_upgrade_00005(monkeypatch, module) -> None: """ Function description: ImageUpgrade.validate_devices updates the set ImageUpgrade.ip_addresses - with the ip addresses of the devices that have issu_detail.upgrade is + with the ip addresses of the devices for which issu_detail.upgrade is not "Failed" Expected results: - 1. instance.ip_addresses will contain {"172.22.150.102"} - 2. fail_json will be called + 1. instance.ip_addresses will contain {"172.22.150.102"} since its + upgrade status is Success + 2. fail_json will be called due to 172.22.150.108 upgrade status is Failed """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImageUpgrade_test_validate_devices_failed" + key = "test_image_mgmt_upgrade_00005a" return responses_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - devices = [ - { - "ip_address": "172.22.150.102" - }, - { - "ip_address": "172.22.150.108" - } - ] + devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] match = "ImageUpgrade.validate_devices: Image upgrade is failing for the " match += "following switch: cvd-2313-leaf, 172.22.150.108, FDO2112189M. " @@ -189,3 +182,46 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert len(module.ip_addresses) == 1 assert "172.22.150.102" in module.ip_addresses assert "172.22.150.108" not in module.ip_addresses + + +def test_image_mgmt_upgrade_00006(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Expected results: + + 1. ImageUpgrade.commit calls fail_json if devices is None + """ + match = "ImageUpgrade.commit: call instance.devices before calling commit." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00007(monkeypatch, module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + values missing that have defaults defined (see ImageUpgrade._init_defaults) + + Expected results: + + 1. merged_config will contain the expected default values + """ + config = {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False From 3615266195721ed1714d3cb8d64e8d1d3f36bda4 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 15:31:57 -1000 Subject: [PATCH 057/300] standardize test names --- ...mage_upgrade_responses_SwitchIssuDetails.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index c8e9900c3..bff82fe23 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -2890,7 +2890,11 @@ "message": "" } }, - "ImageUpgrade_test_validate_devices_success": { + "test_image_mgmt_upgrade_00004a": { + "TEST_NOTES": [ + "FDO21120U5D imageStaged == Success", + "FDO2112189M imageStage == Success" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2944,7 +2948,7 @@ "policy": "KR5M", "status": "In-Sync", "reason": "Upgrade", - "imageStaged": "Failed", + "imageStaged": "Success", "validated": "Success", "upgrade": "Success", "upgGroups": "null", @@ -2980,7 +2984,11 @@ "message": "" } }, - "ImageUpgrade_test_validate_devices_failed": { + "test_image_mgmt_upgrade_00005a": { + "TEST_NOTES": [ + "FDO21120U5D imageStaged == Success", + "FDO2112189M imageStage == Failed" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -3034,7 +3042,7 @@ "policy": "KR5M", "status": "In-Sync", "reason": "Upgrade", - "imageStaged": "Failed", + "imageStaged": "Success", "validated": "Success", "upgrade": "Failed", "upgGroups": "null", From deda29d63c9c31051ea785cd844bfc09078c6941 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 6 Nov 2023 15:33:58 -1000 Subject: [PATCH 058/300] Fix docstring --- plugins/module_utils/image_mgmt/image_policies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 000708241..f2caa09ee 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -23,7 +23,6 @@ class ImagePolicies(ImageUpgradeCommon): epd_image_name = instance.epld_image_name etc... - Policies are retrieved on instantiation of this class. Policies can be refreshed by calling instance.refresh(). Endpoint: From 3b968eaa398e1fd3158d4e19fc7fd7ed88dab5b9 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 07:00:59 -1000 Subject: [PATCH 059/300] Rename "module" to something more descriptive --- .../test_image_upgrade_ControllerVersion.py | 109 +++++++++--------- 1 file changed, 52 insertions(+), 57 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py index 3eb9c1532..1fb362dc6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py @@ -34,24 +34,19 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture -def module(): +def controller_version(): return ControllerVersion(MockAnsibleModule) -@pytest.fixture -def mock_controller_version() -> ControllerVersion: - return ControllerVersion(MockAnsibleModule) - - -def test_common_version_00001(module) -> None: +def test_common_version_00001(controller_version) -> None: """ Properties are initialized to expected values """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None + controller_version._init_properties() + assert isinstance(controller_version.properties, dict) + assert controller_version.properties.get("response_data") == None + assert controller_version.properties.get("response") == None + assert controller_version.properties.get("result") == None @pytest.mark.parametrize( @@ -62,7 +57,7 @@ def test_common_version_00001(module) -> None: ("test_common_version_00002c", None), ], ) -def test_common_version_00002(monkeypatch, module, key, expected) -> None: +def test_common_version_00002(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -87,8 +82,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.dev == expected + controller_version.refresh() + assert controller_version.dev == expected @pytest.mark.parametrize( @@ -98,7 +93,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00003b", None), ], ) -def test_common_version_00003(monkeypatch, module, key, expected) -> None: +def test_common_version_00003(monkeypatch, controller_version, key, expected) -> None: """ Description: @@ -122,8 +117,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.install == expected + controller_version.refresh() + assert controller_version.install == expected @pytest.mark.parametrize( @@ -134,7 +129,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00004c", None), ], ) -def test_common_version_00004(monkeypatch, module, key, expected) -> None: +def test_common_version_00004(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -159,8 +154,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.is_ha_enabled == expected + controller_version.refresh() + assert controller_version.is_ha_enabled == expected @pytest.mark.parametrize( @@ -171,7 +166,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00005c", None), ], ) -def test_common_version_00005(monkeypatch, module, key, expected) -> None: +def test_common_version_00005(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -197,8 +192,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.is_media_controller == expected + controller_version.refresh() + assert controller_version.is_media_controller == expected @pytest.mark.parametrize( @@ -209,7 +204,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00006c", None), ], ) -def test_common_version_00006(monkeypatch, module, key, expected) -> None: +def test_common_version_00006(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -235,11 +230,11 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.is_upgrade_inprogress == expected + controller_version.refresh() + assert controller_version.is_upgrade_inprogress == expected -def test_common_version_00007(monkeypatch, module) -> None: +def test_common_version_00007(monkeypatch, controller_version) -> None: """ Function description: @@ -263,11 +258,11 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert isinstance(module.response_data, dict) + controller_version.refresh() + assert isinstance(controller_version.response_data, dict) -def test_common_version_00008(monkeypatch, module) -> None: +def test_common_version_00008(monkeypatch, controller_version) -> None: """ Function description: @@ -289,10 +284,10 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) with pytest.raises(AnsibleFailJson): - module.refresh() + controller_version.refresh() -def test_common_version_00009(monkeypatch, module) -> None: +def test_common_version_00009(monkeypatch, controller_version) -> None: """ Function description: @@ -315,11 +310,11 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.result == {"found": True, "success": True} + controller_version.refresh() + assert controller_version.result == {"found": True, "success": True} -def test_common_version_00010(monkeypatch, module) -> None: +def test_common_version_00010(monkeypatch, controller_version) -> None: """ Function description: @@ -343,10 +338,10 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) with pytest.raises(AnsibleFailJson): - module.refresh() + controller_version.refresh() -def test_common_version_00011(monkeypatch, module) -> None: +def test_common_version_00011(monkeypatch, controller_version) -> None: """ Function description: @@ -370,14 +365,14 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) with pytest.raises(AnsibleFailJson): - module.refresh() + controller_version.refresh() @pytest.mark.parametrize( "key, expected", [("test_common_version_00012a", "LAN"), ("test_common_version_00012b", None)], ) -def test_common_version_00012(monkeypatch, module, key, expected) -> None: +def test_common_version_00012(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -401,8 +396,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.mode == expected + controller_version.refresh() + assert controller_version.mode == expected @pytest.mark.parametrize( @@ -412,7 +407,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00013b", None), ], ) -def test_common_version_00013(monkeypatch, module, key, expected) -> None: +def test_common_version_00013(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -436,8 +431,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.uuid == expected + controller_version.refresh() + assert controller_version.uuid == expected @pytest.mark.parametrize( @@ -447,7 +442,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00014b", None), ], ) -def test_common_version_00014(monkeypatch, module, key, expected) -> None: +def test_common_version_00014(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -471,8 +466,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.version == expected + controller_version.refresh() + assert controller_version.version == expected @pytest.mark.parametrize( @@ -482,7 +477,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00015b", None), ], ) -def test_common_version_00015(monkeypatch, module, key, expected) -> None: +def test_common_version_00015(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -510,8 +505,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.version_major == expected + controller_version.refresh() + assert controller_version.version_major == expected @pytest.mark.parametrize( @@ -521,7 +516,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00016b", None), ], ) -def test_common_version_00016(monkeypatch, module, key, expected) -> None: +def test_common_version_00016(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -549,8 +544,8 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.version_minor == expected + controller_version.refresh() + assert controller_version.version_minor == expected @pytest.mark.parametrize( @@ -560,7 +555,7 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: ("test_common_version_00017b", None), ], ) -def test_common_version_00017(monkeypatch, module, key, expected) -> None: +def test_common_version_00017(monkeypatch, controller_version, key, expected) -> None: """ Function description: @@ -588,5 +583,5 @@ def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - module.refresh() - assert module.version_patch == expected + controller_version.refresh() + assert controller_version.version_patch == expected From af21244ad1adba441bef77eaddbcb06df7fdfcaf Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 10:27:14 -1000 Subject: [PATCH 060/300] Consider the upgrade status when determining idempotence If have upgrade == "Failed" we should try to upgrade again, else the user would have to use the GUI (or policy management playbook) to reset the upgrade status. --- plugins/modules/dcnm_image_upgrade.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 76360e680..943fc5e2b 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -595,6 +595,9 @@ def _build_idempotent_want(self, want) -> None: self.have.status == "In-Sync" and self.have.reason == "Upgrade" and self.have.policy == want["policy"] + # If upgrade is other than Success, we need to try to upgrade + # again. So only change upgrade.nxos if upgrade is Success. + and self.have.upgrade == "Success" ): self.idempotent_want["upgrade"]["nxos"] = False From bcc6b067b35449e8fe34792c5ef465187a715acd Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 10:33:10 -1000 Subject: [PATCH 061/300] Determine valid EPLD module differently, more... If options.epld.golden is True, then upgrade.nxos must be false or NX-OS throws an error which doesn't appear in any CLI output after the event. So, catch this combination, and fail_json with an explanation. --- .../module_utils/image_mgmt/image_upgrade.py | 31 ++++++++++++------- .../test_image_upgrade_ImageUpgrade.py | 13 -------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 9458f1195..a6c543a78 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -115,8 +115,6 @@ def __init__(self, module): self.method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() - # Maximum number of modules/linecards in a switch - self.max_module_number = 9 self._init_defaults() self._init_properties() @@ -177,11 +175,6 @@ def _init_properties(self) -> None: self.properties["reboot"] = False self.properties["write_erase"] = False - self.valid_epld_module: Set[Union[str, int]] = set() - self.valid_epld_module.add("ALL") - for module in range(1, self.max_module_number + 1): - self.valid_epld_module.add(str(module)) - self.valid_nxos_mode: Set[str] = set() self.valid_nxos_mode.add("disruptive") self.valid_nxos_mode.add("non_disruptive") @@ -373,13 +366,24 @@ def build_payload(self, device) -> None: # EPLD epld_module = device.get("options").get("epld").get("module") epld_golden = device.get("options").get("epld").get("golden") - - if epld_module not in self.valid_epld_module: + if epld_golden is True and device.get("upgrade").get("nxos") is True: msg = f"{self.class_name}.{self.method_name}: " - msg += "options.epld.module must be one of " - msg += f"{self.valid_epld_module}. Got {epld_module}." + msg += "Invalid configuration for " + msg += f"{self.issu_detail.ip_address}. " + msg += "If options.epld.golden is True " + msg += "all other upgrade options, e.g. upgrade.nxos, " + msg += "must be False." self.module.fail_json(msg) + if epld_module != "ALL": + try: + epld_module = int(epld_module) + except: + msg = f"{self.class_name}.{self.method_name}: " + msg += "options.epld.module must either be 'ALL' " + msg += f"or an integer. Got {epld_module}." + self.module.fail_json(msg) + if not isinstance(epld_golden, bool): msg = f"{self.class_name}.{self.method_name}: " msg += "options.epld.golden must be a boolean. " @@ -391,6 +395,7 @@ def build_payload(self, device) -> None: self.payload["epldOptions"]["moduleNumber"] = epld_module self.payload["epldOptions"]["golden"] = epld_golden + # Reboot reboot = device.get("reboot") @@ -551,7 +556,9 @@ def _wait_for_image_upgrade_to_complete(self): msg += f"Seconds remaining {timeout}: upgrade image " msg += f"{upgrade_status} for " msg += f"{device_name}, {serial_number}, {ip_address}, " - msg += f"upgrade_percent {upgrade_percent}." + msg += f"upgrade_percent {upgrade_percent}. " + msg += "Check the controller to determine the cause. " + msg += "Operations > Image Management > Devices > View Details." self.module.fail_json(msg) if upgrade_status == "Success": diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 03c80e66a..93894df2d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -51,7 +51,6 @@ def test_image_mgmt_upgrade_00001(module) -> None: module.__init__(MockAnsibleModule) assert isinstance(module, ImageUpgrade) assert module.class_name == "ImageUpgrade" - assert module.max_module_number == 9 def test_image_mgmt_upgrade_00002(module) -> None: @@ -100,18 +99,6 @@ def test_image_mgmt_upgrade_00003(module) -> None: assert module.properties.get("package_uninstall") == False assert module.properties.get("reboot") == False assert module.properties.get("write_erase") == False - assert module.valid_epld_module == { - "ALL", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - } assert module.valid_nxos_mode == { "disruptive", "non_disruptive", From 78f22bb4a0c3c453ffd851a1988622205cb99a0a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 10:45:26 -1000 Subject: [PATCH 062/300] Remove log messages --- plugins/module_utils/common/controller_version.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 077745251..2fb78b257 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -59,12 +59,6 @@ def refresh(self): self.properties["response"] = dcnm_send(self.module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) - msg = f"REMOVE: {self.class_name}.refresh() response: {self.response}" - self.log_msg(msg) - - msg = f"REMOVE: {self.class_name}.refresh() result: {self.result}" - self.log_msg(msg) - if self.result["success"] == False or self.result["found"] == False: msg = f"{self.class_name}.refresh() failed: {self.result}" self.module.fail_json(msg) @@ -76,9 +70,6 @@ def refresh(self): msg += f"{self.response}" self.module.fail_json(msg) - msg = f"REMOVE: {self.class_name}.refresh() response_data: {self.response_data}" - self.log_msg(msg) - def _get(self, item): return self.make_boolean(self.make_none(self.response_data.get(item))) From 12863eaa9b02c741a9c9dc83257f8999eb537c8d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 10:47:12 -1000 Subject: [PATCH 063/300] More robust handling of missing keys --- .../module_utils/image_mgmt/image_policies.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index f2caa09ee..2b0887d01 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -96,15 +96,22 @@ def _get(self, item): msg += f"accessing property {item}." self.module.fail_json(msg) - if self.properties['response_data'].get(self.policy_name) is None: + if self.policy_name not in self.properties["response_data"]: msg = f"{self.class_name}.{self.method_name}: " - msg += f"policy_name {self.policy_name} is not defined on " - msg += "the controller." + msg += f"policy_name {self.policy_name} is not defined " + msg += "on the controller." self.module.fail_json(msg) - return_item = self.make_boolean(self.properties["response_data"][self.policy_name].get(item)) - return_item = self.make_none(return_item) - return return_item + if item not in self.properties["response_data"][self.policy_name]: + msg = f"{self.class_name}.{self.method_name}: " + msg += f"{self.policy_name} does not have a key named {item}." + self.module.fail_json(msg) + + return self.make_boolean( + self.make_none( + self.properties["response_data"][self.policy_name][item] + ) + ) @property def description(self): From f53ea5b59feec6d6bd22137bab605df0297e3664 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 10:50:54 -1000 Subject: [PATCH 064/300] Remove log messages --- plugins/module_utils/image_mgmt/image_stage.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index dff4adc0d..c859b0983 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -127,13 +127,6 @@ def prune_serial_numbers(self): if self.issu_detail.image_staged == "Success": self.serial_numbers.remove(serial_number) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += "image already staged for " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}." - self.log_msg(msg) - def validate_serial_numbers(self): """ Fail if the image_staged state for any serial_number @@ -191,14 +184,6 @@ def commit(self): ) self.properties["result"] = self._handle_response(self.response, self.verb) - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"response: {self.response}" - self.log_msg(msg) - - msg = f"REMOVE: {self.class_name}.{self.method_name}: " - msg += f"result: {self.result}" - self.log_msg(msg) - if not self.result["success"]: msg = f"{self.class_name}.{self.method_name}: " msg = f"failed: {self.result}. " From 135ce7254f210c7b8aace5efb09dafb018d9c512 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 11:03:35 -1000 Subject: [PATCH 065/300] method_name should be local to each method If method_name is global to a class, the wrong method name is logged if a method calls another method. --- .../module_utils/image_mgmt/image_stage.py | 32 +++--- .../module_utils/image_mgmt/image_upgrade.py | 106 +++++++++--------- plugins/modules/dcnm_image_upgrade.py | 82 +++++++------- 3 files changed, 110 insertions(+), 110 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index c859b0983..fe0043cac 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -81,7 +81,7 @@ class ImageStage(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() self._init_properties() self.serial_numbers_done = set() @@ -92,7 +92,7 @@ def __init__(self, module): self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.properties = {} self.properties["serial_numbers"] = None self.properties["response_data"] = None @@ -109,7 +109,7 @@ def _populate_controller_version(self): 1. This cannot go into ImageUpgradeCommon() due to circular imports resulting in RecursionError """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] instance = ControllerVersion(self.module) instance.refresh() self.controller_version = instance.version @@ -119,7 +119,7 @@ def prune_serial_numbers(self): If the image is already staged on a switch, remove that switch's serial number from the list of serial numbers to stage. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: self.issu_detail.serial_number = serial_number @@ -132,13 +132,13 @@ def validate_serial_numbers(self): Fail if the image_staged state for any serial_number is Failed. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] for serial_number in self.serial_numbers: self.issu_detail.serial_number = serial_number self.issu_detail.refresh() if self.issu_detail.image_staged == "Failed": - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg = "Image staging is failing for the following switch: " msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " @@ -152,10 +152,10 @@ def commit(self): Commit the image staging request to the controller and wait for the images to be staged. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if self.serial_numbers is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "call instance.serial_numbers " msg += "before calling commit." self.module.fail_json(msg) @@ -185,7 +185,7 @@ def commit(self): self.properties["result"] = self._handle_response(self.response, self.verb) if not self.result["success"]: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg = f"failed: {self.result}. " msg += f"Controller response: {self.response}" self.module.fail_json(msg) @@ -199,7 +199,7 @@ def _wait_for_current_actions_to_complete(self): progress. Wait for all actions to complete before staging image. Actions include image staging, image upgrade, and image validation. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.serial_numbers_done = set() serial_numbers_todo = set(copy.copy(self.serial_numbers)) @@ -220,7 +220,7 @@ def _wait_for_current_actions_to_complete(self): self.serial_numbers_done.add(serial_number) if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Timed out waiting for actions to complete. " msg += f"serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " @@ -232,7 +232,7 @@ def _wait_for_image_stage_to_complete(self): """ # Wait for image stage to complete """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.serial_numbers_done = set() timeout = self.check_timeout @@ -254,7 +254,7 @@ def _wait_for_image_stage_to_complete(self): staged_status = self.issu_detail.image_staged if staged_status == "Failed": - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Seconds remaining {timeout}: stage image failed " msg += f"for {device_name}, {serial_number}, {ip_address}. " msg += f"image staged percent: {staged_percent}" @@ -264,7 +264,7 @@ def _wait_for_image_stage_to_complete(self): self.serial_numbers_done.add(serial_number) if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Timed out waiting for image stage to complete. " msg += f"serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " @@ -283,9 +283,9 @@ def serial_numbers(self): @serial_numbers.setter def serial_numbers(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, list): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_numbers must be a " msg += "python list of switch serial numbers." self.module.fail_json(msg) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index a6c543a78..ff8e5a484 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -112,7 +112,7 @@ class ImageUpgrade(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() @@ -122,7 +122,7 @@ def __init__(self, module): self.install_options = ImageInstallOptions(self.module) def _init_defaults(self) -> None: - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.defaults: Dict[str, Any] = {} self.defaults["reboot"] = False @@ -146,7 +146,7 @@ def _init_defaults(self) -> None: self.defaults["options"]["package"]["uninstall"] = False def _init_properties(self) -> None: - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # self.ip_addresses is used in: # self._wait_for_current_actions_to_complete() @@ -216,14 +216,14 @@ def validate_devices(self) -> None: """ Fail if the upgrade state for any device is Failed. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] for device in self.devices: self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() if self.issu_detail.upgrade == "Failed": - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Image upgrade is failing for the following switch: " msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " @@ -236,7 +236,7 @@ def validate_devices(self) -> None: self.ip_addresses.add(str(self.issu_detail.ip_address)) def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if config.get("stage") is None: config["stage"] = self.defaults["stage"] @@ -296,7 +296,7 @@ def build_payload(self, device) -> None: """ Build the request payload to upgrade the switches. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] device = self._merge_defaults_to_switch_config(device) @@ -312,7 +312,7 @@ def build_payload(self, device) -> None: # ImageInstallOptions may fail_json in the refresh() above, which is # fine since it will contain more detailed information. if self.install_options.result["success"] is False: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.install_options.result}. " msg += f"Controller response: {self.install_options.response}" self.module.fail_json(msg) @@ -339,7 +339,7 @@ def build_payload(self, device) -> None: nxos_mode = device.get("options").get("nxos").get("mode") if nxos_mode not in self.valid_nxos_mode: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "options.nxos.mode must be one of " msg += f"{self.valid_nxos_mode}. Got {nxos_mode}." self.module.fail_json(msg) @@ -355,7 +355,7 @@ def build_payload(self, device) -> None: bios_force = device.get("options").get("nxos").get("bios_force") if not isinstance(bios_force, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "options.nxos.bios_force must be a boolean. " msg += f"Got {bios_force}." self.module.fail_json(msg) @@ -367,7 +367,7 @@ def build_payload(self, device) -> None: epld_module = device.get("options").get("epld").get("module") epld_golden = device.get("options").get("epld").get("golden") if epld_golden is True and device.get("upgrade").get("nxos") is True: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Invalid configuration for " msg += f"{self.issu_detail.ip_address}. " msg += "If options.epld.golden is True " @@ -379,13 +379,13 @@ def build_payload(self, device) -> None: try: epld_module = int(epld_module) except: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "options.epld.module must either be 'ALL' " msg += f"or an integer. Got {epld_module}." self.module.fail_json(msg) if not isinstance(epld_golden, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "options.epld.golden must be a boolean. " msg += f"Got {epld_golden}." self.module.fail_json(msg) @@ -400,7 +400,7 @@ def build_payload(self, device) -> None: reboot = device.get("reboot") if not isinstance(reboot, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "reboot must be a boolean. " msg += f"Got {reboot}." self.module.fail_json(msg) @@ -411,13 +411,13 @@ def build_payload(self, device) -> None: write_erase = device.get("options").get("reboot").get("write_erase") if not isinstance(config_reload, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.config_reload must be a boolean. " msg += f"Got {config_reload}." self.module.fail_json(msg) if not isinstance(write_erase, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.write_erase must be a boolean. " msg += f"Got {write_erase}." self.module.fail_json(msg) @@ -431,13 +431,13 @@ def build_payload(self, device) -> None: package_uninstall = device.get("options").get("package").get("uninstall") if not isinstance(package_install, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "options.package.install must be a boolean. " msg += f"Got {package_install}." self.module.fail_json(msg) if not isinstance(package_uninstall, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "options.package.uninstall must be a boolean. " msg += f"Got {package_uninstall}." self.module.fail_json(msg) @@ -450,10 +450,10 @@ def commit(self) -> None: Commit the image upgrade request to the controller and wait for the images to be upgraded. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if self.devices is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "call instance.devices before calling commit." self.module.fail_json(msg) @@ -472,7 +472,7 @@ def commit(self) -> None: self.properties["result"] = self._handle_response(self.response, self.verb) if not self.result["success"]: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.result}. " msg += f"Controller response: {self.response}" self.module.fail_json(msg) @@ -486,7 +486,7 @@ def _wait_for_current_actions_to_complete(self): in progress. Wait for all actions to complete before upgrading image. Actions include image staging, image upgrade, and image validation. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.ipv4_todo = copy.copy(self.ip_addresses) self.ipv4_done = set() @@ -507,7 +507,7 @@ def _wait_for_current_actions_to_complete(self): self.ipv4_done.add(ipv4) continue - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Seconds remaining {timeout}. Waiting " msg += f" on ipv4 {ipv4} actions to complete. " msg += f"staged_percent {self.issu_detail.image_staged_percent}, " @@ -516,7 +516,7 @@ def _wait_for_current_actions_to_complete(self): self.log_msg(msg) if self.ipv4_done != self.ipv4_todo: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Timed out while waiting for actions in progress " msg += "to complete for the following device(s): " msg += f"{self.ipv4_todo}. " @@ -529,7 +529,7 @@ def _wait_for_image_upgrade_to_complete(self): """ Wait for image upgrade to complete """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.ipv4_todo = set(copy.copy(self.ip_addresses)) self.ipv4_done = set() @@ -552,7 +552,7 @@ def _wait_for_image_upgrade_to_complete(self): serial_number = self.issu_detail.serial_number if upgrade_status == "Failed": - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Seconds remaining {timeout}: upgrade image " msg += f"{upgrade_status} for " msg += f"{device_name}, {serial_number}, {ip_address}, " @@ -570,7 +570,7 @@ def _wait_for_image_upgrade_to_complete(self): status = "in progress" if self.ipv4_done != self.ipv4_todo: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "The following device(s) did not complete upgrade: " msg += f"{self.ipv4_todo.difference(self.ipv4_done)}. " msg += "Try increasing issu timeout in the playbook, or check " @@ -590,9 +590,9 @@ def bios_force(self): @bios_force.setter def bios_force(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.bios_force must be a boolean." self.module.fail_json(msg) self.properties["bios_force"] = value @@ -608,9 +608,9 @@ def config_reload(self): @config_reload.setter def config_reload(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.config_reload must be a boolean." self.module.fail_json(msg) self.properties["config_reload"] = value @@ -633,9 +633,9 @@ def devices(self) -> List[Dict]: @devices.setter def devices(self, value: List[Dict]): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, list): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.devices must be a python list of dict." self.module.fail_json(msg) self.properties["devices"] = value @@ -651,9 +651,9 @@ def disruptive(self): @disruptive.setter def disruptive(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.disruptive must be a boolean." self.module.fail_json(msg) self.properties["disruptive"] = value @@ -669,9 +669,9 @@ def epld_golden(self): @epld_golden.setter def epld_golden(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.epld_golden must be a boolean." self.module.fail_json(msg) self.properties["epld_golden"] = value @@ -687,9 +687,9 @@ def epld_upgrade(self): @epld_upgrade.setter def epld_upgrade(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.epld_upgrade must be a boolean." self.module.fail_json(msg) self.properties["epld_upgrade"] = value @@ -707,13 +707,13 @@ def epld_module(self): @epld_module.setter def epld_module(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] try: value = value.upper() except AttributeError: pass if not isinstance(value, int) and value != "ALL": - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"instance.epld_module must be an integer or 'ALL'" self.module.fail_json(msg) self.properties["epld_module"] = value @@ -729,9 +729,9 @@ def force_non_disruptive(self): @force_non_disruptive.setter def force_non_disruptive(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.force_non_disruptivemust be a boolean." self.module.fail_json(msg) self.properties["force_non_disruptive"] = value @@ -747,9 +747,9 @@ def non_disruptive(self): @non_disruptive.setter def non_disruptive(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}.setter: " + msg = f"{self.class_name}.{method_name}.setter: " msg += "instance.non_disruptive must be a boolean." self.module.fail_json(msg) self.properties["non_disruptive"] = value @@ -765,9 +765,9 @@ def package_install(self): @package_install.setter def package_install(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.package_install must be a boolean." self.module.fail_json(msg) self.properties["package_install"] = value @@ -783,9 +783,9 @@ def package_uninstall(self): @package_uninstall.setter def package_uninstall(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.package_uninstall must be a boolean." self.module.fail_json(msg) self.properties["package_uninstall"] = value @@ -801,9 +801,9 @@ def reboot(self): @reboot.setter def reboot(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.reboot must be a boolean." self.module.fail_json(msg) self.properties["reboot"] = value @@ -819,9 +819,9 @@ def write_erase(self): @write_erase.setter def write_erase(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.write_erase must be a boolean." self.module.fail_json(msg) self.properties["write_erase"] = value diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 943fc5e2b..80197a3c3 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -446,7 +446,7 @@ class ImageUpgradeTask(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.params = self.module.params self.endpoints = ApiEndpoints() @@ -457,7 +457,7 @@ def __init__(self, module): self.config = module.params.get("config", {}) if not isinstance(self.config, dict): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "expected dict type for self.config. " msg += f"got {type(self.config).__name__}" self.module.fail_json(msg) @@ -474,20 +474,20 @@ def __init__(self, module): self.mandatory_switch_keys = {"ip_address"} if not self.mandatory_global_keys.issubset(self.config): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Missing mandatory key(s) in playbook global config. " msg += f"expected {self.mandatory_global_keys}, " msg += f"got {self.config.keys()}" self.module.fail_json(msg) if self.config["switches"] is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "missing list of switches in playbook config." self.module.fail_json(msg) for switch in self.config["switches"]: if not self.mandatory_switch_keys.issubset(switch): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "missing mandatory key(s) in playbook switch config. " msg += f"expected {self.mandatory_switch_keys}, " msg += f"got {switch.keys()}" @@ -502,7 +502,7 @@ def get_have(self) -> None: Determine current switch ISSU state on NDFC """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.have = SwitchIssuDetailsByIpAddress(self.module) self.have.refresh() @@ -513,7 +513,7 @@ def get_want(self) -> None: Update self.want_create for all switches defined in the playbook """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self._merge_global_and_switch_configs(self.config) self._validate_switch_configs() @@ -559,7 +559,7 @@ def _build_idempotent_want(self, want) -> None: Caller: self.get_need_merged() """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.have.ip_address = want["ip_address"] @@ -621,7 +621,7 @@ def get_need_merged(self) -> None: our want list that are not in our have list. These items will be sent to the controller. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] need: List[Dict] = [] for want_create in self.want_create: @@ -646,7 +646,7 @@ def get_need_deleted(self) -> None: list that are not in our have list. These items will be sent to the controller. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] need = [] for want in self.want_create: @@ -665,7 +665,7 @@ def get_need_query(self) -> None: For query state, populate self.need list() with all items from our want list. These items will be sent to the controller. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] need = [] for want in self.want_create: @@ -779,12 +779,12 @@ def validate_input(self) -> None: Validate the playbook parameters """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] state = self.params["state"] if state not in ["merged", "deleted", "query"]: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "This module supports deleted, merged, and query states. " msg += f"Got state {state}" self.module.fail_json(msg) @@ -805,10 +805,10 @@ def _validate_input_for_merged_state(self) -> None: Validate that self.config contains appropriate values for merged state """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not self.config: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "config: element is mandatory for state merged" self.module.fail_json(msg) @@ -822,7 +822,7 @@ def _validate_input_for_merged_state(self) -> None: self.validated = copy.deepcopy(valid_params) if invalid_params: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Invalid parameters in playbook: " msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) @@ -837,11 +837,11 @@ def _validate_input_for_deleted_state(self) -> None: 1. This is currently identical to _validate_input_for_merged_state() 2. Adding in case there are differences in the future """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] params_spec = self._build_params_spec_for_merged_state() if not self.config: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "config: element is mandatory for state deleted" self.module.fail_json(msg) @@ -853,7 +853,7 @@ def _validate_input_for_deleted_state(self) -> None: self.validated = copy.deepcopy(valid_params) if invalid_params: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Invalid parameters in playbook: " msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) @@ -868,12 +868,12 @@ def _validate_input_for_query_state(self) -> None: 1. This is currently identical to _validate_input_for_merged_state() 2. Adding in case there are differences in the future """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] params_spec = self._build_params_spec_for_merged_state() if not self.config: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "config: element is mandatory for state query" self.module.fail_json(msg) @@ -885,7 +885,7 @@ def _validate_input_for_query_state(self) -> None: self.validated = copy.deepcopy(valid_params) if invalid_params: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Invalid parameters in playbook: " msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) @@ -905,10 +905,10 @@ def _merge_global_and_switch_configs(self, config) -> None: 5. If global_config and switch_config are both missing a mandatory parameter, fail. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not config.get("switches"): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "playbook is missing list of switches" self.module.fail_json(msg) @@ -937,11 +937,11 @@ def _validate_switch_configs(self) -> None: Callers: - self.get_want """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] for switch in self.switch_configs: if not switch.get("ip_address"): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg = "playbook is missing ip_address for at least one switch" self.module.fail_json(msg) @@ -951,7 +951,7 @@ def _validate_switch_configs(self) -> None: continue if switch.get("policy") is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "playbook is missing image policy for switch " msg += f"{switch.get('ip_address')} " msg += "and global image policy is not defined." @@ -966,7 +966,7 @@ def _build_policy_attach_payload(self) -> None: Callers: - self.handle_merged_state """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.payloads = [] self.switch_details.refresh() @@ -981,14 +981,14 @@ def _build_policy_attach_payload(self) -> None: # Fail if the image policy does not exist. # Image policy creation is handled by a different module. if self.image_policies.name is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"policy {switch.get('policy')} does not exist on " msg += "the controller" self.module.fail_json(msg) # Fail if the image policy does not support the switch platform if self.switch_details.platform not in self.image_policies.platform: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"policy {switch.get('policy')} does not support " msg += f"platform {self.switch_details.platform}. " msg += f"Policy {switch.get('policy')} " @@ -1008,7 +1008,7 @@ def _build_policy_attach_payload(self) -> None: for item in payload: if payload[item] is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Unable to determine {item} for switch " msg += f"{switch.get('ip_address')}. " msg += "Please verify that the switch is managed by " @@ -1024,7 +1024,7 @@ def _send_policy_attach_payload(self) -> None: Callers: - self.handle_merged_state """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if len(self.payloads) == 0: return @@ -1050,7 +1050,7 @@ def _stage_images(self, serial_numbers) -> None: Callers: - handle_merged_state """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] instance = ImageStage(self.module) instance.serial_numbers = serial_numbers @@ -1063,7 +1063,7 @@ def _validate_images(self, serial_numbers) -> None: Callers: - handle_merged_state """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers @@ -1106,7 +1106,7 @@ def _verify_install_options(self, devices) -> None: Callers: - self.handle_merged_state """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if len(devices) == 0: return @@ -1126,7 +1126,7 @@ def _verify_install_options(self, devices) -> None: install_options.status not in ["Success", "Skipped"] and device["upgrade"]["nxos"] is True ): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "NXOS upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " @@ -1137,7 +1137,7 @@ def _verify_install_options(self, devices) -> None: install_options.epld_modules is None and device["upgrade"]["epld"] is True ): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "EPLD upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " @@ -1151,7 +1151,7 @@ def _upgrade_images(self, devices) -> None: Callers: - handle_merged_state """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] upgrade = ImageUpgrade(self.module) upgrade.devices = devices @@ -1166,7 +1166,7 @@ def handle_merged_state(self) -> None: Caller: main() """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self._build_policy_attach_payload() self._send_policy_attach_payload() @@ -1207,7 +1207,7 @@ def handle_deleted_state(self) -> None: Caller: main() """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] detach_policy_devices: Dict[str, Any] = {} @@ -1241,7 +1241,7 @@ def handle_query_state(self) -> None: Caller: main() """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() From 81b5f65761e977acee21045ea58a03e18b4334f7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 13:20:01 -1000 Subject: [PATCH 066/300] Remove fail_json from validate_devices. --- .../module_utils/image_mgmt/image_upgrade.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index ff8e5a484..e7d290b36 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -214,7 +214,10 @@ def _init_properties(self) -> None: def validate_devices(self) -> None: """ - Fail if the upgrade state for any device is Failed. + 1. Perform any pre-upgrade validations (currently none) + 2. Populate self.ip_addresses with the ip_address of all + switches which can be upgraded. This is used in + _wait_for_current_actions_to_complete """ method_name = inspect.stack()[0][3] @@ -222,15 +225,12 @@ def validate_devices(self) -> None: self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() - if self.issu_detail.upgrade == "Failed": - msg = f"{self.class_name}.{method_name}: " - msg += "Image upgrade is failing for the following switch: " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}. " - msg += "Please check the switch " - msg += "to determine the cause and try again." - self.module.fail_json(msg) + # Any device validation from issu_detail would go here. + # We used to fail_json if upgrade == "Failed" but that + # forced users to have to reset the upgrade state on the + # controller. We now allow the upgrade to proceed if + # upgrade == "Failed". But let's leave this method here + # in case we want to add more validation in the future. # used in self._wait_for_current_actions_to_complete() self.ip_addresses.add(str(self.issu_detail.ip_address)) From 055ddd3fa17df7287f99066a351128adf80199db Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 13:24:47 -1000 Subject: [PATCH 067/300] Update test_image_mgmt_upgrade_00005 based on last commit --- .../test_image_upgrade_ImageUpgrade.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 93894df2d..5863ada9d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -141,14 +141,13 @@ def test_image_mgmt_upgrade_00005(monkeypatch, module) -> None: Function description: ImageUpgrade.validate_devices updates the set ImageUpgrade.ip_addresses - with the ip addresses of the devices for which issu_detail.upgrade is - not "Failed" + with the ip addresses of the devices for which validation succeeds. + Currently, validation succeeds for all devices. This function may be + updated in the future to handle various failure scenarios. Expected results: - 1. instance.ip_addresses will contain {"172.22.150.102"} since its - upgrade status is Success - 2. fail_json will be called due to 172.22.150.108 upgrade status is Failed + 1. instance.ip_addresses will contain {"172.22.150.102", "172.22.150.108"} """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -159,16 +158,12 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] - match = "ImageUpgrade.validate_devices: Image upgrade is failing for the " - match += "following switch: cvd-2313-leaf, 172.22.150.108, FDO2112189M. " - match += "Please check the switch to determine the cause and try again." module.devices = devices - with pytest.raises(AnsibleFailJson, match=match): - module.validate_devices() + module.validate_devices() assert isinstance(module.ip_addresses, set) - assert len(module.ip_addresses) == 1 + assert len(module.ip_addresses) == 2 assert "172.22.150.102" in module.ip_addresses - assert "172.22.150.108" not in module.ip_addresses + assert "172.22.150.108" in module.ip_addresses def test_image_mgmt_upgrade_00006(monkeypatch, module) -> None: From cec1ae0947d963cc05fb8539cdf22e1c1e055275 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 7 Nov 2023 17:22:30 -1000 Subject: [PATCH 068/300] Add testcases for ImageUpgrade.commit(), more... Add logging to image_upgrade.py to obtain controller responses for future tests. Remove these later... --- .../module_utils/image_mgmt/image_upgrade.py | 43 ++++- .../image_upgrade_payloads_ImageUpgrade.json | 60 +++++++ ...upgrade_responses_ImageInstallOptions.json | 110 +++++++++++++ .../image_upgrade_responses_ImageUpgrade.json | 31 ++++ ...e_upgrade_responses_SwitchIssuDetails.json | 102 ++++++++++++ .../test_image_upgrade_ImageUpgrade.py | 149 +++++++++++++++++- 6 files changed, 492 insertions(+), 3 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index e7d290b36..a66eaf52a 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -298,17 +298,34 @@ def build_payload(self, device) -> None: """ method_name = inspect.stack()[0][3] + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"device PRE : {json.dumps(device, indent=4, sort_keys=True)}" + self.log_msg(msg) + device = self._merge_defaults_to_switch_config(device) + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"device POST: {json.dumps(device, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"issu_detail.response: {json.dumps(self.issu_detail.response, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.install_options.serial_number = self.issu_detail.serial_number self.install_options.policy_name = device.get("policy") self.install_options.epld = device.get("upgrade").get("epld") self.install_options.nxos = device.get("upgrade").get("nxos") self.install_options.package_install = device.get("options").get("package").get("install") self.install_options.refresh() + + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"install_options.response: {json.dumps(self.install_options.response, indent=4, sort_keys=True)}" + self.log_msg(msg) + # ImageInstallOptions may fail_json in the refresh() above, which is # fine since it will contain more detailed information. if self.install_options.result["success"] is False: @@ -445,6 +462,10 @@ def build_payload(self, device) -> None: self.payload["pacakgeInstall"] = package_install self.payload["pacakgeUnInstall"] = package_uninstall + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log_msg(msg) + def commit(self) -> None: """ Commit the image upgrade request to the controller and wait @@ -452,6 +473,10 @@ def commit(self) -> None: """ method_name = inspect.stack()[0][3] + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"self.devices: {self.devices}" + self.log_msg(msg) + if self.devices is None: msg = f"{self.class_name}.{method_name}: " msg += "call instance.devices before calling commit." @@ -463,14 +488,30 @@ def commit(self) -> None: self.path: str = self.endpoints.image_upgrade.get("path") self.verb: str = self.endpoints.image_upgrade.get("verb") + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"self.verb {self.verb}, self.path: {self.path}" + self.log_msg(msg) + for device in self.devices: + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"device: {json.dumps(device, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.build_payload(device) + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) self.properties["result"] = self._handle_response(self.response, self.verb) + msg = f"REMOVE: {self.class_name}.{method_name}: " + msg += f"self.response: {self.response}" + self.log_msg(msg) + if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.result}. " @@ -507,7 +548,7 @@ def _wait_for_current_actions_to_complete(self): self.ipv4_done.add(ipv4) continue - msg = f"{self.class_name}.{method_name}: " + msg = f"REMOVE: {self.class_name}.{method_name}: " msg += f"Seconds remaining {timeout}. Waiting " msg += f" on ipv4 {ipv4} actions to complete. " msg += f"staged_percent {self.issu_detail.image_staged_percent}, " diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json new file mode 100644 index 000000000..c05debed8 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json @@ -0,0 +1,60 @@ +{ + "test_image_mgmt_upgrade_00008a": { + "devices": [ + { + "policyName": "KR5M", + "serialNumber": "FDO21120U5D" + } + ], + "epldOptions": { + "golden": false, + "moduleNumber": "ALL" + }, + "epldUpgrade": true, + "issuUpgrade": false, + "issuUpgradeOptions1": { + "disruptive": true, + "forceNonDisruptive": false, + "nonDisruptive": false + }, + "issuUpgradeOptions2": { + "biosForce": true + }, + "pacakgeInstall": false, + "pacakgeUnInstall": false, + "reboot": false, + "rebootOptions": { + "configReload": false, + "writeErase": false + } + }, + "test_image_mgmt_upgrade_00009a": { + "devices": [ + { + "policyName": "NR3F", + "serialNumber": "FDO21120U5D" + } + ], + "epldOptions": { + "golden": false, + "moduleNumber": "ALL" + }, + "epldUpgrade": true, + "issuUpgrade": true, + "issuUpgradeOptions1": { + "disruptive": true, + "forceNonDisruptive": false, + "nonDisruptive": false + }, + "issuUpgradeOptions2": { + "biosForce": false + }, + "pacakgeInstall": false, + "pacakgeUnInstall": false, + "reboot": false, + "rebootOptions": { + "configReload": false, + "writeErase": false + } + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json index 029971fc5..3084d1c50 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json @@ -209,5 +209,115 @@ "DATA": { "error": "Selected policy KR5M does not have package to continue." } + }, + "test_image_mgmt_upgrade_00008a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "KR5M", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.2.5", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "KR5M" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "KR5M" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00009a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json new file mode 100644 index 000000000..35dd81f2c --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json @@ -0,0 +1,31 @@ +{ + "test_image_mgmt_upgrade_00008a": { + "DATA": 121, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00009a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_0000XX": { + "DATA": { + "error": "Selected upgrade option is 'isGolden'. It does not allow when upgrade options selected more than one option. " + }, + "MESSAGE": "Internal Server Error", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 500, + "TEST_NOTES": [ + "Returned under the following conditions", + "upgrade.epld == True", + "upgrade.nxos == True", + "options.epld.golden == True" + ] + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index bff82fe23..597f90e6b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3077,5 +3077,107 @@ ], "message": "" } + }, + "test_image_mgmt_upgrade_00008a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-07 23:47", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "KR5M", + "reason": "Upgrade", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "In-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "Success", + "upgradePercent": 100, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00009a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 5863ada9d..d1b30a644 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -22,9 +22,30 @@ def does_not_raise(): patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." + +dcnm_send_image_upgrade = patch_image_mgmt + "image_upgrade.dcnm_send" +dcnm_send_install_options = patch_image_mgmt + "install_options.dcnm_send" dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +def payloads_image_upgrade(key: str) -> Dict[str, str]: + payload_file = f"image_upgrade_payloads_ImageUpgrade" + payload = load_fixture(payload_file).get(key) + print(f"payload_data_image_upgrade: {key} : {payload}") + return payload + +def responses_image_upgrade(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ImageUpgrade" + response = load_fixture(response_file).get(key) + print(f"response_data_image_upgrade: {key} : {response}") + return response + +def responses_install_options(key: str) -> Dict[str, str]: + response_file = f"image_upgrade_responses_ImageInstallOptions" + response = load_fixture(response_file).get(key) + print(f"response_data_install_options: {key} : {response}") + return response + def responses_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -166,7 +187,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" in module.ip_addresses -def test_image_mgmt_upgrade_00006(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00006(module) -> None: """ Function: ImageUpgrade.commit @@ -179,7 +200,7 @@ def test_image_mgmt_upgrade_00006(monkeypatch, module) -> None: module.commit() -def test_image_mgmt_upgrade_00007(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00007(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -207,3 +228,127 @@ def test_image_mgmt_upgrade_00007(monkeypatch, module) -> None: assert merged_config["options"]["reboot"]["write_erase"] == False assert merged_config["options"]["package"]["install"] == False assert merged_config["options"]["package"]["uninstall"] == False + +def test_image_mgmt_upgrade_00008(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded. + 2. The methods called by commit are mocked to simulate that the + the image has already been staged and validated and the device + has already been upgraded to the desired version. + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing. + + + Expected results: + + 1. module.payload will equal a payload previously obtained by + running ansible-playbook against the controller for this scenario + """ + key = "test_image_mgmt_upgrade_00008a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + module.devices = [ + { + 'policy': 'KR5M', + 'stage': True, + 'upgrade': { + 'nxos': False, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': True + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': False + } + ] + module.commit() + + payload = payloads_image_upgrade(key) + assert module.payload == payload + +def test_image_mgmt_upgrade_00009(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + + Expected results: + + 1. module.payload will equal a payload previously obtained by + running ansible-playbook against the controller for this scenario + """ + key = "test_image_mgmt_upgrade_00009a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': False + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + module.commit() + + payload = payloads_image_upgrade(key) + assert isinstance(module.payload["devices"], list) + assert module.payload == payload + + + From 3c6acd10e8f50fef137622825b85b911eed7ee10 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 Nov 2023 10:11:19 -1000 Subject: [PATCH 069/300] Add unit tests, more... epld_module - Accept a module number in str() form and silently convert to int() --- .../module_utils/image_mgmt/image_upgrade.py | 34 +- .../image_upgrade_responses_ImageUpgrade.json | 21 + ...e_upgrade_responses_SwitchIssuDetails.json | 153 ++++++ .../test_image_upgrade_ImageUpgrade.py | 484 +++++++++++++++++- 4 files changed, 678 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index a66eaf52a..fa51bdb8e 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -664,8 +664,7 @@ def devices(self) -> List[Dict]: list() of dict() with the following structure: [ { - "serial_number": "FDO211218HH", - "policy_name": "NR1F" + "ip_address": "192.168.1.1" } ] Must be set before calling instance.commit() @@ -677,8 +676,22 @@ def devices(self, value: List[Dict]): method_name = inspect.stack()[0][3] if not isinstance(value, list): msg = f"{self.class_name}.{method_name}: " - msg += "instance.devices must be a python list of dict." + msg += "instance.devices must be a python list of dict. " + msg += f"Got {value}." self.module.fail_json(msg) + for device in value: + if not isinstance(device, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.devices must be a python list of dict. " + msg += f"Got {value}." + self.module.fail_json(msg) + if "ip_address" not in device: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.devices must be a python list of dict, " + msg += "where each dict contains the following keys: " + msg += "ip_address. " + msg += f"Got {value}." + self.module.fail_json(msg) self.properties["devices"] = value @property @@ -753,6 +766,10 @@ def epld_module(self, value): value = value.upper() except AttributeError: pass + try: + value = int(value) + except: + pass if not isinstance(value, int) and value != "ALL": msg = f"{self.class_name}.{method_name}: " msg += f"instance.epld_module must be an integer or 'ALL'" @@ -773,7 +790,7 @@ def force_non_disruptive(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " - msg += "instance.force_non_disruptivemust be a boolean." + msg += "instance.force_non_disruptive must be a boolean." self.module.fail_json(msg) self.properties["force_non_disruptive"] = value @@ -790,7 +807,7 @@ def non_disruptive(self): def non_disruptive(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}.setter: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.non_disruptive must be a boolean." self.module.fail_json(msg) self.properties["non_disruptive"] = value @@ -910,10 +927,3 @@ def response(self): instance.commit() must be called first. """ return self.properties.get("response") - - @property - def serial_numbers(self): - """ - Return a list of serial numbers from self.devices - """ - return [device.get("serial_number") for device in self.devices] diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json index 35dd81f2c..8fc905ef4 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json @@ -13,6 +13,27 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, + "test_image_mgmt_upgrade_00045a": { + "DATA": 121, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00046a": { + "DATA": 121, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00047a": { + "DATA": 121, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, "test_image_mgmt_upgrade_0000XX": { "DATA": { "error": "Selected upgrade option is 'isGolden'. It does not allow when upgrade options selected more than one option. " diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 597f90e6b..8eaa7cddc 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3179,5 +3179,158 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00045a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-07 23:47", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "KR5M", + "reason": "Upgrade", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "In-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "Success", + "upgradePercent": 100, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00046a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-07 23:47", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "KR5M", + "reason": "Upgrade", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "In-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "Success", + "upgradePercent": 100, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00047a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-07 23:47", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "KR5M", + "reason": "Upgrade", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "In-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "Success", + "upgradePercent": 100, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index d1b30a644..0f66c18ed 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -132,7 +132,7 @@ def test_image_mgmt_upgrade_00004(monkeypatch, module) -> None: Function description: ImageUpgrade.validate_devices updates the set ImageUpgrade.ip_addresses - with the ip addresses of the devices that have issu_detail.upgrade is + with the ip addresses of the devices for which issu_detail.upgrade is not "Failed" Expected results: @@ -347,8 +347,488 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): module.commit() payload = payloads_image_upgrade(key) - assert isinstance(module.payload["devices"], list) assert module.payload == payload +match_00030 = "ImageUpgrade.bios_force: instance.bios_force must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00030)) + ], +) +def test_image_mgmt_upgrade_00030(module, value, expected) -> None: + """ + ImageUpgrade.bios_force setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00030): + module.bios_force = value + else: + module.bios_force = value + assert module.bios_force == expected + + +match_00031 = "ImageUpgrade.config_reload: " +match_00031 += "instance.config_reload must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00031)) + ], +) +def test_image_mgmt_upgrade_00031(module, value, expected) -> None: + """ + ImageUpgrade.config_reload setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00031): + module.config_reload = value + else: + module.config_reload = value + assert module.config_reload == expected + + +match_00032_common = "ImageUpgrade.devices: " +match_00032_common += "instance.devices must be a python list of dict" + +match_00032_fail_1 = f"{match_00032_common}. Got not a list." +match_00032_fail_2 = fr"{match_00032_common}. Got \['not a dict'\]." + +match_00032_fail_3 = f"{match_00032_common}, where each dict contains " +match_00032_fail_3 += "the following keys: ip_address. " +match_00032_fail_3 += r"Got \[\{'bad_key_ip_address': '192.168.1.1'\}\]." + +data_00032_pass = [{"ip_address": "192.168.1.1"}] +data_00032_fail_1 = "not a list" +data_00032_fail_2 = ["not a dict"] +data_00032_fail_3 = [{"bad_key_ip_address": "192.168.1.1"}] +@pytest.mark.parametrize( + "value, expected", + [ + (data_00032_pass, does_not_raise()), + (data_00032_fail_1, pytest.raises(AnsibleFailJson, match=match_00032_fail_1)), + (data_00032_fail_2, pytest.raises(AnsibleFailJson, match=match_00032_fail_2)), + (data_00032_fail_3, pytest.raises(AnsibleFailJson, match=match_00032_fail_3)) + ], +) +def test_image_mgmt_upgrade_00032(module, value, expected) -> None: + """ + ImageUpgrade.devices setter + """ + with expected: + module.devices = value + + +match_00033 = "ImageUpgrade.disruptive: " +match_00033 += "instance.disruptive must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00033)) + ], +) +def test_image_mgmt_upgrade_00033(module, value, expected) -> None: + """ + ImageUpgrade.disruptive setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00033): + module.disruptive = value + else: + module.disruptive = value + assert module.disruptive == expected + + +match_00034 = "ImageUpgrade.epld_golden: " +match_00034 += "instance.epld_golden must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00034)) + ], +) +def test_image_mgmt_upgrade_00034(module, value, expected) -> None: + """ + ImageUpgrade.epld_golden setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00034): + module.epld_golden = value + else: + module.epld_golden = value + assert module.epld_golden == expected + + +match_00035 = "ImageUpgrade.epld_upgrade: " +match_00035 += "instance.epld_upgrade must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00035)) + ], +) +def test_image_mgmt_upgrade_00035(module, value, expected) -> None: + """ + ImageUpgrade.epld_upgrade setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00035): + module.epld_upgrade = value + else: + module.epld_upgrade = value + assert module.epld_upgrade == expected + + +match_00036_fail_1 = "ImageUpgrade.epld_module: " +match_00036_fail_1 += "instance.epld_module must be an integer or 'ALL'" +@pytest.mark.parametrize( + "value, expected", + [ + ("ALL", does_not_raise()), + (1, does_not_raise()), + (27, does_not_raise()), + ("27", does_not_raise()), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00036_fail_1)) + ], +) +def test_image_mgmt_upgrade_00036(module, value, expected) -> None: + """ + ImageUpgrade.epld_module setter + """ + with expected: + module.epld_module = value + + +match_00037 = "ImageUpgrade.force_non_disruptive: " +match_00037 += "instance.force_non_disruptive must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00037)) + ], +) +def test_image_mgmt_upgrade_00037(module, value, expected) -> None: + """ + ImageUpgrade.force_non_disruptive setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00037): + module.force_non_disruptive = value + else: + module.force_non_disruptive = value + assert module.force_non_disruptive == expected + + +match_00038 = "ImageUpgrade.non_disruptive: " +match_00038 += "instance.non_disruptive must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00038)) + ], +) +def test_image_mgmt_upgrade_00038(module, value, expected) -> None: + """ + ImageUpgrade.non_disruptive setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00038): + module.non_disruptive = value + else: + module.non_disruptive = value + assert module.non_disruptive == expected + + +match_00039 = "ImageUpgrade.package_install: " +match_00039 += "instance.package_install must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00039)) + ], +) +def test_image_mgmt_upgrade_00039(module, value, expected) -> None: + """ + ImageUpgrade.package_install setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00039): + module.package_install = value + else: + module.package_install = value + assert module.package_install == expected + + +match_00040 = "ImageUpgrade.package_uninstall: " +match_00040 += "instance.package_uninstall must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00040)) + ], +) +def test_image_mgmt_upgrade_00040(module, value, expected) -> None: + """ + ImageUpgrade.package_uninstall setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00040): + module.package_uninstall = value + else: + module.package_uninstall = value + assert module.package_uninstall == expected + + +match_00041 = "ImageUpgrade.reboot: " +match_00041 += "instance.reboot must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00041)) + ], +) +def test_image_mgmt_upgrade_00041(module, value, expected) -> None: + """ + ImageUpgrade.reboot setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00041): + module.reboot = value + else: + module.reboot = value + assert module.reboot == expected + + +match_00042 = "ImageUpgrade.write_erase: " +match_00042 += "instance.write_erase must be a boolean." +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00042)) + ], +) +def test_image_mgmt_upgrade_00042(module, value, expected) -> None: + """ + ImageUpgrade.write_erase setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00042): + module.write_erase = value + else: + module.write_erase = value + assert module.write_erase == expected + + +# getters + +def test_image_mgmt_upgrade_00043(module) -> None: + """ + ImageUpgrade.check_interval + """ + assert module.check_interval == 10 + + +def test_image_mgmt_upgrade_00044(module) -> None: + """ + ImageUpgrade.check_timeout + """ + assert module.check_timeout == 1800 + + +def test_image_mgmt_upgrade_00045(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.response_data + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded. + 2. The methods called by commit are mocked to simulate that the + the image has already been staged and validated and the device + has already been upgraded to the desired version. + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing. + + + Expected results: + + 1. module.response_data == 121 + """ + key = "test_image_mgmt_upgrade_00045a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + module.devices = [ + { + 'policy': 'KR5M', + 'stage': True, + 'upgrade': { + 'nxos': False, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': True + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': False + } + ] + module.commit() + assert module.response_data == 121 + + +def test_image_mgmt_upgrade_00046(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.result + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded. + 2. The methods called by commit are mocked to simulate that the + the image has already been staged and validated and the device + has already been upgraded to the desired version. + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing. + + + Expected results: + + 1. module.result == {'success': True, 'changed': True} + """ + key = "test_image_mgmt_upgrade_00046a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + module.devices = [ + { + 'policy': 'KR5M', + 'stage': True, + 'upgrade': { + 'nxos': False, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': True + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': False + } + ] + module.commit() + assert module.result == {'success': True, 'changed': True} + +def test_image_mgmt_upgrade_00047(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.response + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded. + 2. The methods called by commit are mocked to simulate that the + the image has already been staged and validated and the device + has already been upgraded to the desired version. + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing. + + + Expected results: + + 1. module.response is a dict + """ + key = "test_image_mgmt_upgrade_00047a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + module.devices = [ + { + 'policy': 'KR5M', + 'stage': True, + 'upgrade': { + 'nxos': False, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': True + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': False + } + ] + module.commit() + print(f"module.response: {module.response}") + assert isinstance(module.response, dict) + assert module.response["DATA"] == 121 From 9787f2ba31da1e92b550ab97f920369a317de346 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 Nov 2023 10:59:37 -1000 Subject: [PATCH 070/300] Add unit tests for _merge_defaults_to_switch_config --- .../image_upgrade_payloads_ImageUpgrade.json | 4 +- ...upgrade_responses_ImageInstallOptions.json | 4 +- .../image_upgrade_responses_ImageUpgrade.json | 4 +- ...e_upgrade_responses_SwitchIssuDetails.json | 4 +- .../test_image_upgrade_ImageUpgrade.py | 451 +++++++++++++++++- 5 files changed, 455 insertions(+), 12 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json index c05debed8..74c29cf34 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_upgrade_00008a": { + "test_image_mgmt_upgrade_00019a": { "devices": [ { "policyName": "KR5M", @@ -28,7 +28,7 @@ "writeErase": false } }, - "test_image_mgmt_upgrade_00009a": { + "test_image_mgmt_upgrade_00020a": { "devices": [ { "policyName": "NR3F", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json index 3084d1c50..38b6df1ac 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json @@ -210,7 +210,7 @@ "error": "Selected policy KR5M does not have package to continue." } }, - "test_image_mgmt_upgrade_00008a": { + "test_image_mgmt_upgrade_00019a": { "DATA": { "compatibilityStatusList": [ { @@ -265,7 +265,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00009a": { + "test_image_mgmt_upgrade_00020a": { "DATA": { "compatibilityStatusList": [ { diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json index 8fc905ef4..f0eb496dc 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json @@ -1,12 +1,12 @@ { - "test_image_mgmt_upgrade_00008a": { + "test_image_mgmt_upgrade_00019a": { "DATA": 121, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00009a": { + "test_image_mgmt_upgrade_00020a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 8eaa7cddc..24e0007bb 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3078,7 +3078,7 @@ "message": "" } }, - "test_image_mgmt_upgrade_00008a": { + "test_image_mgmt_upgrade_00019a": { "DATA": { "lastOperDataObject": [ { @@ -3129,7 +3129,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00009a": { + "test_image_mgmt_upgrade_00020a": { "DATA": { "lastOperDataObject": [ { diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 0f66c18ed..964784668 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -229,7 +229,450 @@ def test_image_mgmt_upgrade_00007(module) -> None: assert merged_config["options"]["package"]["install"] == False assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00008(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00008(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except upgrade.nxos. This forces the code + to take the upgrade.epld is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "upgrade": { + "nxos": False + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == False + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00009(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except upgrade.epld. This forces the code + to take the upgrade.nxos is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "upgrade": { + "epld": True + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == True + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00010(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options, which is empty. This forces + the code to take the options.nxos is None path and provide default + values for options.nxos and options.epld. + + Expected results: + + 1. merged_config will contain the expected default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {}, + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00011(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.nxos.mode. This forces the code + to take the options.nxos.bios_force is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": { + "nxos": { + "mode": "non_disruptive" + } + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "non_disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00012(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.nxos.bios_force. This forces the code + to take the options.nxos.mode is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": { + "nxos": { + "bios_force": True + } + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == True + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00013(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.epld.module. This forces + the code to take the options.epld.golden is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": { + "epld": { + "module": 27 + } + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == 27 + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00014(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.epld.golden. This forces + the code to take the options.epld.module is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": { + "epld": { + "golden": True + } + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == True + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00015(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.reboot.config_reload. This + forces the code to take the options.reboot.write_erase is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": { + "reboot": { + "config_reload": True + } + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == True + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00016(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.reboot.config_reload. This + forces the code to take the options.reboot.write_erase is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": { + "reboot": { + "write_erase": True + } + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == True + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00017(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.package.install. This + forces the code to take the options.package.uninstall is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": { + "package": { + "install": True + } + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == True + assert merged_config["options"]["package"]["uninstall"] == False + + +def test_image_mgmt_upgrade_00018(module) -> None: + """ + Function: ImageUpgrade._merge_defaults_to_switch_config + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.package.uninstall. This + forces the code to take the options.package.install is None path. + + Expected results: + + 1. merged_config will contain the expected default values + 2. merged_config will contain the expected non-default values + """ + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": { + "package": { + "uninstall": True + } + } + } + + merged_config = module._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] == False + assert merged_config["stage"] == True + assert merged_config["validate"] == True + assert merged_config["upgrade"]["nxos"] == True + assert merged_config["upgrade"]["epld"] == False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] == False + assert merged_config["options"]["reboot"]["config_reload"] == False + assert merged_config["options"]["reboot"]["write_erase"] == False + assert merged_config["options"]["package"]["install"] == False + assert merged_config["options"]["package"]["uninstall"] == True + + +def test_image_mgmt_upgrade_00019(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit @@ -248,7 +691,7 @@ def test_image_mgmt_upgrade_00008(monkeypatch, module) -> None: 1. module.payload will equal a payload previously obtained by running ansible-playbook against the controller for this scenario """ - key = "test_image_mgmt_upgrade_00008a" + key = "test_image_mgmt_upgrade_00019a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -289,7 +732,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): payload = payloads_image_upgrade(key) assert module.payload == payload -def test_image_mgmt_upgrade_00009(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00020(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit @@ -307,7 +750,7 @@ def test_image_mgmt_upgrade_00009(monkeypatch, module) -> None: 1. module.payload will equal a payload previously obtained by running ansible-playbook against the controller for this scenario """ - key = "test_image_mgmt_upgrade_00009a" + key = "test_image_mgmt_upgrade_00020a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: From 0e587d2e486ba807c0afcfdd1e45ed85d62a757c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 Nov 2023 11:24:53 -1000 Subject: [PATCH 071/300] build_payload: Add another unit test, more... ImageUpgrade.nxos_mode: sort valid_nxos_mode so that the match in corresponding unit-test works consistently. --- .../module_utils/image_mgmt/image_upgrade.py | 3 +- .../image_upgrade_payloads_ImageUpgrade.json | 29 +++++++++ ...upgrade_responses_ImageInstallOptions.json | 55 ++++++++++++++++ ...e_upgrade_responses_SwitchIssuDetails.json | 51 +++++++++++++++ .../test_image_upgrade_ImageUpgrade.py | 62 +++++++++++++++++++ 5 files changed, 199 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index fa51bdb8e..b34dc7e7e 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -358,7 +358,8 @@ def build_payload(self, device) -> None: if nxos_mode not in self.valid_nxos_mode: msg = f"{self.class_name}.{method_name}: " msg += "options.nxos.mode must be one of " - msg += f"{self.valid_nxos_mode}. Got {nxos_mode}." + msg += f"{sorted(self.valid_nxos_mode)}. " + msg += f"Got {nxos_mode}." self.module.fail_json(msg) if nxos_mode == "non_disruptive": diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json index 74c29cf34..985fc0671 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json @@ -56,5 +56,34 @@ "configReload": false, "writeErase": false } + }, + "test_image_mgmt_upgrade_00021a": { + "devices": [ + { + "policyName": "NR3F", + "serialNumber": "FDO21120U5D" + } + ], + "epldOptions": { + "golden": false, + "moduleNumber": "ALL" + }, + "epldUpgrade": true, + "issuUpgrade": true, + "issuUpgradeOptions1": { + "disruptive": true, + "forceNonDisruptive": false, + "nonDisruptive": false + }, + "issuUpgradeOptions2": { + "biosForce": false + }, + "pacakgeInstall": false, + "pacakgeUnInstall": false, + "reboot": false, + "rebootOptions": { + "configReload": false, + "writeErase": false + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json index 38b6df1ac..a8b2e2e20 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json @@ -319,5 +319,60 @@ "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00021a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 24e0007bb..0ccb96a1b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3180,6 +3180,57 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, + "test_image_mgmt_upgrade_00021a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, "test_image_mgmt_upgrade_00045a": { "DATA": { "lastOperDataObject": [ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 964784668..d3066fd3a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -793,6 +793,68 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): assert module.payload == payload +match_00021 = "ImageUpgrade.build_payload: options.nxos.mode must be one of " +match_00021 += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " +match_00021 += "Got FOO." +def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain an invalid nxos.mode value + + Expected results: + + 1. build_payload will call fail_json + """ + key = "test_image_mgmt_upgrade_00021a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'FOO', + 'bios_force': False + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + with pytest.raises(AnsibleFailJson, match=match_00021): + module.commit() + + match_00030 = "ImageUpgrade.bios_force: instance.bios_force must be a boolean." @pytest.mark.parametrize( "value, expected", From dd5f469e045292b7e117ed1439d9b242076c8d7e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 Nov 2023 12:43:02 -1000 Subject: [PATCH 072/300] Remove check for install_options.success, more... Removing since install_options will fail_json in the event of failure. --- plugins/module_utils/image_mgmt/image_upgrade.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index b34dc7e7e..360bf878d 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -326,14 +326,6 @@ def build_payload(self, device) -> None: msg += f"install_options.response: {json.dumps(self.install_options.response, indent=4, sort_keys=True)}" self.log_msg(msg) - # ImageInstallOptions may fail_json in the refresh() above, which is - # fine since it will contain more detailed information. - if self.install_options.result["success"] is False: - msg = f"{self.class_name}.{method_name}: " - msg += f"failed: {self.install_options.result}. " - msg += f"Controller response: {self.install_options.response}" - self.module.fail_json(msg) - # devices_to_upgrade must currently be a single device devices_to_upgrade: List[dict] = [] From 22015270a6afa0efc5f5b6ef7e7a1e17621fbae6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 Nov 2023 15:06:18 -1000 Subject: [PATCH 073/300] ImageUpgrade.commit - add more unit tests --- .../module_utils/image_mgmt/image_upgrade.py | 4 + ...upgrade_responses_ImageInstallOptions.json | 605 ++++++++ .../image_upgrade_responses_ImageUpgrade.json | 77 ++ ...e_upgrade_responses_SwitchIssuDetails.json | 561 ++++++++ .../test_image_upgrade_ImageUpgrade.py | 1226 +++++++++++++---- 5 files changed, 2203 insertions(+), 270 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 360bf878d..dd84790eb 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -441,6 +441,10 @@ def build_payload(self, device) -> None: package_uninstall = device.get("options").get("package").get("uninstall") if not isinstance(package_install, bool): + # This code is never hit since ImageInstallOptions calls + # fail_json on invalid options.package.install. + # We'll leave this here in case we change ImageInstallOptions + # in the future. msg = f"{self.class_name}.{method_name}: " msg += "options.package.install must be a boolean. " msg += f"Got {package_install}." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json index a8b2e2e20..d7f3a43f9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json @@ -374,5 +374,610 @@ "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00022a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00023a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00024a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00025a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00026a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00027a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00028a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00029a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00030a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00031a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00032a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json index f0eb496dc..9873ae545 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json @@ -13,6 +13,83 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, + "test_image_mgmt_upgrade_00022a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00023a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00024a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00025a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00026a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00027a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00028a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00029a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00030a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00031a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00032a": { + "DATA": 123, + "MESSAGE": "Internal Server Error", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 500 + }, "test_image_mgmt_upgrade_00045a": { "DATA": 121, "MESSAGE": "OK", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 0ccb96a1b..6a22d2ddc 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3231,6 +3231,567 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, + "test_image_mgmt_upgrade_00022a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00023a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00024a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00025a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00026a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00027a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00028a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00029a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00030a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00031a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00032a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, "test_image_mgmt_upgrade_00045a": { "DATA": { "lastOperDataObject": [ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index d3066fd3a..628d404da 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -793,9 +793,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): assert module.payload == payload -match_00021 = "ImageUpgrade.build_payload: options.nxos.mode must be one of " -match_00021 += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " -match_00021 += "Got FOO." def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit @@ -812,7 +809,7 @@ def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: Expected results: - 1. build_payload will call fail_json + 1. commit calls build_payload, which calls fail_json """ key = "test_image_mgmt_upgrade_00021a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -851,77 +848,957 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): 'policy_changed': True } ] - with pytest.raises(AnsibleFailJson, match=match_00021): + match = "ImageUpgrade.build_payload: options.nxos.mode must be one of " + match += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " + match += "Got FOO." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain nxos.mode non_disruptive + forcing the code to take nxos_mode == "non_disruptive" path + + Expected results: + + 1. self.payload["issuUpgradeOptions1"]["disruptive"] == False + 2. self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] == False + 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] == True + """ + key = "test_image_mgmt_upgrade_00022a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'non_disruptive', + 'bios_force': False + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + module.commit() + assert module.payload["issuUpgradeOptions1"]["disruptive"] == False + assert module.payload["issuUpgradeOptions1"]["forceNonDisruptive"] == False + assert module.payload["issuUpgradeOptions1"]["nonDisruptive"] == True + + +def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain nxos.mode force_non_disruptive + forcing the code to take nxos_mode == "force_non_disruptive" path + + Expected results: + + 1. self.payload["issuUpgradeOptions1"]["disruptive"] == False + 2. self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] == True + 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] == False + """ + key = "test_image_mgmt_upgrade_00023a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'force_non_disruptive', + 'bios_force': False + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + module.commit() + assert module.payload["issuUpgradeOptions1"]["disruptive"] == False + assert module.payload["issuUpgradeOptions1"]["forceNonDisruptive"] == True + assert module.payload["issuUpgradeOptions1"]["nonDisruptive"] == False + + +def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain invalid value for + options.nxos.bios_force + + Expected results: + + 1. commit calls build_payload which calls fail_json + """ + key = "test_image_mgmt_upgrade_00024a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': "FOO" + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.build_payload: " + match += r"options.nxos.bios_force must be a boolean. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00025(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain epld golden True and + upgrade.nxos True. + + Expected results: + + 1. commit calls build_payload which calls fail_json + """ + key = "test_image_mgmt_upgrade_00025a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'options': { + 'epld': { + 'module': 'ALL', + 'golden': True + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.build_payload: Invalid configuration for " + match += "172.22.150.102. If options.epld.golden is True " + match += "all other upgrade options, e.g. upgrade.nxos, " + match += "must be False." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00026(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain invalid epld.module + + Expected results: + + 1. commit calls build_payload which calls fail_json + """ + key = "test_image_mgmt_upgrade_00026a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'options': { + 'epld': { + 'module': 'FOO', + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.build_payload: " + match += "options.epld.module must either be 'ALL' " + match += r"or an integer. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00027(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain invalid epld.module + + Expected results: + + 1. commit calls build_payload which calls fail_json + """ + key = "test_image_mgmt_upgrade_00027a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'options': { + 'epld': { + 'golden': 'FOO', + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.build_payload: " + match += r"options.epld.golden must be a boolean. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00028(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain invalid value for reboot + + Expected results: + + 1. commit calls build_payload which calls fail_json + """ + key = "test_image_mgmt_upgrade_00028a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': True + }, + 'reboot': "FOO", + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.build_payload: " + match += r"reboot must be a boolean. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00029(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain invalid value for + options.reboot.config_reload + + Expected results: + + 1. commit calls build_payload which calls fail_json + """ + key = "test_image_mgmt_upgrade_00029a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'reboot': { + 'config_reload': "FOO", + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.build_payload: " + match += r"options.reboot.config_reload must be a boolean. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00030(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain invalid value for + options.reboot.config_reload + + Expected results: + + 1. commit calls build_payload which calls fail_json + """ + key = "test_image_mgmt_upgrade_00030a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'reboot': { + 'write_erase': "FOO", + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.build_payload: " + match += r"options.reboot.write_erase must be a boolean. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. module.devices is set to contain invalid value for + options.package.uninstall + + Expected results: + + 1. commit calls build_payload which calls fail_json + + NOTES: + 1. The corresponding test for options.package.install is missing. + It's not needed since ImageInstallOptions will call fail_json + on invalid values before ImageUpgrade has a chance to verify + the value. + """ + key = "test_image_mgmt_upgrade_00031a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'options': { + 'package': { + 'uninstall': "FOO", + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.build_payload: " + match += r"options.package.uninstall must be a boolean. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + +def test_image_mgmt_upgrade_00032(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + + 4. ImageUpgrade response (mock_dcnm_send_image_upgrade) is set + to return RESULT_CODE 500 with MESSAGE "Internal Server Error" + + Expected results: + + 1. commit calls fail_json because self.result will not equal "success" + + """ + key = "test_image_mgmt_upgrade_00032a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + + module.devices = [ + { + 'policy': 'NR3F', + 'stage': True, + 'upgrade': { + 'nxos': True, + 'epld': False + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': True + } + ] + match = "ImageUpgrade.commit: failed: " + match += r"\{'success': False, 'changed': False\}. " + match += r"Controller response: \{'DATA': 123, " + match += "'MESSAGE': 'Internal Server Error', 'METHOD': 'POST', " + match += "'REQUEST_PATH': " + match += "'https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/" + match += "imagemanagement/rest/imageupgrade/upgrade-image', " + match += r"'RETURN_CODE': 500\}" + with pytest.raises(AnsibleFailJson, match=match): module.commit() -match_00030 = "ImageUpgrade.bios_force: instance.bios_force must be a boolean." +# getters + +def test_image_mgmt_upgrade_00043(module) -> None: + """ + ImageUpgrade.check_interval + """ + assert module.check_interval == 10 + + +def test_image_mgmt_upgrade_00044(module) -> None: + """ + ImageUpgrade.check_timeout + """ + assert module.check_timeout == 1800 + + +def test_image_mgmt_upgrade_00045(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.response_data + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded. + 2. The methods called by commit are mocked to simulate that the + the image has already been staged and validated and the device + has already been upgraded to the desired version. + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing. + + + Expected results: + + 1. module.response_data == 121 + """ + key = "test_image_mgmt_upgrade_00045a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + module.devices = [ + { + 'policy': 'KR5M', + 'stage': True, + 'upgrade': { + 'nxos': False, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': True + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': False + } + ] + module.commit() + assert module.response_data == 121 + + +def test_image_mgmt_upgrade_00046(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.result + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded. + 2. The methods called by commit are mocked to simulate that the + the image has already been staged and validated and the device + has already been upgraded to the desired version. + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing. + + + Expected results: + + 1. module.result == {'success': True, 'changed': True} + """ + key = "test_image_mgmt_upgrade_00046a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + module.devices = [ + { + 'policy': 'KR5M', + 'stage': True, + 'upgrade': { + 'nxos': False, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': True + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': False + } + ] + module.commit() + assert module.result == {'success': True, 'changed': True} + + +def test_image_mgmt_upgrade_00047(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.response + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded. + 2. The methods called by commit are mocked to simulate that the + the image has already been staged and validated and the device + has already been upgraded to the desired version. + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing. + + + Expected results: + + 1. module.response is a dict + """ + key = "test_image_mgmt_upgrade_00047a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) + monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + + module.devices = [ + { + 'policy': 'KR5M', + 'stage': True, + 'upgrade': { + 'nxos': False, + 'epld': True + }, + 'options': { + 'nxos': { + 'mode': 'disruptive', + 'bios_force': True + } + }, + 'validate': True, + 'ip_address': '172.22.150.102', + 'policy_changed': False + } + ] + module.commit() + print(f"module.response: {module.response}") + assert isinstance(module.response, dict) + assert module.response["DATA"] == 121 + + +# setters + +match_00060 = "ImageUpgrade.bios_force: instance.bios_force must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00030)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00060)) ], ) -def test_image_mgmt_upgrade_00030(module, value, expected) -> None: +def test_image_mgmt_upgrade_00060(module, value, expected) -> None: """ ImageUpgrade.bios_force setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00030): + with pytest.raises(AnsibleFailJson, match=match_00060): module.bios_force = value else: module.bios_force = value assert module.bios_force == expected -match_00031 = "ImageUpgrade.config_reload: " -match_00031 += "instance.config_reload must be a boolean." +match_00061 = "ImageUpgrade.config_reload: " +match_00061 += "instance.config_reload must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00031)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00061)) ], ) -def test_image_mgmt_upgrade_00031(module, value, expected) -> None: +def test_image_mgmt_upgrade_00061(module, value, expected) -> None: """ ImageUpgrade.config_reload setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00031): + with pytest.raises(AnsibleFailJson, match=match_00061): module.config_reload = value else: module.config_reload = value assert module.config_reload == expected -match_00032_common = "ImageUpgrade.devices: " -match_00032_common += "instance.devices must be a python list of dict" +match_00062_common = "ImageUpgrade.devices: " +match_00062_common += "instance.devices must be a python list of dict" -match_00032_fail_1 = f"{match_00032_common}. Got not a list." -match_00032_fail_2 = fr"{match_00032_common}. Got \['not a dict'\]." +match_00062_fail_1 = f"{match_00062_common}. Got not a list." +match_00062_fail_2 = fr"{match_00062_common}. Got \['not a dict'\]." -match_00032_fail_3 = f"{match_00032_common}, where each dict contains " -match_00032_fail_3 += "the following keys: ip_address. " -match_00032_fail_3 += r"Got \[\{'bad_key_ip_address': '192.168.1.1'\}\]." +match_00062_fail_3 = f"{match_00062_common}, where each dict contains " +match_00062_fail_3 += "the following keys: ip_address. " +match_00062_fail_3 += r"Got \[\{'bad_key_ip_address': '192.168.1.1'\}\]." -data_00032_pass = [{"ip_address": "192.168.1.1"}] -data_00032_fail_1 = "not a list" -data_00032_fail_2 = ["not a dict"] -data_00032_fail_3 = [{"bad_key_ip_address": "192.168.1.1"}] +data_00062_pass = [{"ip_address": "192.168.1.1"}] +data_00062_fail_1 = "not a list" +data_00062_fail_2 = ["not a dict"] +data_00062_fail_3 = [{"bad_key_ip_address": "192.168.1.1"}] @pytest.mark.parametrize( "value, expected", [ - (data_00032_pass, does_not_raise()), - (data_00032_fail_1, pytest.raises(AnsibleFailJson, match=match_00032_fail_1)), - (data_00032_fail_2, pytest.raises(AnsibleFailJson, match=match_00032_fail_2)), - (data_00032_fail_3, pytest.raises(AnsibleFailJson, match=match_00032_fail_3)) + (data_00062_pass, does_not_raise()), + (data_00062_fail_1, pytest.raises(AnsibleFailJson, match=match_00062_fail_1)), + (data_00062_fail_2, pytest.raises(AnsibleFailJson, match=match_00062_fail_2)), + (data_00062_fail_3, pytest.raises(AnsibleFailJson, match=match_00062_fail_3)) ], ) -def test_image_mgmt_upgrade_00032(module, value, expected) -> None: +def test_image_mgmt_upgrade_00062(module, value, expected) -> None: """ ImageUpgrade.devices setter """ @@ -929,74 +1806,74 @@ def test_image_mgmt_upgrade_00032(module, value, expected) -> None: module.devices = value -match_00033 = "ImageUpgrade.disruptive: " -match_00033 += "instance.disruptive must be a boolean." +match_00063 = "ImageUpgrade.disruptive: " +match_00063 += "instance.disruptive must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00033)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00063)) ], ) -def test_image_mgmt_upgrade_00033(module, value, expected) -> None: +def test_image_mgmt_upgrade_00063(module, value, expected) -> None: """ ImageUpgrade.disruptive setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00033): + with pytest.raises(AnsibleFailJson, match=match_00063): module.disruptive = value else: module.disruptive = value assert module.disruptive == expected -match_00034 = "ImageUpgrade.epld_golden: " -match_00034 += "instance.epld_golden must be a boolean." +match_00064 = "ImageUpgrade.epld_golden: " +match_00064 += "instance.epld_golden must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00034)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00064)) ], ) -def test_image_mgmt_upgrade_00034(module, value, expected) -> None: +def test_image_mgmt_upgrade_00064(module, value, expected) -> None: """ ImageUpgrade.epld_golden setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00034): + with pytest.raises(AnsibleFailJson, match=match_00064): module.epld_golden = value else: module.epld_golden = value assert module.epld_golden == expected -match_00035 = "ImageUpgrade.epld_upgrade: " -match_00035 += "instance.epld_upgrade must be a boolean." +match_00065 = "ImageUpgrade.epld_upgrade: " +match_00065 += "instance.epld_upgrade must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00035)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00065)) ], ) -def test_image_mgmt_upgrade_00035(module, value, expected) -> None: +def test_image_mgmt_upgrade_00065(module, value, expected) -> None: """ ImageUpgrade.epld_upgrade setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00035): + with pytest.raises(AnsibleFailJson, match=match_00065): module.epld_upgrade = value else: module.epld_upgrade = value assert module.epld_upgrade == expected -match_00036_fail_1 = "ImageUpgrade.epld_module: " -match_00036_fail_1 += "instance.epld_module must be an integer or 'ALL'" +match_00066_fail_1 = "ImageUpgrade.epld_module: " +match_00066_fail_1 += "instance.epld_module must be an integer or 'ALL'" @pytest.mark.parametrize( "value, expected", [ @@ -1004,10 +1881,10 @@ def test_image_mgmt_upgrade_00035(module, value, expected) -> None: (1, does_not_raise()), (27, does_not_raise()), ("27", does_not_raise()), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00036_fail_1)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00066_fail_1)) ], ) -def test_image_mgmt_upgrade_00036(module, value, expected) -> None: +def test_image_mgmt_upgrade_00066(module, value, expected) -> None: """ ImageUpgrade.epld_module setter """ @@ -1015,325 +1892,134 @@ def test_image_mgmt_upgrade_00036(module, value, expected) -> None: module.epld_module = value -match_00037 = "ImageUpgrade.force_non_disruptive: " -match_00037 += "instance.force_non_disruptive must be a boolean." +match_00067 = "ImageUpgrade.force_non_disruptive: " +match_00067 += "instance.force_non_disruptive must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00037)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00067)) ], ) -def test_image_mgmt_upgrade_00037(module, value, expected) -> None: +def test_image_mgmt_upgrade_00067(module, value, expected) -> None: """ ImageUpgrade.force_non_disruptive setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00037): + with pytest.raises(AnsibleFailJson, match=match_00067): module.force_non_disruptive = value else: module.force_non_disruptive = value assert module.force_non_disruptive == expected -match_00038 = "ImageUpgrade.non_disruptive: " -match_00038 += "instance.non_disruptive must be a boolean." +match_00068 = "ImageUpgrade.non_disruptive: " +match_00068 += "instance.non_disruptive must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00038)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00068)) ], ) -def test_image_mgmt_upgrade_00038(module, value, expected) -> None: +def test_image_mgmt_upgrade_00068(module, value, expected) -> None: """ ImageUpgrade.non_disruptive setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00038): + with pytest.raises(AnsibleFailJson, match=match_00068): module.non_disruptive = value else: module.non_disruptive = value assert module.non_disruptive == expected -match_00039 = "ImageUpgrade.package_install: " -match_00039 += "instance.package_install must be a boolean." +match_00069 = "ImageUpgrade.package_install: " +match_00069 += "instance.package_install must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00039)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00069)) ], ) -def test_image_mgmt_upgrade_00039(module, value, expected) -> None: +def test_image_mgmt_upgrade_00069(module, value, expected) -> None: """ ImageUpgrade.package_install setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00039): + with pytest.raises(AnsibleFailJson, match=match_00069): module.package_install = value else: module.package_install = value assert module.package_install == expected -match_00040 = "ImageUpgrade.package_uninstall: " -match_00040 += "instance.package_uninstall must be a boolean." +match_00070 = "ImageUpgrade.package_uninstall: " +match_00070 += "instance.package_uninstall must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00040)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00070)) ], ) -def test_image_mgmt_upgrade_00040(module, value, expected) -> None: +def test_image_mgmt_upgrade_00070(module, value, expected) -> None: """ ImageUpgrade.package_uninstall setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00040): + with pytest.raises(AnsibleFailJson, match=match_00070): module.package_uninstall = value else: module.package_uninstall = value assert module.package_uninstall == expected -match_00041 = "ImageUpgrade.reboot: " -match_00041 += "instance.reboot must be a boolean." +match_00071 = "ImageUpgrade.reboot: " +match_00071 += "instance.reboot must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00041)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00071)) ], ) -def test_image_mgmt_upgrade_00041(module, value, expected) -> None: +def test_image_mgmt_upgrade_00071(module, value, expected) -> None: """ ImageUpgrade.reboot setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00041): + with pytest.raises(AnsibleFailJson, match=match_00071): module.reboot = value else: module.reboot = value assert module.reboot == expected -match_00042 = "ImageUpgrade.write_erase: " -match_00042 += "instance.write_erase must be a boolean." +match_00072 = "ImageUpgrade.write_erase: " +match_00072 += "instance.write_erase must be a boolean." @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00042)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00072)) ], ) -def test_image_mgmt_upgrade_00042(module, value, expected) -> None: +def test_image_mgmt_upgrade_00072(module, value, expected) -> None: """ ImageUpgrade.write_erase setter """ if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00042): + with pytest.raises(AnsibleFailJson, match=match_00072): module.write_erase = value else: module.write_erase = value assert module.write_erase == expected - -# getters - -def test_image_mgmt_upgrade_00043(module) -> None: - """ - ImageUpgrade.check_interval - """ - assert module.check_interval == 10 - - -def test_image_mgmt_upgrade_00044(module) -> None: - """ - ImageUpgrade.check_timeout - """ - assert module.check_timeout == 1800 - - -def test_image_mgmt_upgrade_00045(monkeypatch, module) -> None: - """ - Function: ImageUpgrade.response_data - - Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device - to be upgraded. - 2. The methods called by commit are mocked to simulate that the - the image has already been staged and validated and the device - has already been upgraded to the desired version. - 3. Methods called by commit that wait for current actions, and - image upgrade, to complete are mocked to do nothing. - - - Expected results: - - 1. module.response_data == 121 - """ - key = "test_image_mgmt_upgrade_00045a" - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return {} - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): - pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - - module.devices = [ - { - 'policy': 'KR5M', - 'stage': True, - 'upgrade': { - 'nxos': False, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': True - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': False - } - ] - module.commit() - assert module.response_data == 121 - - -def test_image_mgmt_upgrade_00046(monkeypatch, module) -> None: - """ - Function: ImageUpgrade.result - - Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device - to be upgraded. - 2. The methods called by commit are mocked to simulate that the - the image has already been staged and validated and the device - has already been upgraded to the desired version. - 3. Methods called by commit that wait for current actions, and - image upgrade, to complete are mocked to do nothing. - - - Expected results: - - 1. module.result == {'success': True, 'changed': True} - """ - key = "test_image_mgmt_upgrade_00046a" - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return {} - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): - pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - - module.devices = [ - { - 'policy': 'KR5M', - 'stage': True, - 'upgrade': { - 'nxos': False, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': True - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': False - } - ] - module.commit() - assert module.result == {'success': True, 'changed': True} - - -def test_image_mgmt_upgrade_00047(monkeypatch, module) -> None: - """ - Function: ImageUpgrade.response - - Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device - to be upgraded. - 2. The methods called by commit are mocked to simulate that the - the image has already been staged and validated and the device - has already been upgraded to the desired version. - 3. Methods called by commit that wait for current actions, and - image upgrade, to complete are mocked to do nothing. - - - Expected results: - - 1. module.response is a dict - """ - key = "test_image_mgmt_upgrade_00047a" - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return {} - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): - pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - - module.devices = [ - { - 'policy': 'KR5M', - 'stage': True, - 'upgrade': { - 'nxos': False, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': True - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': False - } - ] - module.commit() - print(f"module.response: {module.response}") - assert isinstance(module.response, dict) - assert module.response["DATA"] == 121 From 8611263b468f7ee9db2e1ac067e5a2e85652e891 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 Nov 2023 15:22:58 -1000 Subject: [PATCH 074/300] run through black/isort --- .../test_image_upgrade_ImageUpgrade.py | 821 ++++++++++-------- 1 file changed, 469 insertions(+), 352 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 628d404da..e5e72c692 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -34,18 +34,21 @@ def payloads_image_upgrade(key: str) -> Dict[str, str]: print(f"payload_data_image_upgrade: {key} : {payload}") return payload + def responses_image_upgrade(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_ImageUpgrade" response = load_fixture(response_file).get(key) print(f"response_data_image_upgrade: {key} : {response}") return response + def responses_install_options(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_ImageInstallOptions" response = load_fixture(response_file).get(key) print(f"response_data_install_options: {key} : {response}") return response + def responses_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -229,6 +232,7 @@ def test_image_mgmt_upgrade_00007(module) -> None: assert merged_config["options"]["package"]["install"] == False assert merged_config["options"]["package"]["uninstall"] == False + def test_image_mgmt_upgrade_00008(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -247,9 +251,7 @@ def test_image_mgmt_upgrade_00008(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "upgrade": { - "nxos": False - } + "upgrade": {"nxos": False}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -286,9 +288,7 @@ def test_image_mgmt_upgrade_00009(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "upgrade": { - "epld": True - } + "upgrade": {"epld": True}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -362,11 +362,7 @@ def test_image_mgmt_upgrade_00011(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "options": { - "nxos": { - "mode": "non_disruptive" - } - } + "options": {"nxos": {"mode": "non_disruptive"}}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -403,11 +399,7 @@ def test_image_mgmt_upgrade_00012(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "options": { - "nxos": { - "bios_force": True - } - } + "options": {"nxos": {"bios_force": True}}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -444,11 +436,7 @@ def test_image_mgmt_upgrade_00013(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "options": { - "epld": { - "module": 27 - } - } + "options": {"epld": {"module": 27}}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -485,11 +473,7 @@ def test_image_mgmt_upgrade_00014(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "options": { - "epld": { - "golden": True - } - } + "options": {"epld": {"golden": True}}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -526,11 +510,7 @@ def test_image_mgmt_upgrade_00015(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "options": { - "reboot": { - "config_reload": True - } - } + "options": {"reboot": {"config_reload": True}}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -567,11 +547,7 @@ def test_image_mgmt_upgrade_00016(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "options": { - "reboot": { - "write_erase": True - } - } + "options": {"reboot": {"write_erase": True}}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -608,11 +584,7 @@ def test_image_mgmt_upgrade_00017(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "options": { - "package": { - "install": True - } - } + "options": {"package": {"install": True}}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -649,11 +621,7 @@ def test_image_mgmt_upgrade_00018(module) -> None: "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, - "options": { - "package": { - "uninstall": True - } - } + "options": {"package": {"uninstall": True}}, } merged_config = module._merge_defaults_to_switch_config(config) @@ -692,45 +660,51 @@ def test_image_mgmt_upgrade_00019(monkeypatch, module) -> None: running ansible-playbook against the controller for this scenario """ key = "test_image_mgmt_upgrade_00019a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'KR5M', - 'stage': True, - 'upgrade': { - 'nxos': False, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': True - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': False + "policy": "KR5M", + "stage": True, + "upgrade": {"nxos": False, "epld": True}, + "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": False, } ] module.commit() - payload = payloads_image_upgrade(key) - assert module.payload == payload + assert module.payload == payloads_image_upgrade(key) + def test_image_mgmt_upgrade_00020(monkeypatch, module) -> None: """ @@ -751,46 +725,50 @@ def test_image_mgmt_upgrade_00020(monkeypatch, module) -> None: running ansible-playbook against the controller for this scenario """ key = "test_image_mgmt_upgrade_00020a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': False - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "options": {"nxos": {"mode": "disruptive", "bios_force": False}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] module.commit() - payload = payloads_image_upgrade(key) - assert module.payload == payload + assert module.payload == payloads_image_upgrade(key) def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: @@ -812,40 +790,45 @@ def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: 1. commit calls build_payload, which calls fail_json """ key = "test_image_mgmt_upgrade_00021a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'FOO', - 'bios_force': False - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "options": {"nxos": {"mode": "FOO", "bios_force": False}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: options.nxos.mode must be one of " @@ -877,40 +860,45 @@ def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] == True """ key = "test_image_mgmt_upgrade_00022a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'non_disruptive', - 'bios_force': False - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "options": {"nxos": {"mode": "non_disruptive", "bios_force": False}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] module.commit() @@ -941,40 +929,45 @@ def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] == False """ key = "test_image_mgmt_upgrade_00023a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'force_non_disruptive', - 'bios_force': False - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "options": {"nxos": {"mode": "force_non_disruptive", "bios_force": False}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] module.commit() @@ -995,7 +988,7 @@ def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for + 4. module.devices is set to contain invalid value for options.nxos.bios_force Expected results: @@ -1003,40 +996,45 @@ def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: 1. commit calls build_payload which calls fail_json """ key = "test_image_mgmt_upgrade_00024a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': "FOO" - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "options": {"nxos": {"mode": "disruptive", "bios_force": "FOO"}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: " @@ -1057,7 +1055,7 @@ def test_image_mgmt_upgrade_00025(monkeypatch, module) -> None: 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain epld golden True and + 4. module.devices is set to contain epld golden True and upgrade.nxos True. Expected results: @@ -1065,40 +1063,45 @@ def test_image_mgmt_upgrade_00025(monkeypatch, module) -> None: 1. commit calls build_payload which calls fail_json """ key = "test_image_mgmt_upgrade_00025a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'options': { - 'epld': { - 'module': 'ALL', - 'golden': True - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "options": {"epld": {"module": "ALL", "golden": True}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: Invalid configuration for " @@ -1121,46 +1124,56 @@ def test_image_mgmt_upgrade_00026(monkeypatch, module) -> None: 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid epld.module + 4. module.devices is set to contain invalid epld.module Expected results: 1. commit calls build_payload which calls fail_json """ key = "test_image_mgmt_upgrade_00026a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'options': { - 'epld': { - 'module': 'FOO', + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "options": { + "epld": { + "module": "FOO", } }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: " @@ -1182,46 +1195,56 @@ def test_image_mgmt_upgrade_00027(monkeypatch, module) -> None: 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid epld.module + 4. module.devices is set to contain invalid epld.module Expected results: 1. commit calls build_payload which calls fail_json """ key = "test_image_mgmt_upgrade_00027a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'options': { - 'epld': { - 'golden': 'FOO', + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "options": { + "epld": { + "golden": "FOO", } }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: " @@ -1242,42 +1265,52 @@ def test_image_mgmt_upgrade_00028(monkeypatch, module) -> None: 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for reboot + 4. module.devices is set to contain invalid value for reboot Expected results: 1. commit calls build_payload which calls fail_json """ key = "test_image_mgmt_upgrade_00028a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': True - }, - 'reboot': "FOO", - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": True}, + "reboot": "FOO", + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: " @@ -1299,46 +1332,56 @@ def test_image_mgmt_upgrade_00029(monkeypatch, module) -> None: image upgrade, to complete are mocked to do nothing 4. module.devices is set to contain invalid value for - options.reboot.config_reload + options.reboot.config_reload Expected results: 1. commit calls build_payload which calls fail_json """ key = "test_image_mgmt_upgrade_00029a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': False - }, - 'options': { - 'reboot': { - 'config_reload': "FOO", + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": False}, + "options": { + "reboot": { + "config_reload": "FOO", } }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: " @@ -1360,46 +1403,56 @@ def test_image_mgmt_upgrade_00030(monkeypatch, module) -> None: image upgrade, to complete are mocked to do nothing 4. module.devices is set to contain invalid value for - options.reboot.config_reload + options.reboot.config_reload Expected results: 1. commit calls build_payload which calls fail_json """ key = "test_image_mgmt_upgrade_00030a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': False - }, - 'options': { - 'reboot': { - 'write_erase': "FOO", + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": False}, + "options": { + "reboot": { + "write_erase": "FOO", } }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: " @@ -1421,7 +1474,7 @@ def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: image upgrade, to complete are mocked to do nothing 4. module.devices is set to contain invalid value for - options.package.uninstall + options.package.uninstall Expected results: @@ -1434,39 +1487,49 @@ def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: the value. """ key = "test_image_mgmt_upgrade_00031a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': False - }, - 'options': { - 'package': { - 'uninstall': "FOO", + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": False}, + "options": { + "package": { + "uninstall": "FOO", } }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.build_payload: " @@ -1496,34 +1559,44 @@ def test_image_mgmt_upgrade_00032(monkeypatch, module) -> None: """ key = "test_image_mgmt_upgrade_00032a" + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_install_options(key) + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) - + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'NR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': False - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': True + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": False}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, } ] match = "ImageUpgrade.commit: failed: " @@ -1540,6 +1613,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): # getters + def test_image_mgmt_upgrade_00043(module) -> None: """ ImageUpgrade.check_interval @@ -1573,43 +1647,49 @@ def test_image_mgmt_upgrade_00045(monkeypatch, module) -> None: 1. module.response_data == 121 """ key = "test_image_mgmt_upgrade_00045a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'KR5M', - 'stage': True, - 'upgrade': { - 'nxos': False, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': True - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': False + "policy": "KR5M", + "stage": True, + "upgrade": {"nxos": False, "epld": True}, + "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": False, } ] module.commit() - assert module.response_data == 121 + assert module.response_data == 121 def test_image_mgmt_upgrade_00046(monkeypatch, module) -> None: @@ -1631,43 +1711,49 @@ def test_image_mgmt_upgrade_00046(monkeypatch, module) -> None: 1. module.result == {'success': True, 'changed': True} """ key = "test_image_mgmt_upgrade_00046a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'KR5M', - 'stage': True, - 'upgrade': { - 'nxos': False, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': True - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': False + "policy": "KR5M", + "stage": True, + "upgrade": {"nxos": False, "epld": True}, + "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": False, } ] module.commit() - assert module.result == {'success': True, 'changed': True} + assert module.result == {"success": True, "changed": True} def test_image_mgmt_upgrade_00047(monkeypatch, module) -> None: @@ -1689,39 +1775,45 @@ def test_image_mgmt_upgrade_00047(monkeypatch, module) -> None: 1. module.response is a dict """ key = "test_image_mgmt_upgrade_00047a" + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_issu_details(key) + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) + def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) - monkeypatch.setattr(module, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete) - monkeypatch.setattr(module, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete) + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) module.devices = [ { - 'policy': 'KR5M', - 'stage': True, - 'upgrade': { - 'nxos': False, - 'epld': True - }, - 'options': { - 'nxos': { - 'mode': 'disruptive', - 'bios_force': True - } - }, - 'validate': True, - 'ip_address': '172.22.150.102', - 'policy_changed': False + "policy": "KR5M", + "stage": True, + "upgrade": {"nxos": False, "epld": True}, + "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": False, } ] module.commit() @@ -1733,12 +1825,14 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): # setters match_00060 = "ImageUpgrade.bios_force: instance.bios_force must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00060)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00060)), ], ) def test_image_mgmt_upgrade_00060(module, value, expected) -> None: @@ -1755,12 +1849,14 @@ def test_image_mgmt_upgrade_00060(module, value, expected) -> None: match_00061 = "ImageUpgrade.config_reload: " match_00061 += "instance.config_reload must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00061)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00061)), ], ) def test_image_mgmt_upgrade_00061(module, value, expected) -> None: @@ -1779,9 +1875,9 @@ def test_image_mgmt_upgrade_00061(module, value, expected) -> None: match_00062_common += "instance.devices must be a python list of dict" match_00062_fail_1 = f"{match_00062_common}. Got not a list." -match_00062_fail_2 = fr"{match_00062_common}. Got \['not a dict'\]." +match_00062_fail_2 = rf"{match_00062_common}. Got \['not a dict'\]." -match_00062_fail_3 = f"{match_00062_common}, where each dict contains " +match_00062_fail_3 = f"{match_00062_common}, where each dict contains " match_00062_fail_3 += "the following keys: ip_address. " match_00062_fail_3 += r"Got \[\{'bad_key_ip_address': '192.168.1.1'\}\]." @@ -1789,13 +1885,15 @@ def test_image_mgmt_upgrade_00061(module, value, expected) -> None: data_00062_fail_1 = "not a list" data_00062_fail_2 = ["not a dict"] data_00062_fail_3 = [{"bad_key_ip_address": "192.168.1.1"}] + + @pytest.mark.parametrize( "value, expected", [ (data_00062_pass, does_not_raise()), (data_00062_fail_1, pytest.raises(AnsibleFailJson, match=match_00062_fail_1)), (data_00062_fail_2, pytest.raises(AnsibleFailJson, match=match_00062_fail_2)), - (data_00062_fail_3, pytest.raises(AnsibleFailJson, match=match_00062_fail_3)) + (data_00062_fail_3, pytest.raises(AnsibleFailJson, match=match_00062_fail_3)), ], ) def test_image_mgmt_upgrade_00062(module, value, expected) -> None: @@ -1808,12 +1906,14 @@ def test_image_mgmt_upgrade_00062(module, value, expected) -> None: match_00063 = "ImageUpgrade.disruptive: " match_00063 += "instance.disruptive must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00063)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00063)), ], ) def test_image_mgmt_upgrade_00063(module, value, expected) -> None: @@ -1830,12 +1930,14 @@ def test_image_mgmt_upgrade_00063(module, value, expected) -> None: match_00064 = "ImageUpgrade.epld_golden: " match_00064 += "instance.epld_golden must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00064)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00064)), ], ) def test_image_mgmt_upgrade_00064(module, value, expected) -> None: @@ -1852,12 +1954,14 @@ def test_image_mgmt_upgrade_00064(module, value, expected) -> None: match_00065 = "ImageUpgrade.epld_upgrade: " match_00065 += "instance.epld_upgrade must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00065)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00065)), ], ) def test_image_mgmt_upgrade_00065(module, value, expected) -> None: @@ -1874,6 +1978,8 @@ def test_image_mgmt_upgrade_00065(module, value, expected) -> None: match_00066_fail_1 = "ImageUpgrade.epld_module: " match_00066_fail_1 += "instance.epld_module must be an integer or 'ALL'" + + @pytest.mark.parametrize( "value, expected", [ @@ -1881,7 +1987,7 @@ def test_image_mgmt_upgrade_00065(module, value, expected) -> None: (1, does_not_raise()), (27, does_not_raise()), ("27", does_not_raise()), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00066_fail_1)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00066_fail_1)), ], ) def test_image_mgmt_upgrade_00066(module, value, expected) -> None: @@ -1894,12 +2000,14 @@ def test_image_mgmt_upgrade_00066(module, value, expected) -> None: match_00067 = "ImageUpgrade.force_non_disruptive: " match_00067 += "instance.force_non_disruptive must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00067)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00067)), ], ) def test_image_mgmt_upgrade_00067(module, value, expected) -> None: @@ -1916,12 +2024,14 @@ def test_image_mgmt_upgrade_00067(module, value, expected) -> None: match_00068 = "ImageUpgrade.non_disruptive: " match_00068 += "instance.non_disruptive must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00068)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00068)), ], ) def test_image_mgmt_upgrade_00068(module, value, expected) -> None: @@ -1938,12 +2048,14 @@ def test_image_mgmt_upgrade_00068(module, value, expected) -> None: match_00069 = "ImageUpgrade.package_install: " match_00069 += "instance.package_install must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00069)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00069)), ], ) def test_image_mgmt_upgrade_00069(module, value, expected) -> None: @@ -1960,12 +2072,14 @@ def test_image_mgmt_upgrade_00069(module, value, expected) -> None: match_00070 = "ImageUpgrade.package_uninstall: " match_00070 += "instance.package_uninstall must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00070)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00070)), ], ) def test_image_mgmt_upgrade_00070(module, value, expected) -> None: @@ -1982,12 +2096,14 @@ def test_image_mgmt_upgrade_00070(module, value, expected) -> None: match_00071 = "ImageUpgrade.reboot: " match_00071 += "instance.reboot must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00071)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00071)), ], ) def test_image_mgmt_upgrade_00071(module, value, expected) -> None: @@ -2004,12 +2120,14 @@ def test_image_mgmt_upgrade_00071(module, value, expected) -> None: match_00072 = "ImageUpgrade.write_erase: " match_00072 += "instance.write_erase must be a boolean." + + @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00072)) + ("FOO", pytest.raises(AnsibleFailJson, match=match_00072)), ], ) def test_image_mgmt_upgrade_00072(module, value, expected) -> None: @@ -2022,4 +2140,3 @@ def test_image_mgmt_upgrade_00072(module, value, expected) -> None: else: module.write_erase = value assert module.write_erase == expected - From 9c9b5cb6528c1699964d34170a0d9e7f25c16751 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 06:50:45 -1000 Subject: [PATCH 075/300] build_payload: convert boolean-like strings to boolean Give the user some leeway in setting boolean-like items e.g. instead of True, let the user enter "true" or "TRUE" --- .../module_utils/image_mgmt/image_upgrade.py | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index dd84790eb..bc9f2e882 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -354,16 +354,20 @@ def build_payload(self, device) -> None: msg += f"Got {nxos_mode}." self.module.fail_json(msg) + verify_nxos_mode_list = [] if nxos_mode == "non_disruptive": + verify_nxos_mode_list.append(True) self.payload["issuUpgradeOptions1"]["nonDisruptive"] = True if nxos_mode == "disruptive": + verify_nxos_mode_list.append(True) self.payload["issuUpgradeOptions1"]["disruptive"] = True if nxos_mode == "force_non_disruptive": + verify_nxos_mode_list.append(True) self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True # biosForce corresponds to BIOS Force GUI option bios_force = device.get("options").get("nxos").get("bios_force") - + bios_force = self.make_bool(bios_force) if not isinstance(bios_force, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.nxos.bios_force must be a boolean. " @@ -376,6 +380,22 @@ def build_payload(self, device) -> None: # EPLD epld_module = device.get("options").get("epld").get("module") epld_golden = device.get("options").get("epld").get("golden") + epld_upgrade = device.get("upgrade").get("epld") + + epld_golden = self.make_bool(epld_golden) + if not isinstance(epld_golden, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "options.epld.golden must be a boolean. " + msg += f"Got {epld_golden}." + self.module.fail_json(msg) + + epld_upgrade = self.make_bool(epld_upgrade) + if not isinstance(epld_upgrade, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "upgrade.epld must be a boolean. " + msg += f"Got {epld_upgrade}." + self.module.fail_json(msg) + if epld_golden is True and device.get("upgrade").get("nxos") is True: msg = f"{self.class_name}.{method_name}: " msg += "Invalid configuration for " @@ -394,13 +414,8 @@ def build_payload(self, device) -> None: msg += f"or an integer. Got {epld_module}." self.module.fail_json(msg) - if not isinstance(epld_golden, bool): - msg = f"{self.class_name}.{method_name}: " - msg += "options.epld.golden must be a boolean. " - msg += f"Got {epld_golden}." - self.module.fail_json(msg) - self.payload["epldUpgrade"] = device.get("upgrade").get("epld") + self.payload["epldUpgrade"] = epld_upgrade self.payload["epldOptions"] = {} self.payload["epldOptions"]["moduleNumber"] = epld_module self.payload["epldOptions"]["golden"] = epld_golden @@ -409,6 +424,7 @@ def build_payload(self, device) -> None: # Reboot reboot = device.get("reboot") + reboot = self.make_bool(reboot) if not isinstance(reboot, bool): msg = f"{self.class_name}.{method_name}: " msg += "reboot must be a boolean. " @@ -420,12 +436,14 @@ def build_payload(self, device) -> None: config_reload = device.get("options").get("reboot").get("config_reload") write_erase = device.get("options").get("reboot").get("write_erase") + config_reload = self.make_bool(config_reload) if not isinstance(config_reload, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.config_reload must be a boolean. " msg += f"Got {config_reload}." self.module.fail_json(msg) + write_erase = self.make_bool(write_erase) if not isinstance(write_erase, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.write_erase must be a boolean. " @@ -440,6 +458,7 @@ def build_payload(self, device) -> None: package_install = device.get("options").get("package").get("install") package_uninstall = device.get("options").get("package").get("uninstall") + package_install = self.make_bool(package_install) if not isinstance(package_install, bool): # This code is never hit since ImageInstallOptions calls # fail_json on invalid options.package.install. @@ -450,6 +469,7 @@ def build_payload(self, device) -> None: msg += f"Got {package_install}." self.module.fail_json(msg) + package_uninstall = self.make_bool(package_uninstall) if not isinstance(package_uninstall, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.package.uninstall must be a boolean. " From 1e54622c1b8cf2516405cf6acc060a63c789e130 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 07:00:05 -1000 Subject: [PATCH 076/300] Fix bad method name make_bool -> make_boolean --- plugins/module_utils/image_mgmt/image_upgrade.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index bc9f2e882..ba1a37fad 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -367,7 +367,7 @@ def build_payload(self, device) -> None: # biosForce corresponds to BIOS Force GUI option bios_force = device.get("options").get("nxos").get("bios_force") - bios_force = self.make_bool(bios_force) + bios_force = self.make_boolean(bios_force) if not isinstance(bios_force, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.nxos.bios_force must be a boolean. " @@ -382,14 +382,14 @@ def build_payload(self, device) -> None: epld_golden = device.get("options").get("epld").get("golden") epld_upgrade = device.get("upgrade").get("epld") - epld_golden = self.make_bool(epld_golden) + epld_golden = self.make_boolean(epld_golden) if not isinstance(epld_golden, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.epld.golden must be a boolean. " msg += f"Got {epld_golden}." self.module.fail_json(msg) - epld_upgrade = self.make_bool(epld_upgrade) + epld_upgrade = self.make_boolean(epld_upgrade) if not isinstance(epld_upgrade, bool): msg = f"{self.class_name}.{method_name}: " msg += "upgrade.epld must be a boolean. " @@ -424,7 +424,7 @@ def build_payload(self, device) -> None: # Reboot reboot = device.get("reboot") - reboot = self.make_bool(reboot) + reboot = self.make_boolean(reboot) if not isinstance(reboot, bool): msg = f"{self.class_name}.{method_name}: " msg += "reboot must be a boolean. " @@ -436,14 +436,14 @@ def build_payload(self, device) -> None: config_reload = device.get("options").get("reboot").get("config_reload") write_erase = device.get("options").get("reboot").get("write_erase") - config_reload = self.make_bool(config_reload) + config_reload = self.make_boolean(config_reload) if not isinstance(config_reload, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.config_reload must be a boolean. " msg += f"Got {config_reload}." self.module.fail_json(msg) - write_erase = self.make_bool(write_erase) + write_erase = self.make_boolean(write_erase) if not isinstance(write_erase, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.write_erase must be a boolean. " @@ -458,7 +458,7 @@ def build_payload(self, device) -> None: package_install = device.get("options").get("package").get("install") package_uninstall = device.get("options").get("package").get("uninstall") - package_install = self.make_bool(package_install) + package_install = self.make_boolean(package_install) if not isinstance(package_install, bool): # This code is never hit since ImageInstallOptions calls # fail_json on invalid options.package.install. @@ -469,7 +469,7 @@ def build_payload(self, device) -> None: msg += f"Got {package_install}." self.module.fail_json(msg) - package_uninstall = self.make_bool(package_uninstall) + package_uninstall = self.make_boolean(package_uninstall) if not isinstance(package_uninstall, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.package.uninstall must be a boolean. " From 1f81beac3d902ea43143f76fe5cceba2128a5dee Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 07:13:00 -1000 Subject: [PATCH 077/300] Remove duplicate testcase 00004 This testcase didn't used to be a duplicate, but we changed the code and then changed this testcase without realizing that it's now a duplicate of 00005. Renumber all subsequent testcases to make room for one additional testcase (next commit) --- .../test_image_upgrade_ImageUpgrade.py | 56 +++++-------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index e5e72c692..e470d2123 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -134,36 +134,6 @@ def test_image_mgmt_upgrade_00004(monkeypatch, module) -> None: """ Function description: - ImageUpgrade.validate_devices updates the set ImageUpgrade.ip_addresses - with the ip addresses of the devices for which issu_detail.upgrade is - not "Failed" - - Expected results: - - 1. instance.ip_addresses will contain {"172.22.150.102", "172.22.150.108"} - 2. fail_json will not be called - """ - - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_upgrade_00004a" - return responses_issu_details(key) - - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - - devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] - - module.devices = devices - module.validate_devices() - assert isinstance(module.ip_addresses, set) - assert len(module.ip_addresses) == 2 - assert "172.22.150.102" in module.ip_addresses - assert "172.22.150.108" in module.ip_addresses - - -def test_image_mgmt_upgrade_00005(monkeypatch, module) -> None: - """ - Function description: - ImageUpgrade.validate_devices updates the set ImageUpgrade.ip_addresses with the ip addresses of the devices for which validation succeeds. Currently, validation succeeds for all devices. This function may be @@ -190,7 +160,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" in module.ip_addresses -def test_image_mgmt_upgrade_00006(module) -> None: +def test_image_mgmt_upgrade_00005(module) -> None: """ Function: ImageUpgrade.commit @@ -203,7 +173,7 @@ def test_image_mgmt_upgrade_00006(module) -> None: module.commit() -def test_image_mgmt_upgrade_00007(module) -> None: +def test_image_mgmt_upgrade_00006(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -233,7 +203,7 @@ def test_image_mgmt_upgrade_00007(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00008(module) -> None: +def test_image_mgmt_upgrade_00007(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -270,7 +240,7 @@ def test_image_mgmt_upgrade_00008(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00009(module) -> None: +def test_image_mgmt_upgrade_00008(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -307,7 +277,7 @@ def test_image_mgmt_upgrade_00009(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00010(module) -> None: +def test_image_mgmt_upgrade_00009(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -344,7 +314,7 @@ def test_image_mgmt_upgrade_00010(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00011(module) -> None: +def test_image_mgmt_upgrade_00010(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -381,7 +351,7 @@ def test_image_mgmt_upgrade_00011(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00012(module) -> None: +def test_image_mgmt_upgrade_00011(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -418,7 +388,7 @@ def test_image_mgmt_upgrade_00012(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00013(module) -> None: +def test_image_mgmt_upgrade_00012(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -455,7 +425,7 @@ def test_image_mgmt_upgrade_00013(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00014(module) -> None: +def test_image_mgmt_upgrade_00013(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -492,7 +462,7 @@ def test_image_mgmt_upgrade_00014(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00015(module) -> None: +def test_image_mgmt_upgrade_00014(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -529,7 +499,7 @@ def test_image_mgmt_upgrade_00015(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00016(module) -> None: +def test_image_mgmt_upgrade_00015(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -566,7 +536,7 @@ def test_image_mgmt_upgrade_00016(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00017(module) -> None: +def test_image_mgmt_upgrade_00016(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config @@ -603,7 +573,7 @@ def test_image_mgmt_upgrade_00017(module) -> None: assert merged_config["options"]["package"]["uninstall"] == False -def test_image_mgmt_upgrade_00018(module) -> None: +def test_image_mgmt_upgrade_00017(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config From dfe5686fa87440739032e3f9a9f02735fd0fd34f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 08:03:56 -1000 Subject: [PATCH 078/300] ImageUpgrade: verify upgrade.nxos/upgrade.epld, more... test_image_upgrade_ImageUpgrade.py - Add summary for all tests - Add tests for: - upgrade.nxos invalid value (00018) - upgrade.epld invalid value (00033) - Fix 00015 (options.reboot.config_reload) test description --- .../module_utils/image_mgmt/image_upgrade.py | 18 ++ ...upgrade_responses_ImageInstallOptions.json | 55 +++++ .../image_upgrade_responses_ImageUpgrade.json | 7 + ...e_upgrade_responses_SwitchIssuDetails.json | 51 +++++ .../test_image_upgrade_ImageUpgrade.py | 188 ++++++++++++++++-- 5 files changed, 303 insertions(+), 16 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index ba1a37fad..f6a97d245 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -308,6 +308,24 @@ def build_payload(self, device) -> None: msg += f"device POST: {json.dumps(device, indent=4, sort_keys=True)}" self.log_msg(msg) + # device.upgrade + nxos_upgrade = device.get("upgrade").get("nxos") + nxos_upgrade = self.make_boolean(nxos_upgrade) + if not isinstance(nxos_upgrade, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "upgrade.nxos must be a boolean. " + msg += f"Got {nxos_upgrade}." + self.module.fail_json(msg) + + epld_upgrade = device.get("upgrade").get("epld") + epld_upgrade = self.make_boolean(epld_upgrade) + if not isinstance(epld_upgrade, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "upgrade.epld must be a boolean. " + msg += f"Got {epld_upgrade}." + self.module.fail_json(msg) + + # TODO:2 Validate ip_address self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json index d7f3a43f9..2b520541d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json @@ -979,5 +979,60 @@ "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00033a": { + "DATA": { + "compatibilityStatusList": [ + { + "compDisp": "REMOVED", + "deviceName": "leaf1", + "installOption": "disruptive", + "ipAddress": "172.22.150.102", + "osType": "64bit", + "platform": "N9K/N3K", + "policyName": "NR3F", + "preIssuLink": "Not Applicable", + "repStatus": "skipped", + "status": "Success", + "timestamp": "NA", + "version": "10.3.1", + "versionCheck": "REMOVED" + } + ], + "epldModules": { + "bException": false, + "exceptionReason": null, + "moduleList": [ + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "IO FPGA", + "name": null, + "newVersion": "0x15", + "oldVersion": "0x15", + "policyName": "NR3F" + }, + { + "deviceName": "leaf1", + "ipAddress": "172.22.150.102", + "modelName": "N9K-C93180YC-EX", + "module": 1, + "moduleType": "MI FPGA", + "name": null, + "newVersion": "0x04", + "oldVersion": "0x4", + "policyName": "NR3F" + } + ] + }, + "errMessage": "", + "installPacakges": null + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json index 9873ae545..24118a02c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json @@ -90,6 +90,13 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 500 }, + "test_image_mgmt_upgrade_00033a": { + "DATA": 123, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", + "RETURN_CODE": 200 + }, "test_image_mgmt_upgrade_00045a": { "DATA": 121, "MESSAGE": "OK", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 6a22d2ddc..d371ad601 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3792,6 +3792,57 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, + "test_image_mgmt_upgrade_00033a": { + "DATA": { + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "ethswitchid": 165300, + "fabric": "f8", + "fcoEEnabled": false, + "group": "f8", + "id": 1, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "ip_address": "172.22.150.102", + "issuAllowed": "", + "lastUpgAction": "2023-Nov-08 02:11", + "mds": false, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": null, + "platform": "N9K", + "policy": "NR3F", + "reason": "Validate", + "role": "leaf", + "serialNumber": "FDO21120U5D", + "status": "Out-Of-Sync", + "statusPercent": 100, + "sys_name": "leaf1", + "systemMode": "Normal", + "upgGroups": "None", + "upgrade": "None", + "upgradePercent": 0, + "validated": "Success", + "validatedPercent": 100, + "vdcId": 0, + "vdc_id": -1, + "version": "10.2(5)", + "vpcPeer": null, + "vpcRole": null, + "vpc_role": null + } + ], + "message": "", + "status": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "RETURN_CODE": 200 + }, "test_image_mgmt_upgrade_00045a": { "DATA": { "lastOperDataObject": [ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index e470d2123..12ab21e09 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -176,6 +176,7 @@ def test_image_mgmt_upgrade_00005(module) -> None: def test_image_mgmt_upgrade_00006(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: merged_config contains all default values Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -206,6 +207,7 @@ def test_image_mgmt_upgrade_00006(module) -> None: def test_image_mgmt_upgrade_00007(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the upgrade.epld is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -243,6 +245,7 @@ def test_image_mgmt_upgrade_00007(module) -> None: def test_image_mgmt_upgrade_00008(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the upgrade.nxos is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -280,6 +283,7 @@ def test_image_mgmt_upgrade_00008(module) -> None: def test_image_mgmt_upgrade_00009(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.nxos is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -317,6 +321,7 @@ def test_image_mgmt_upgrade_00009(module) -> None: def test_image_mgmt_upgrade_00010(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.nxos.bios_force is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -354,6 +359,7 @@ def test_image_mgmt_upgrade_00010(module) -> None: def test_image_mgmt_upgrade_00011(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.nxos.mode is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -391,6 +397,7 @@ def test_image_mgmt_upgrade_00011(module) -> None: def test_image_mgmt_upgrade_00012(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.epld.golden is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -428,6 +435,7 @@ def test_image_mgmt_upgrade_00012(module) -> None: def test_image_mgmt_upgrade_00013(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.epld.module is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -465,6 +473,7 @@ def test_image_mgmt_upgrade_00013(module) -> None: def test_image_mgmt_upgrade_00014(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.reboot.write_erase is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -502,11 +511,12 @@ def test_image_mgmt_upgrade_00014(module) -> None: def test_image_mgmt_upgrade_00015(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.reboot.config_reload is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.reboot.config_reload. This - forces the code to take the options.reboot.write_erase is None path. + default values missing except options.reboot.write_erase. This + forces the code to take the options.reboot.config_reload is None path. Expected results: @@ -539,6 +549,7 @@ def test_image_mgmt_upgrade_00015(module) -> None: def test_image_mgmt_upgrade_00016(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.package.uninstall is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -576,6 +587,7 @@ def test_image_mgmt_upgrade_00016(module) -> None: def test_image_mgmt_upgrade_00017(module) -> None: """ Function: ImageUpgrade._merge_defaults_to_switch_config + Test: Force code coverage of the options.package.install is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -610,9 +622,77 @@ def test_image_mgmt_upgrade_00017(module) -> None: assert merged_config["options"]["package"]["uninstall"] == True +def test_image_mgmt_upgrade_00018(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + Test: upgrade.nxos set to invalid value + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded. + 2. The methods called by commit are mocked to simulate that the + the image has already been staged and validated and the device + has already been upgraded to the desired version. + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing. + + Expected results: + + 1. commit will call build_payload which will call fail_json + """ + key = "test_image_mgmt_upgrade_00019a" + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) + + module.devices = [ + { + "policy": "KR5M", + "stage": True, + "upgrade": {"nxos": "FOO", "epld": True}, + "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": False, + } + ] + match = r"ImageUpgrade.build_payload: upgrade.nxos must be a boolean. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + def test_image_mgmt_upgrade_00019(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: non-default values are set for several options + Test: policy_changed is set to False + Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -627,7 +707,9 @@ def test_image_mgmt_upgrade_00019(monkeypatch, module) -> None: Expected results: 1. module.payload will equal a payload previously obtained by - running ansible-playbook against the controller for this scenario + running ansible-playbook against the controller for this + scenario which verifies that the non-default values are + included in the payload. """ key = "test_image_mgmt_upgrade_00019a" @@ -679,6 +761,8 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00020(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: User explicitely sets default values for several options + Test: policy_changed is set to True Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -744,6 +828,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Invalid value for nxos.mode Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -752,7 +837,6 @@ def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain an invalid nxos.mode value Expected results: @@ -811,6 +895,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Force code coverage of nxos.mode == "non_disruptive" path Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -819,7 +904,6 @@ def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain nxos.mode non_disruptive forcing the code to take nxos_mode == "non_disruptive" path @@ -880,6 +964,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Force code coverage of nxos.mode == "force_non_disruptive" path Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -888,7 +973,6 @@ def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain nxos.mode force_non_disruptive forcing the code to take nxos_mode == "force_non_disruptive" path @@ -949,6 +1033,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Invalid value for options.nxos.bios_force Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -957,7 +1042,6 @@ def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for options.nxos.bios_force @@ -1016,6 +1100,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00025(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Incompatible values for options.epld.golden and upgrade.nxos Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -1024,7 +1109,6 @@ def test_image_mgmt_upgrade_00025(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain epld golden True and upgrade.nxos True. @@ -1085,6 +1169,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00026(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Invalid value for epld.module Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -1156,6 +1241,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00027(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Invalid value for epld.golden Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -1164,8 +1250,7 @@ def test_image_mgmt_upgrade_00027(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - 4. module.devices is set to contain invalid epld.module + 4. module.devices is set to contain invalid epld.golden Expected results: @@ -1226,6 +1311,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00028(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Invalid value for reboot Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -1234,7 +1320,6 @@ def test_image_mgmt_upgrade_00028(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for reboot Expected results: @@ -1292,6 +1377,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00029(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Invalid value for options.reboot.config_reload Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -1300,7 +1386,6 @@ def test_image_mgmt_upgrade_00029(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for options.reboot.config_reload @@ -1363,6 +1448,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00030(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Invalid value for options.reboot.write_erase Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -1371,9 +1457,8 @@ def test_image_mgmt_upgrade_00030(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for - options.reboot.config_reload + options.reboot.write_erase Expected results: @@ -1434,6 +1519,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Invalid value for options.package.uninstall Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -1442,7 +1528,6 @@ def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for options.package.uninstall @@ -1511,6 +1596,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00032(monkeypatch, module) -> None: """ Function: ImageUpgrade.commit + Test: Bad result code in image upgrade response Setup: 1. ImageUpgrade.devices is set to a list of one dict for a device @@ -1519,7 +1605,6 @@ def test_image_mgmt_upgrade_00032(monkeypatch, module) -> None: device has not yet been upgraded to the desired version 3. Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. ImageUpgrade response (mock_dcnm_send_image_upgrade) is set to return RESULT_CODE 500 with MESSAGE "Internal Server Error" @@ -1581,6 +1666,77 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): module.commit() +def test_image_mgmt_upgrade_00033(monkeypatch, module) -> None: + """ + Function: ImageUpgrade.commit + Test: Invalid value for upgrade.epld + + Setup: + 1. ImageUpgrade.devices is set to a list of one dict for a device + to be upgraded + 2. The methods called by commit are mocked to simulate that the + device has not yet been upgraded to the desired version + 3. Methods called by commit that wait for current actions, and + image upgrade, to complete are mocked to do nothing + 4. module.devices is set to contain invalid value for + upgrade.epld + + Expected results: + + 1. commit calls build_payload which calls fail_json + """ + key = "test_image_mgmt_upgrade_00033a" + + def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + return responses_install_options(key) + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_issu_details(key) + + def mock_wait_for_current_actions_to_complete(*args, **kwargs): + pass + + def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): + pass + + monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr( + module, + "_wait_for_current_actions_to_complete", + mock_wait_for_current_actions_to_complete, + ) + monkeypatch.setattr( + module, + "_wait_for_image_upgrade_to_complete", + mock_wait_for_image_upgrade_to_complete, + ) + + module.devices = [ + { + "policy": "NR3F", + "stage": True, + "upgrade": {"nxos": True, "epld": "FOO"}, + "options": { + "package": { + "uninstall": "FOO", + } + }, + "validate": True, + "ip_address": "172.22.150.102", + "policy_changed": True, + } + ] + match = "ImageUpgrade.build_payload: " + match += r"upgrade.epld must be a boolean. Got FOO\." + with pytest.raises(AnsibleFailJson, match=match): + module.commit() + + # getters From 19bac408e76c95fcf4d1c7afbdacfbab6deb7891 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 08:10:00 -1000 Subject: [PATCH 079/300] upgrade.epld - remove redundant assignment/check --- plugins/module_utils/image_mgmt/image_upgrade.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index f6a97d245..ec3505201 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -398,7 +398,6 @@ def build_payload(self, device) -> None: # EPLD epld_module = device.get("options").get("epld").get("module") epld_golden = device.get("options").get("epld").get("golden") - epld_upgrade = device.get("upgrade").get("epld") epld_golden = self.make_boolean(epld_golden) if not isinstance(epld_golden, bool): @@ -407,13 +406,6 @@ def build_payload(self, device) -> None: msg += f"Got {epld_golden}." self.module.fail_json(msg) - epld_upgrade = self.make_boolean(epld_upgrade) - if not isinstance(epld_upgrade, bool): - msg = f"{self.class_name}.{method_name}: " - msg += "upgrade.epld must be a boolean. " - msg += f"Got {epld_upgrade}." - self.module.fail_json(msg) - if epld_golden is True and device.get("upgrade").get("nxos") is True: msg = f"{self.class_name}.{method_name}: " msg += "Invalid configuration for " From 17f22cb2ec581ff12714a6ac57ae133ef2fda05b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 09:38:46 -1000 Subject: [PATCH 080/300] Add unit tests, more... - ImageUpgrade._wait_for_current_actions_to_complete: Add unit tests 00080 and 00081 - ImageUpgrade._wait_for_current_actions_to_complete: self.ipv4_todo should be a set() - ImageUpgrade._wait_for_current_actions_to_complete: ipv4_todo/ipv4_done should be sorted for unit tests - Run class and test .py through black/isort - ImageUpgrade.check_interval - add setter for unit test - ImageUpgrade.check_timeout - add setter for unit test --- .../module_utils/image_mgmt/image_upgrade.py | 46 +++-- ...e_upgrade_responses_SwitchIssuDetails.json | 189 ++++++++++++++++++ .../test_image_upgrade_ImageUpgrade.py | 91 +++++++++ 3 files changed, 308 insertions(+), 18 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index ec3505201..53f21c947 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -337,7 +337,9 @@ def build_payload(self, device) -> None: self.install_options.policy_name = device.get("policy") self.install_options.epld = device.get("upgrade").get("epld") self.install_options.nxos = device.get("upgrade").get("nxos") - self.install_options.package_install = device.get("options").get("package").get("install") + self.install_options.package_install = ( + device.get("options").get("package").get("install") + ) self.install_options.refresh() msg = f"REMOVE: {self.class_name}.{method_name}: " @@ -424,13 +426,11 @@ def build_payload(self, device) -> None: msg += f"or an integer. Got {epld_module}." self.module.fail_json(msg) - self.payload["epldUpgrade"] = epld_upgrade self.payload["epldOptions"] = {} self.payload["epldOptions"]["moduleNumber"] = epld_module self.payload["epldOptions"]["golden"] = epld_golden - # Reboot reboot = device.get("reboot") @@ -556,8 +556,8 @@ def _wait_for_current_actions_to_complete(self): """ method_name = inspect.stack()[0][3] - self.ipv4_todo = copy.copy(self.ip_addresses) self.ipv4_done = set() + self.ipv4_todo = set(copy.copy(self.ip_addresses)) timeout = self.check_timeout while self.ipv4_done != self.ipv4_todo and timeout > 0: @@ -575,21 +575,14 @@ def _wait_for_current_actions_to_complete(self): self.ipv4_done.add(ipv4) continue - msg = f"REMOVE: {self.class_name}.{method_name}: " - msg += f"Seconds remaining {timeout}. Waiting " - msg += f" on ipv4 {ipv4} actions to complete. " - msg += f"staged_percent {self.issu_detail.image_staged_percent}, " - msg += f"validated_percent {self.issu_detail.validated_percent}, " - msg += f"upgrade_percent {self.issu_detail.upgrade_percent}" - self.log_msg(msg) - if self.ipv4_done != self.ipv4_todo: msg = f"{self.class_name}.{method_name}: " - msg += "Timed out while waiting for actions in progress " - msg += "to complete for the following device(s): " - msg += f"{self.ipv4_todo}. " - msg += "Try increasing issu timeout in the playbook, or check " - msg += "the device(s) to determine the cause " + msg += f"Timed out waiting for actions to complete. " + msg += f"ipv4_done: " + msg += f"{','.join(sorted(self.ipv4_done))}, " + msg += f"ipv4_todo: " + msg += f"{','.join(sorted(self.ipv4_todo))}. " + msg += "check the device(s) to determine the cause " msg += "(e.g. show install all status)." self.module.fail_json(msg) @@ -911,7 +904,6 @@ def write_erase(self, value): self.module.fail_json(msg) self.properties["write_erase"] = value - # getter properties @property def check_interval(self): """ @@ -919,6 +911,14 @@ def check_interval(self): """ return self.properties.get("check_interval") + @check_interval.setter + def check_interval(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_interval must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_interval"] = value + @property def check_timeout(self): """ @@ -926,6 +926,16 @@ def check_timeout(self): """ return self.properties.get("check_timeout") + @check_timeout.setter + def check_timeout(self, value): + if not isinstance(value, int): + msg = f"{self.__class__.__name__}: instance.check_timeout must " + msg += f"be an integer." + self.module.fail_json(msg) + self.properties["check_timeout"] = value + + # getter properties + @property def response_data(self): """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index d371ad601..309aad488 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3995,5 +3995,194 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 + }, + "test_image_mgmt_upgrade_00080a": { + "TEST_NOTES": [ + "172.22.150.102 validated, upgrade, imageStaged: Success", + "172.22.150.108 validated, upgrade, imageStaged: Success" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_mgmt_upgrade_00081a": { + "TEST_NOTES": [ + "172.22.150.102 upgrade: Success", + "172.22.150.108 upgrade: In-Progress", + "172.22.150.108 upgradePercent: 50" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "In-Progress", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 50, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 12ab21e09..95a356181 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -10,6 +10,8 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ ImageUpgrade +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsByIpAddress from .fixture import load_fixture @@ -68,6 +70,11 @@ def module(): return ImageUpgrade(MockAnsibleModule) +@pytest.fixture +def mock_issu_details() -> SwitchIssuDetailsByIpAddress: + return SwitchIssuDetailsByIpAddress(MockAnsibleModule) + + def test_image_mgmt_upgrade_00001(module) -> None: """ ImageUpgrade.__init__ initializes class attributes to expected values @@ -2266,3 +2273,87 @@ def test_image_mgmt_upgrade_00072(module, value, expected) -> None: else: module.write_erase = value assert module.write_erase == expected + + +def test_image_mgmt_upgrade_00080(monkeypatch, module, mock_issu_details) -> None: + """ + Function: ImageUpgrade._wait_for_current_actions_to_complete + Test: Verify that two switches are added to ipv4_done + + _wait_for_current_actions_to_complete waits until staging, validation, + and upgrade actions are complete for all ip addresses. It calls + SwitchIssuDetailsByIpAddress.actions_in_progress() and expects + this to return False. actions_in_progress() returns True until none of + the following keys has a value of "In-Progress": + + ["imageStaged", "upgrade", "validated"] + + Expectations: + 1. module.ipv4_done should be a set() + 2. module.ipv4_done should be length 2 + 3. module.ipv4_done should contain all ip addresses in + module.ip_addresses + 4. The function should return without calling fail_json. + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_upgrade_00080a" + return responses_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.issu_detail = mock_issu_details + module.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + module.check_interval = 0 + module._wait_for_current_actions_to_complete() + assert isinstance(module.ipv4_done, set) + assert len(module.ipv4_done) == 2 + assert "172.22.150.102" in module.ipv4_done + assert "172.22.150.108" in module.ipv4_done + + +def test_image_mgmt_upgrade_00081(monkeypatch, module, mock_issu_details) -> None: + """ + Function: ImageUpgrade._wait_for_current_actions_to_complete + Test: Verify that one switch is added to ipv4_done + Test: Verify that fail_json is called due to timeout + + See test_image_mgmt_upgrade_00080 for functional details. + + Expectations: + 1. module.ipv4_done should be a set() + 2. module.ipv4_done should be length 1 + 3. module.ipv4_done should contain 172.22.150.102 + 3. module.ipv4_done should not contain 172.22.150.108 + 4. The function should call fail_json due to timeout + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_upgrade_00081a" + return responses_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.issu_detail = mock_issu_details + module.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + module.check_interval = 1 + module.check_timeout = 1 + + match = "ImageUpgrade._wait_for_current_actions_to_complete: " + match += "Timed out waiting for actions to complete. " + match += r"ipv4_done: 172\.22\.150\.102, " + match += r"ipv4_todo: 172\.22\.150\.102,172\.22\.150\.108\. " + match += r"check the device\(s\) to determine the cause " + match += r"\(e\.g\. show install all status\)\." + with pytest.raises(AnsibleFailJson, match=match): + module._wait_for_current_actions_to_complete() + assert isinstance(module.ipv4_done, set) + assert len(module.ipv4_done) == 1 + assert "172.22.150.102" in module.ipv4_done + assert "172.22.150.108" not in module.ipv4_done From 4826d913c7a8f9ef73069b153c5a00b34dd9de09 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 10:49:28 -1000 Subject: [PATCH 081/300] Add unit test for _wait_for_image_upgrade_to_complete Add unit test test_image_mgmt_upgrade_00090 --- ...e_upgrade_responses_SwitchIssuDetails.json | 95 +++++++++++++++++++ .../test_image_upgrade_ImageUpgrade.py | 46 +++++++++ 2 files changed, 141 insertions(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 309aad488..07d1b9407 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -4184,5 +4184,100 @@ ], "message": "" } + }, + "test_image_mgmt_upgrade_00090a": { + "TEST_NOTES": [ + "172.22.150.102 upgrade: Success", + "172.22.150.108 upgrade: Failed", + "172.22.150.108 upgradePercent: 50" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Failed", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 50, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 95a356181..a2f6d8e54 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -2357,3 +2357,49 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert len(module.ipv4_done) == 1 assert "172.22.150.102" in module.ipv4_done assert "172.22.150.108" not in module.ipv4_done + + +def test_image_mgmt_upgrade_00090(monkeypatch, module, mock_issu_details) -> None: + """ + Function: ImageUpgrade._wait_for_image_upgrade_to_complete + Test: One ip address is added to ipv4_done due to + issu_detail.upgrade == "Success" + Test: fail_json is called due one ip address with + issu_detail.upgrade == "Failed" + + _wait_for_image_upgrade_to_complete looks at the upgrade status for each + ip address and waits for it to be "Success" or "Failed". + In the case where all ip addresses are "Success", the module returns. + In the case where any ip address is "Failed", the module calls fail_json. + + Expectations: + 1. module.ipv4_done is a set() + 2. module.ipv4_done has length 1 + 3. module.ipv4_done contains 172.22.150.102, upgrade is "Success" + 4. Call fail_json on ip address 172.22.150.108, upgrade is "Failed" + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_upgrade_00090a" + return responses_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.issu_detail = mock_issu_details + module.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + module.check_interval = 0 + match = "ImageUpgrade._wait_for_image_upgrade_to_complete: " + match += "Seconds remaining 1800: " + match += "upgrade image Failed for cvd-2313-leaf, FDO2112189M, " + match += r"172\.22\.150\.108, upgrade_percent 50\. " + match += "Check the controller to determine the cause. " + match += "Operations > Image Management > Devices > View Details." + with pytest.raises(AnsibleFailJson, match=match): + module._wait_for_image_upgrade_to_complete() + assert isinstance(module.ipv4_done, set) + assert len(module.ipv4_done) == 1 + assert "172.22.150.102" in module.ipv4_done + assert "172.22.150.108" not in module.ipv4_done From 99b1d2e4e3d3572c43bc8eb3cd9758c08eacca28 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 11:07:18 -1000 Subject: [PATCH 082/300] Add unit test for _wait_for_image_upgrade_to_complete Add unit test test_image_mgmt_upgrade_00091 --- ...e_upgrade_responses_SwitchIssuDetails.json | 95 +++++++++++++++++++ .../test_image_upgrade_ImageUpgrade.py | 49 ++++++++++ 2 files changed, 144 insertions(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 07d1b9407..0601fb04e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -4279,5 +4279,100 @@ ], "message": "" } + }, + "test_image_mgmt_upgrade_00091a": { + "TEST_NOTES": [ + "172.22.150.102 upgrade: Success", + "172.22.150.108 upgrade: In-Progress", + "172.22.150.108 upgradePercent: 50" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "In-Progress", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 50, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index a2f6d8e54..89c9fa625 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -2403,3 +2403,52 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert len(module.ipv4_done) == 1 assert "172.22.150.102" in module.ipv4_done assert "172.22.150.108" not in module.ipv4_done + + +def test_image_mgmt_upgrade_00091(monkeypatch, module, mock_issu_details) -> None: + """ + Function: ImageUpgrade._wait_for_image_upgrade_to_complete + Test: One ip address is added to ipv4_done due to + issu_detail.upgrade == "Success" + Test: fail_json is called due to timeout because one + ip address has issu_detail.upgrade == "In-Progress" + + _wait_for_image_upgrade_to_complete looks at the upgrade status for each + ip address and waits for it to be "Success" or "Failed". + In the case where all ip addresses are "Success", the module returns. + In the case where any ip address is "Failed", the module calls fail_json. + In the case where any ip address is "In-Progress", the module waits until + timeout is exceeded + + Expectations: + 1. module.ipv4_done is a set() + 2. module.ipv4_done has length 1 + 3. module.ipv4_done contains 172.22.150.102, upgrade is "Success" + 4. Call fail_json due to timeout exceeded + """ + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_upgrade_00091a" + return responses_issu_details(key) + + monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + + module.issu_detail = mock_issu_details + module.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + module.check_interval = 1 + module.check_timeout = 1 + + match = "ImageUpgrade._wait_for_image_upgrade_to_complete: " + match += r"The following device\(s\) did not complete upgrade: " + match += r"\['172\.22\.150\.108'\]. " + match += r"Check the device\(s\) to determine the cause " + match += r"\(e\.g\. show install all status\)\." + with pytest.raises(AnsibleFailJson, match=match): + module._wait_for_image_upgrade_to_complete() + assert isinstance(module.ipv4_done, set) + assert len(module.ipv4_done) == 1 + assert "172.22.150.102" in module.ipv4_done + assert "172.22.150.108" not in module.ipv4_done From a5bc6990780277b1426bdff57adb73a49657d254 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 11:21:15 -1000 Subject: [PATCH 083/300] Sort set difference for unit tests, more... Also remove unused var status. This was used in log_msg() previously but these log_msg() have since been removed. --- plugins/module_utils/image_mgmt/image_upgrade.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 53f21c947..fbbe6faa1 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -624,18 +624,12 @@ def _wait_for_image_upgrade_to_complete(self): if upgrade_status == "Success": self.ipv4_done.add(ipv4) - status = "succeeded" - if upgrade_status == None: - status = "not started" - if upgrade_status == "In-Progress": - status = "in progress" if self.ipv4_done != self.ipv4_todo: msg = f"{self.class_name}.{method_name}: " msg += "The following device(s) did not complete upgrade: " - msg += f"{self.ipv4_todo.difference(self.ipv4_done)}. " - msg += "Try increasing issu timeout in the playbook, or check " - msg += "the device(s) to determine the cause " + msg += f"{sorted(self.ipv4_todo.difference(self.ipv4_done))}. " + msg += "Check the device(s) to determine the cause " msg += "(e.g. show install all status)." self.module.fail_json(msg) From afe991c0bd848cc248fc5d5499789fcb764f449d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 13:52:16 -1000 Subject: [PATCH 084/300] _get() run return value thru make_boolean() --- .../module_utils/image_mgmt/switch_issu_details.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 673fab081..e0ed5fe45 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -676,7 +676,9 @@ def _get(self, item): msg += f"{self.ip_address} unknown property name: {item}." self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.ip_address].get(item)) + return self.make_none( + self.make_boolean(self.data_subclass[self.ip_address].get(item)) + ) @property def filtered_data(self): @@ -763,7 +765,9 @@ def _get(self, item): msg += f"{self.serial_number} unknown property name: {item}." self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.serial_number].get(item)) + return self.make_none( + self.make_boolean(self.data_subclass[self.serial_number].get(item)) + ) @property def filtered_data(self): @@ -848,7 +852,9 @@ def _get(self, item): msg += f"{self.device_name} unknown property name: {item}." self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.device_name].get(item)) + return self.make_none( + self.make_boolean(self.data_subclass[self.device_name].get(item)) + ) @property def filtered_data(self): From 2615dd2c557b30c2107dd8b1604cec183c8b8806 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 15:30:56 -1000 Subject: [PATCH 085/300] test_image_upgrade_ImagePolicyAction: Standardize test names, more... Rename mock_ image_policy_action to image_policy_action Rename mock_issu_details to issu_details Rename mock_image_policies to image_policies Rename match, if if is defined globally, to match_ Add more consistent docstrings --- ...image_upgrade_responses_ImagePolicies.json | 30 +- ...e_upgrade_responses_ImagePolicyAction.json | 28 +- ...image_upgrade_responses_SwitchDetails.json | 16 +- ...e_upgrade_responses_SwitchIssuDetails.json | 64 +++- .../test_image_upgrade_ImagePolicyAction.py | 332 +++++++++--------- 5 files changed, 269 insertions(+), 201 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json index 98865e96f..ec765e2de 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json @@ -94,7 +94,7 @@ "message": "" } }, - "ImagePolicyAction_test_validate_request_1": { + "test_image_mgmt_image_policy_action_00013a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", @@ -120,7 +120,33 @@ "message": "" } }, - "ImagePolicyAction_test_commit_all": { + "test_image_mgmt_image_policy_action_00020a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "test_image_mgmt_image_policy_action_00021a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json index 5891db54a..c2f6d6da6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json @@ -120,33 +120,7 @@ "message": "" } }, - "ImagePolicyAction_test_commit_all": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "policyName": "KR5M", - "policyType": "PLATFORM", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "policyDescr": "10.2.(5) with EPLD", - "platformPolicies": "", - "epldImgName": "n9000-epld.10.2.5.M.img", - "rpmimages": "", - "imageName": "nxos64-cs.10.2.5.M.bin", - "agnostic": "false", - "ref_count": 10 - } - ], - "message": "" - } - }, - "ImagePolicyAction_detach_policy_200": { + "test_image_mgmt_image_policy_action_00021a": { "RETURN_CODE": 200, "METHOD": "DELETE", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy?serialNumber=FDO21120U5D,FDO211218GC,FOX2109PGD0", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json index 474a3337d..eb8225236 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json @@ -235,13 +235,6 @@ } ] }, - "SwitchDetails_get_return_code_404": { - "RETURN_CODE": 404, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitchess", - "MESSAGE": "Not Found", - "DATA": {} - }, "SwitchDetails_get_return_code_500": { "RETURN_CODE": 500, "METHOD": "GET", @@ -249,7 +242,14 @@ "MESSAGE": "Internal Server Error", "DATA": {} }, - "ImagePolicyAction_test_commit_all": { + "SwitchDetails_get_return_code_404": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitchess", + "MESSAGE": "Not Found", + "DATA": {} + }, + "test_image_mgmt_image_policy_action_00021a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 0601fb04e..3c9f59216 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -2530,7 +2530,7 @@ "message": "" } }, - "ImagePolicyAction_test_build_attach_payload": { + "test_image_mgmt_image_policy_action_00003a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2737,7 +2737,10 @@ "message": "" } }, - "ImagePolicyAction_test_build_attach_payload_fail_json": { + "test_image_mgmt_image_policy_action_00004a": { + "TEST_NOTES": [ + "deviceName: null" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2747,7 +2750,7 @@ "lastOperDataObject": [ { "serialNumber": "FDO2112189M", - "deviceName": "none", + "deviceName": "null", "version": "10.2(5)", "policy": "KR5M", "status": "In-Sync", @@ -2788,7 +2791,7 @@ "message": "" } }, - "ImagePolicyAction_test_validate_request_1": { + "test_image_mgmt_image_policy_action_00013a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2839,7 +2842,58 @@ "message": "" } }, - "ImagePolicyAction_test_commit_all": { + "test_image_mgmt_image_policy_action_00020a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_mgmt_image_policy_action_00021a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index bf1af6d8e..e731faf12 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -67,71 +67,68 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture -def mock_image_policy_action(): +def image_policy_action(): return ImagePolicyAction(MockAnsibleModule) @pytest.fixture -def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: +def issu_details() -> SwitchIssuDetailsBySerialNumber: return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) @pytest.fixture -def mock_image_policies() -> ImagePolicies: +def image_policies() -> ImagePolicies: return ImagePolicies(MockAnsibleModule) -# test_init - - -def test_init(mock_image_policy_action) -> None: - mock_image_policy_action.__init__(MockAnsibleModule) - assert isinstance(mock_image_policy_action, ImagePolicyAction) - assert isinstance(mock_image_policy_action.switch_issu_details, SwitchIssuDetailsBySerialNumber) - assert mock_image_policy_action.valid_actions == {"attach", "detach", "query"} - - -# test_init_properties - - -def test_init_properties(mock_image_policy_action) -> None: +def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: """ - Properties are initialized to expected values + Function: __init__ + Test: All class attributes initialized to expected values """ - mock_image_policy_action._init_properties() - assert isinstance(mock_image_policy_action.properties, dict) - assert mock_image_policy_action.properties.get("action") == None - assert mock_image_policy_action.properties.get("response") == None - assert mock_image_policy_action.properties.get("result") == None - assert mock_image_policy_action.properties.get("policy_name") == None - assert mock_image_policy_action.properties.get("query_result") == None - assert mock_image_policy_action.properties.get("serial_numbers") == None + image_policy_action.__init__(MockAnsibleModule) + assert isinstance(image_policy_action, ImagePolicyAction) + assert isinstance(image_policy_action.switch_issu_details, SwitchIssuDetailsBySerialNumber) + assert image_policy_action.valid_actions == {"attach", "detach", "query"} -# test_build_attach_payload +def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: + """ + Function: _init_properties + Test: All class properties initialized to expected values + """ + image_policy_action._init_properties() + assert isinstance(image_policy_action.properties, dict) + assert image_policy_action.properties.get("action") == None + assert image_policy_action.properties.get("response") == None + assert image_policy_action.properties.get("result") == None + assert image_policy_action.properties.get("policy_name") == None + assert image_policy_action.properties.get("query_result") == None + assert image_policy_action.properties.get("serial_numbers") == None -def test_build_attach_payload(monkeypatch, mock_image_policy_action, mock_issu_details) -> None: +def test_image_mgmt_image_policy_action_00003(monkeypatch, image_policy_action, issu_details) -> None: """ - build_attach_payload builds the payload to send in the POST request - to attach policies to devices + Function: + build_payload - Expectations: - 1. mock_image_policy_action.payload should be a list() + Test: + - fail_json is not called + - image_policy_action.payloads is a list + - image_policy_action.payloads has length 5 - Expected results: - 1. mock_image_policy_action.fail_json should not be called - 2. mock_image_policy_action.payloads should be a list() - 3. mock_image_policy_action.payloads should have length 5 + Description: + build_payload builds the payload to send in the POST request + to attach policies to devices """ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_build_attach_payload" + key = "test_image_mgmt_image_policy_action_00003a" return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) - mock_image_policy_action.switch_issu_details = mock_issu_details - mock_image_policy_action.policy_name = "KR5M" - mock_image_policy_action.serial_numbers = [ + image_policy_action.switch_issu_details = issu_details + image_policy_action.policy_name = "KR5M" + image_policy_action.serial_numbers = [ "FDO2112189M", "FDO211218AX", "FDO211218B5", @@ -139,170 +136,189 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: "FDO211218GC", ] with does_not_raise(): - mock_image_policy_action.build_attach_payload() - assert isinstance(mock_image_policy_action.payloads, list) - assert len(mock_image_policy_action.payloads) == 5 + image_policy_action.build_payload() + assert isinstance(image_policy_action.payloads, list) + assert len(image_policy_action.payloads) == 5 -# test_build_attach_payload_fail_json +def test_image_mgmt_image_policy_action_00004(monkeypatch, image_policy_action, issu_details) -> None: + """ + Function: + build_payload + Test: + - fail_json is called since deviceName is null in the issu_details + response + - The error message is matched -def test_build_attach_payload_fail_json(monkeypatch, mock_image_policy_action, mock_issu_details) -> None: - """ - build_attach_payload builds the payload to send in the POST request + Description: + build_payload builds the payload to send in the POST request to attach policies to devices. If any key in the payload has a value of None, the function calls fail_json. - - Expected results: - 1. fail_json should be called because deviceName is None in the issu_details response """ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_build_attach_payload_fail_json" + key = "test_image_mgmt_image_policy_action_00004a" return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) - mock_image_policy_action.switch_issu_details = mock_issu_details - mock_image_policy_action.policy_name = "KR5M" - mock_image_policy_action.serial_numbers = [ + image_policy_action.switch_issu_details = issu_details + image_policy_action.policy_name = "KR5M" + image_policy_action.serial_numbers = [ "FDO2112189M", ] - error_message = "Unable to determine hostName for switch " - error_message += "172.22.150.108, FDO2112189M, None. " - error_message += "Please verify that the switch is managed by " - error_message += "the controller." - with pytest.raises(AnsibleFailJson, match=error_message): - mock_image_policy_action.build_attach_payload() + match = "Unable to determine hostName for switch " + match += "172.22.150.108, FDO2112189M, None. " + match += "Please verify that the switch is managed by " + match += "the controller." + with pytest.raises(AnsibleFailJson, match=match): + image_policy_action.build_payload() -# test_validate_request_policy_name_none +def test_image_mgmt_image_policy_action_00010(image_policy_action, issu_details) -> None: + """ + Function: + validate_request + Test: + - fail_json is called because image_policy_action.action is None + - The error message is matched -def test_validate_request_action_none(mock_image_policy_action, mock_issu_details) -> None: - """ + Description: validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a validation-specific error message. - - Expected results: - 1. fail_json should be called because action is None """ - mock_image_policy_action.switch_issu_details = mock_issu_details - mock_image_policy_action.policy_name = "KR5M" - mock_image_policy_action.serial_numbers = [ + image_policy_action.switch_issu_details = issu_details + image_policy_action.policy_name = "KR5M" + image_policy_action.serial_numbers = [ "FDO2112189M", ] match = "ImagePolicyAction.validate_request: " match += "instance.action must be set before calling commit()" with pytest.raises(AnsibleFailJson, match=match): - mock_image_policy_action.validate_request() + image_policy_action.validate_request() -# test_validate_request_policy_name_none +# test_image_mgmt_image_policy_action_00011 -match = "ImagePolicyAction.validate_request: " -match += "instance.policy_name must be set before calling commit()" +match_00011 = "ImagePolicyAction.validate_request: " +match_00011 += "instance.policy_name must be set before calling commit()" @pytest.mark.parametrize( "action,expected", [ - ("attach", pytest.raises(AnsibleFailJson, match=match)), - ("detach", pytest.raises(AnsibleFailJson, match=match)), - ("query", pytest.raises(AnsibleFailJson, match=match)), + ("attach", pytest.raises(AnsibleFailJson, match=match_00011)), + ("detach", pytest.raises(AnsibleFailJson, match=match_00011)), + ("query", pytest.raises(AnsibleFailJson, match=match_00011)), ], ) -def test_validate_request_policy_name_none( - action, expected, mock_image_policy_action, mock_issu_details +def test_image_mgmt_image_policy_action_00011( + action, expected, image_policy_action, issu_details ) -> None: """ + Function: + validate_request + + Test: + - fail_json is called because image_policy_action.policy_name is None + - The error message is matched + + Description: validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a validation-specific error message. - - Expected results: - 1. fail_json should be called because policy_name is None """ - mock_image_policy_action.switch_issu_details = mock_issu_details - mock_image_policy_action.action = action - mock_image_policy_action.serial_numbers = [ + image_policy_action.switch_issu_details = issu_details + image_policy_action.action = action + image_policy_action.serial_numbers = [ "FDO2112189M", ] with expected: - mock_image_policy_action.validate_request() + image_policy_action.validate_request() -# test_validate_request_serial_numbers_none -match = "ImagePolicyAction.validate_request: " -match += "instance.serial_numbers must be set before calling commit()" +match_00012 = "ImagePolicyAction.validate_request: " +match_00012 += "instance.serial_numbers must be set before calling commit()" @pytest.mark.parametrize( "action,expected", [ - ("attach", pytest.raises(AnsibleFailJson, match=match)), - ("detach", pytest.raises(AnsibleFailJson, match=match)), + ("attach", pytest.raises(AnsibleFailJson, match=match_00012)), + ("detach", pytest.raises(AnsibleFailJson, match=match_00012)), ("query", does_not_raise()), ], ) -def test_validate_request_serial_numbers_none( - action, expected, mock_image_policy_action, mock_issu_details +def test_image_mgmt_image_policy_action_00012( + action, expected, image_policy_action, issu_details ) -> None: """ + Function: + validate_request + + Test: + - fail_json is called for action == attach because + image_policy_action.serial_numbers is None + - fail_json is called for action == detach because + image_policy_action.serial_numbers is None + - fail_json is NOT called for action == query because + validate_request is exited early for action == "query" + - The error message, if any, is matched + + Description: validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a validation-specific error message. - - Expected results: - 1. action == attach fail_json should be called - 2. action == detach fail_json should be called - 3. action == query fail_json should NOT be called """ - mock_image_policy_action.switch_issu_details = mock_issu_details - mock_image_policy_action.action = action - mock_image_policy_action.policy_name = "KR5M" + image_policy_action.switch_issu_details = issu_details + image_policy_action.action = action + image_policy_action.policy_name = "KR5M" with expected: - mock_image_policy_action.validate_request() - + image_policy_action.validate_request() -# test_validate_request_image_policy_does_not_support_switch_platform -def test_validate_request_image_policy_does_not_support_switch_platform(monkeypatch, mock_image_policy_action, mock_issu_details, mock_image_policies +def test_image_mgmt_image_policy_action_00013(monkeypatch, image_policy_action, issu_details, image_policies ) -> None: """ + Function: + validate_request + + Test: + - fail_json is called because policy KR5M supports playform N9K/N3K + and the response from ImagePolicies contains platform + TEST_UNKNOWN_PLATFORM + - The error message is matched + + Description: + validate_request performs a number of validations prior to calling commit validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a validation-specific error message. - - Expected results: - 1. fail_json should be called since the policy KR5M - supports "N9K switch platform and the response from ImagePolicies - contains platform == "TEST_UNKNOWN_PLATFORM" """ - + key = "test_image_mgmt_image_policy_action_00013a" def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_validate_request_1" return responses_switch_issu_details(key) def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_validate_request_1" return responses_image_policies(key) monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - mock_image_policy_action.switch_issu_details = mock_issu_details - mock_image_policy_action.image_policies = mock_image_policies - mock_image_policy_action.action = "attach" - mock_image_policy_action.policy_name = "KR5M" - mock_image_policy_action.serial_numbers = ["FDO2112189M"] + image_policy_action.switch_issu_details = issu_details + image_policy_action.image_policies = image_policies + image_policy_action.action = "attach" + image_policy_action.policy_name = "KR5M" + image_policy_action.serial_numbers = ["FDO2112189M"] match = "ImagePolicyAction.validate_request: " @@ -310,15 +326,19 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: match += r"KR5M supports the following platform\(s\): N9K/N3K" with pytest.raises(AnsibleFailJson, match=match): - mock_image_policy_action.validate_request() - + image_policy_action.validate_request() -# test_commit_unknown_action -def test_commit_action_unknown(monkeypatch, mock_image_policy_action) -> None: +def test_image_mgmt_image_policy_action_00020(monkeypatch, image_policy_action) -> None: """ - Verify that fail_json is called if action is unknown. + Function: + commit + Test: + - fail_json is called because action is unknown + - The error message is matched + + Description: commit calls validate_request() and then calls one of the following functions based on the value of action: action == "attach" : _attach_policy @@ -333,69 +353,64 @@ def test_commit_action_unknown(monkeypatch, mock_image_policy_action) -> None: Since action == "FOO" is not covered in commit()'s if clauses, the else clause is taken and fail_json is called. - - Expected results: - 1. action is unknown, so module.fail_json is called """ + key = "test_image_mgmt_image_policy_action_00020a" def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_commit_all" return responses_switch_issu_details(key) def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_commit_all" return responses_image_policies(key) + def mock_validate_request(*args, **kwargs) -> None: pass monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - monkeypatch.setattr(mock_image_policy_action, "validate_request", mock_validate_request) - monkeypatch.setattr(mock_image_policy_action, "valid_actions", {"attach", "detach", "query", "FOO"}) + monkeypatch.setattr(image_policy_action, "validate_request", mock_validate_request) + monkeypatch.setattr(image_policy_action, "valid_actions", {"attach", "detach", "query", "FOO"}) - mock_image_policy_action.policy_name = "KR5M" - mock_image_policy_action.serial_numbers = ["FDO2112189M"] - mock_image_policy_action.action = "FOO" + image_policy_action.policy_name = "KR5M" + image_policy_action.serial_numbers = ["FDO2112189M"] + image_policy_action.action = "FOO" match = "ImagePolicyAction.commit: Unknown action FOO." with pytest.raises(AnsibleFailJson, match=match): - mock_image_policy_action.commit() + image_policy_action.commit() -def test_commit_action_detach_success(monkeypatch, mock_image_policy_action) -> None: +def test_image_mgmt_image_policy_action_00021(monkeypatch, image_policy_action) -> None: """ - Verify that commit is successful for action == "detach" given a - 200 response from the controller in ImagePolicyAction._detach_policy + Function: + commit + + Test: + - action is "detach", so ImagePolicyAction._detach_policy is called + - commit is successful given a 200 response from the controller in + ImagePolicyAction._detach_policy + - ImagePolicyAction.response contains RESULT_CODE 200 + Description: commit calls validate_request() and then calls one of the following functions based on the value of action: action == "attach" : _attach_policy action == "detach" : _detach_policy action == "query" : _query_policy - - Expected results: - 1. action is "detach", so ImagePolicyAction_detach_policy is called - 2. ImagePolicyAction.response contains RESULT_CODE 200 - 3. commit is successful """ - + key = "test_image_mgmt_image_policy_action_00021a" def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_commit_all" return responses_image_policies(key) def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_commit_all" return responses_switch_details(key) def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_test_commit_all" return responses_switch_issu_details(key) def mock_dcnm_send_image_policy_action(*args, **kwargs) -> Dict[str, Any]: - key = "ImagePolicyAction_detach_policy_200" return responses_image_policy_action(key) monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) @@ -403,17 +418,16 @@ def mock_dcnm_send_image_policy_action(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) - mock_image_policy_action.policy_name = "KR5M" - mock_image_policy_action.serial_numbers = ["FDO2112189M"] - # mock_image_policy_action.serial_numbers = ["FDO21120U5D","FDO211218GC","FOX2109PGD0"] - mock_image_policy_action.action = "detach" - - mock_image_policy_action.commit() - assert isinstance(mock_image_policy_action.response, dict) - assert mock_image_policy_action.response.get("RETURN_CODE") == 200 - assert mock_image_policy_action.response.get("METHOD") == "DELETE" - assert mock_image_policy_action.response.get("MESSAGE") == "OK" - assert mock_image_policy_action.response.get("DATA") == "Successfully detach the policy from device." - assert mock_image_policy_action.result.get("success") == True - assert mock_image_policy_action.result.get("changed") == True + image_policy_action.policy_name = "KR5M" + image_policy_action.serial_numbers = ["FDO2112189M"] + image_policy_action.action = "detach" + + image_policy_action.commit() + assert isinstance(image_policy_action.response, dict) + assert image_policy_action.response.get("RETURN_CODE") == 200 + assert image_policy_action.response.get("METHOD") == "DELETE" + assert image_policy_action.response.get("MESSAGE") == "OK" + assert image_policy_action.response.get("DATA") == "Successfully detach the policy from device." + assert image_policy_action.result.get("success") == True + assert image_policy_action.result.get("changed") == True From edcf4753af1fd5956aba74b2d1b9f3555d2b4634 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 15:31:27 -1000 Subject: [PATCH 086/300] image_policy_action: rename build_attach_payload to build_payload --- .../module_utils/image_mgmt/image_policy_action.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index d42a4b4fb..8ec90b08d 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -57,7 +57,7 @@ def _init_properties(self): self.properties["query_result"] = None self.properties["serial_numbers"] = None - def build_attach_payload(self): + def build_payload(self): """ build the payload to send in the POST request to attach policies to devices @@ -156,7 +156,7 @@ def _attach_policy(self): """ self.method_name = inspect.stack()[0][3] - self.build_attach_payload() + self.build_payload() self.path = self.endpoints.policy_attach.get("path") self.verb = self.endpoints.policy_attach.get("verb") @@ -168,6 +168,7 @@ def _attach_policy(self): response = dcnm_send(self.module, self.verb, self.path, data=json.dumps(payload)) result = self._handle_response(response, self.verb) + self.log_msg(f"{self.class_name}.{self.method_name}: response: {json.dumps(response, indent=4)}") if not result["success"]: msg = f"{self.class_name}.{self.method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " @@ -198,6 +199,8 @@ def _detach_policy(self): self.properties["response"] = dcnm_send(self.module, self.verb, self.path) self.properties["result"] = self._handle_response(self.response, self.verb) + self.log_msg(f"{self.class_name}.{self.method_name}: response: {json.dumps(self.response, indent=4)}") + if not self.result["success"]: self._failure(self.response) @@ -232,7 +235,9 @@ def query_result(self): @property def action(self): """ - Set the action to take. Either "attach" or "detach". + Set the action to take. + + One of "attach", "detach", "query" Must be set prior to calling instance.commit() """ From 5257fbc57f5ca0cdf6e9f4b38634cb221e310663 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 15:35:23 -1000 Subject: [PATCH 087/300] Run thru black/isort --- .../image_mgmt/image_policy_action.py | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 8ec90b08d..53b5a167f 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -1,12 +1,16 @@ import inspect import json -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import SwitchIssuDetailsBySerialNumber + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ + ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send class ImagePolicyAction(ImageUpgradeCommon): @@ -165,10 +169,15 @@ def _attach_policy(self): results = [] for payload in self.payloads: - response = dcnm_send(self.module, self.verb, self.path, data=json.dumps(payload)) + response = dcnm_send( + self.module, self.verb, self.path, data=json.dumps(payload) + ) result = self._handle_response(response, self.verb) - self.log_msg(f"{self.class_name}.{self.method_name}: response: {json.dumps(response, indent=4)}") + msg = f"{self.class_name}.{self.method_name}: " + msg += f"response: {json.dumps(response, indent=4)}" + self.log_msg(msg) + if not result["success"]: msg = f"{self.class_name}.{self.method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " @@ -196,10 +205,12 @@ def _detach_policy(self): query_params = ",".join(self.serial_numbers) self.path += f"?serialNumber={query_params}" - self.properties["response"] = dcnm_send(self.module, self.verb, self.path) - self.properties["result"] = self._handle_response(self.response, self.verb) + self.properties["response"] = dcnm_send(self.module, self.verb, self.path) + self.properties["result"] = self._handle_response(self.response, self.verb) - self.log_msg(f"{self.class_name}.{self.method_name}: response: {json.dumps(self.response, indent=4)}") + msg = f"{self.class_name}.{self.method_name}: " + msg += f"response: {json.dumps(self.response, indent=4)}" + self.log_msg(msg) if not self.result["success"]: self._failure(self.response) @@ -236,7 +247,7 @@ def query_result(self): def action(self): """ Set the action to take. - + One of "attach", "detach", "query" Must be set prior to calling instance.commit() From fea0a937b77c63facf5c5fa8ab9f92325073fd04 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 15:41:45 -1000 Subject: [PATCH 088/300] Don't hardcode method names --- plugins/module_utils/image_mgmt/image_policy_action.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 53b5a167f..d2b8766be 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -221,7 +221,7 @@ def _query_policy(self): verb: GET endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ """ - self.method_name = "_query_policy" + self.method_name = inspect.stack()[0][3] self.path = self.endpoints.policy_info.get("path") self.verb = self.endpoints.policy_info.get("verb") @@ -256,7 +256,7 @@ def action(self): @action.setter def action(self, value): - self.method_name = "action.setter" + self.method_name = inspect.stack()[0][3] if value not in self.valid_actions: msg = f"{self.class_name}.{self.method_name}: " @@ -299,6 +299,7 @@ def policy_name(self): @policy_name.setter def policy_name(self, value): + self.method_name = inspect.stack()[0][3] self.properties["policy_name"] = value @property @@ -313,7 +314,7 @@ def serial_numbers(self): @serial_numbers.setter def serial_numbers(self, value): - self.method_name = "serial_numbers.setter" + self.method_name = inspect.stack()[0][3] if not isinstance(value, list): msg = f"{self.class_name}.{self.method_name}: " msg += "instance.serial_numbers must be a " From 9aa6897c46600fda9e3baa101d56c87a4eae6cef Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 16:04:52 -1000 Subject: [PATCH 089/300] Provide user's value in fail_json message --- plugins/module_utils/image_mgmt/image_policy_action.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index d2b8766be..3a2bcf474 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -261,7 +261,8 @@ def action(self, value): if value not in self.valid_actions: msg = f"{self.class_name}.{self.method_name}: " msg += "instance.action must be one of " - msg += f"{','.join(sorted(self.valid_actions))}" + msg += f"{','.join(sorted(self.valid_actions))}. " + msg += f"Got {value}." self.module.fail_json(msg) self.properties["action"] = value @@ -318,6 +319,7 @@ def serial_numbers(self, value): if not isinstance(value, list): msg = f"{self.class_name}.{self.method_name}: " msg += "instance.serial_numbers must be a " - msg += f"python list of switch serial numbers." + msg += f"python list of switch serial numbers. " + msg += f"Got {value}." self.module.fail_json(msg) self.properties["serial_numbers"] = value From b0e30eab3dc37235af24a0e2099d39cc85d1e82e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 Nov 2023 16:07:54 -1000 Subject: [PATCH 090/300] Add unit tests, run thru black/isort, more... Add unit tests for setters: - action - serial_numbers --- .../test_image_upgrade_ImagePolicyAction.py | 118 +++++++++++++++--- 1 file changed, 99 insertions(+), 19 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index e731faf12..4523292c3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -9,10 +9,10 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ - ImagePolicyAction from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ + ImagePolicyAction from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ SwitchDetails from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ @@ -34,24 +34,28 @@ def does_not_raise(): dcnm_send_switch_details = patch_image_mgmt + "switch_details.dcnm_send" dcnm_send_switch_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" + def responses_image_policies(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_ImagePolicies" response = load_fixture(response_file).get(key) print(f"responses_image_policies: {key} : {response}") return response + def responses_image_policy_action(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_ImagePolicyAction" response = load_fixture(response_file).get(key) print(f"responses_image_policy_action: {key} : {response}") return response + def responses_switch_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchDetails" response = load_fixture(response_file).get(key) print(f"responses_switch_details: {key} : {response}") return response + def responses_switch_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -70,10 +74,12 @@ def fail_json(msg) -> AnsibleFailJson: def image_policy_action(): return ImagePolicyAction(MockAnsibleModule) + @pytest.fixture def issu_details() -> SwitchIssuDetailsBySerialNumber: return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) + @pytest.fixture def image_policies() -> ImagePolicies: return ImagePolicies(MockAnsibleModule) @@ -86,7 +92,9 @@ def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: """ image_policy_action.__init__(MockAnsibleModule) assert isinstance(image_policy_action, ImagePolicyAction) - assert isinstance(image_policy_action.switch_issu_details, SwitchIssuDetailsBySerialNumber) + assert isinstance( + image_policy_action.switch_issu_details, SwitchIssuDetailsBySerialNumber + ) assert image_policy_action.valid_actions == {"attach", "detach", "query"} @@ -105,7 +113,9 @@ def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: assert image_policy_action.properties.get("serial_numbers") == None -def test_image_mgmt_image_policy_action_00003(monkeypatch, image_policy_action, issu_details) -> None: +def test_image_mgmt_image_policy_action_00003( + monkeypatch, image_policy_action, issu_details +) -> None: """ Function: build_payload @@ -124,7 +134,9 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_image_policy_action_00003a" return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr( + dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + ) image_policy_action.switch_issu_details = issu_details image_policy_action.policy_name = "KR5M" @@ -141,7 +153,9 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: assert len(image_policy_action.payloads) == 5 -def test_image_mgmt_image_policy_action_00004(monkeypatch, image_policy_action, issu_details) -> None: +def test_image_mgmt_image_policy_action_00004( + monkeypatch, image_policy_action, issu_details +) -> None: """ Function: build_payload @@ -161,7 +175,9 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_image_policy_action_00004a" return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr( + dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + ) image_policy_action.switch_issu_details = issu_details image_policy_action.policy_name = "KR5M" @@ -176,7 +192,9 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: image_policy_action.build_payload() -def test_image_mgmt_image_policy_action_00010(image_policy_action, issu_details) -> None: +def test_image_mgmt_image_policy_action_00010( + image_policy_action, issu_details +) -> None: """ Function: validate_request @@ -202,8 +220,6 @@ def test_image_mgmt_image_policy_action_00010(image_policy_action, issu_details) image_policy_action.validate_request() -# test_image_mgmt_image_policy_action_00011 - match_00011 = "ImagePolicyAction.validate_request: " match_00011 += "instance.policy_name must be set before calling commit()" @@ -243,7 +259,6 @@ def test_image_mgmt_image_policy_action_00011( image_policy_action.validate_request() - match_00012 = "ImagePolicyAction.validate_request: " match_00012 += "instance.serial_numbers must be set before calling commit()" @@ -286,7 +301,8 @@ def test_image_mgmt_image_policy_action_00012( image_policy_action.validate_request() -def test_image_mgmt_image_policy_action_00013(monkeypatch, image_policy_action, issu_details, image_policies +def test_image_mgmt_image_policy_action_00013( + monkeypatch, image_policy_action, issu_details, image_policies ) -> None: """ Function: @@ -305,13 +321,16 @@ def test_image_mgmt_image_policy_action_00013(monkeypatch, image_policy_action, validation-specific error message. """ key = "test_image_mgmt_image_policy_action_00013a" + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: return responses_image_policies(key) - monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr( + dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + ) monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) image_policy_action.switch_issu_details = issu_details @@ -320,7 +339,6 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: image_policy_action.policy_name = "KR5M" image_policy_action.serial_numbers = ["FDO2112189M"] - match = "ImagePolicyAction.validate_request: " match += "policy KR5M does not support platform TEST_UNKNOWN_PLATFORM. " match += r"KR5M supports the following platform\(s\): N9K/N3K" @@ -365,11 +383,15 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: def mock_validate_request(*args, **kwargs) -> None: pass - monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr( + dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + ) monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) monkeypatch.setattr(image_policy_action, "validate_request", mock_validate_request) - monkeypatch.setattr(image_policy_action, "valid_actions", {"attach", "detach", "query", "FOO"}) + monkeypatch.setattr( + image_policy_action, "valid_actions", {"attach", "detach", "query", "FOO"} + ) image_policy_action.policy_name = "KR5M" image_policy_action.serial_numbers = ["FDO2112189M"] @@ -414,9 +436,13 @@ def mock_dcnm_send_image_policy_action(*args, **kwargs) -> Dict[str, Any]: return responses_image_policy_action(key) monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - monkeypatch.setattr(dcnm_send_image_policy_action, mock_dcnm_send_image_policy_action) + monkeypatch.setattr( + dcnm_send_image_policy_action, mock_dcnm_send_image_policy_action + ) monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) - monkeypatch.setattr(dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr( + dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + ) image_policy_action.policy_name = "KR5M" image_policy_action.serial_numbers = ["FDO2112189M"] @@ -427,7 +453,61 @@ def mock_dcnm_send_image_policy_action(*args, **kwargs) -> Dict[str, Any]: assert image_policy_action.response.get("RETURN_CODE") == 200 assert image_policy_action.response.get("METHOD") == "DELETE" assert image_policy_action.response.get("MESSAGE") == "OK" - assert image_policy_action.response.get("DATA") == "Successfully detach the policy from device." + assert ( + image_policy_action.response.get("DATA") + == "Successfully detach the policy from device." + ) assert image_policy_action.result.get("success") == True assert image_policy_action.result.get("changed") == True + +match_00060 = "ImagePolicyAction.action: instance.action must be " +match_00060 += "one of attach,detach,query. Got FOO." + + +@pytest.mark.parametrize( + "value, expected", + [ + ("attach", "attach"), + ("detach", "detach"), + ("query", "query"), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00060)), + ], +) +def test_image_mgmt_image_policy_action_00060( + image_policy_action, value, expected +) -> None: + """ + ImagePolicyAction.action setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00060): + image_policy_action.action = value + else: + image_policy_action.action = value + assert image_policy_action.action == expected + + +match_00061 = "ImagePolicyAction.serial_numbers: instance.serial_numbers " +match_00061 += "must be a python list of switch serial numbers. Got FOO." + + +@pytest.mark.parametrize( + "value, expected", + [ + (["FDO2112189M", "FDO21120U5D"], ["FDO2112189M", "FDO21120U5D"]), + ("FOO", pytest.raises(AnsibleFailJson, match=match_00061)), + ], +) +def test_image_mgmt_image_policy_action_00061( + image_policy_action, value, expected +) -> None: + """ + ImagePolicyAction.serial_numbers setter + """ + if value == "FOO": + with pytest.raises(AnsibleFailJson, match=match_00061): + image_policy_action.serial_numbers = value + else: + image_policy_action.serial_numbers = value + assert image_policy_action.serial_numbers == expected From 3b73d7ce3ac457c87e1ccab5bb2407f5897729e8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 10 Nov 2023 09:47:54 -1000 Subject: [PATCH 091/300] Standardize test names --- ...image_upgrade_responses_ImagePolicies.json | 107 ++++++- ...e_upgrade_responses_ImagePolicyAction.json | 123 +-------- .../test_image_upgrade_ImagePolicies.py | 260 +++++++++++------- 3 files changed, 262 insertions(+), 228 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json index ec765e2de..16624a302 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json @@ -1,5 +1,5 @@ { - "policymgnt_policies_get_return_code_200": { + "test_image_mgmt_image_policies_00010a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", @@ -39,14 +39,75 @@ "message": "" } }, - "policymgnt_policies_get_return_code_200_empty_DATA": { + "test_image_mgmt_image_policies_00020a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + }, + { + "policyName": "OR1F", + "policyType": "PLATFORM", + "nxosVersion": "10.4.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "OR1F EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.4.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.4.1.F.bin", + "agnostic": "false", + "ref_count": 0 + } + ], + "message": "" + } + }, + "test_image_mgmt_image_policies_00021a": { + "TEST_NOTES": [ + "404 RETURN_CODE" + ], + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/bad/path", + "MESSAGE": "Not Found", + "DATA": { + "timestamp": "2023-10-22T21:01:17.375+00:00", + "status": 404, + "error": "Not Found", + "path": "/rest/policymgnt/policiess" + } + }, + "test_image_mgmt_image_policies_00022a": { + "TEST_NOTES": [ + "DATA field is empty" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", "MESSAGE": "OK", "DATA": {} }, - "policymgnt_policies_get_return_code_200_controller_has_no_defined_image_policies": { + "test_image_mgmt_image_policies_00023a": { + "TEST_NOTES": [ + "DATA has no defined image policies" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", @@ -57,19 +118,41 @@ "message": "" } }, - "policymgnt_policies_get_return_code_404": { - "RETURN_CODE": 404, + "test_image_mgmt_image_policies_00024a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "policyName FOO is missing in response" + ], + "RETURN_CODE": 200, "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policiess", - "MESSAGE": "Not Found", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", "DATA": { - "timestamp": "2023-10-22T21:01:17.375+00:00", - "status": 404, - "error": "Not Found", - "path": "/rest/policymgnt/policiess" + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" } }, - "policymgnt_policies_get_return_code_200_policyName_missing_in_response": { + "test_image_mgmt_image_policies_00025a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "policyName key is missing" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json index c2f6d6da6..bf47cc21a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json @@ -1,130 +1,9 @@ { - "policymgnt_policies_get_return_code_200": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "policyName": "KR5M", - "policyType": "PLATFORM", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "policyDescr": "10.2.(5) with EPLD", - "platformPolicies": "", - "epldImgName": "n9000-epld.10.2.5.M.img", - "rpmimages": "", - "imageName": "nxos64-cs.10.2.5.M.bin", - "agnostic": "false", - "ref_count": 10 - }, - { - "policyName": "OR1F", - "policyType": "PLATFORM", - "nxosVersion": "10.4.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "policyDescr": "OR1F EPLD", - "platformPolicies": "", - "epldImgName": "n9000-epld.10.4.1.F.img", - "rpmimages": "", - "imageName": "nxos64-cs.10.4.1.F.bin", - "agnostic": "false", - "ref_count": 0 - } - ], - "message": "" - } - }, - "policymgnt_policies_get_return_code_200_empty_DATA": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", - "MESSAGE": "OK", - "DATA": {} - }, - "policymgnt_policies_get_return_code_200_controller_has_no_defined_image_policies": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [], - "message": "" - } - }, - "policymgnt_policies_get_return_code_404": { - "RETURN_CODE": 404, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policiess", - "MESSAGE": "Not Found", - "DATA": { - "timestamp": "2023-10-22T21:01:17.375+00:00", - "status": 404, - "error": "Not Found", - "path": "/rest/policymgnt/policiess" - } - }, - "policymgnt_policies_get_return_code_200_policyName_missing_in_response": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "policyType": "PLATFORM", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "policyDescr": "10.2.(5) with EPLD", - "platformPolicies": "", - "epldImgName": "n9000-epld.10.2.5.M.img", - "rpmimages": "", - "imageName": "nxos64-cs.10.2.5.M.bin", - "agnostic": "false", - "ref_count": 10 - } - ], - "message": "" - } - }, - "ImagePolicyAction_test_validate_request_1": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "policyName": "KR5M", - "policyType": "PLATFORM", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "policyDescr": "10.2.(5) with EPLD", - "platformPolicies": "", - "epldImgName": "n9000-epld.10.2.5.M.img", - "rpmimages": "", - "imageName": "nxos64-cs.10.2.5.M.bin", - "agnostic": "false", - "ref_count": 10 - } - ], - "message": "" - } - }, "test_image_mgmt_image_policy_action_00021a": { "RETURN_CODE": 200, "METHOD": "DELETE", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy?serialNumber=FDO21120U5D,FDO211218GC,FOX2109PGD0", "MESSAGE": "OK", "DATA": "Successfully detach the policy from device." - } + } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py index 0eb17c4a2..27be5f673 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py @@ -6,6 +6,8 @@ from typing import Any, Dict import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ @@ -34,58 +36,87 @@ def responses_image_policies(key: str) -> Dict[str, str]: @pytest.fixture -def module(): +def image_policies(): return ImagePolicies(MockAnsibleModule) -def test_init_properties(module) -> None: +def test_image_mgmt_image_policies_00001(image_policies) -> None: """ - Properties are initialized to None + Function + - __init__ + + Test + - Class attributes are initialized to expected values """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("policy_name") == None - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None + image_policies.__init__(MockAnsibleModule) + assert image_policies.module == MockAnsibleModule + assert image_policies.class_name == "ImagePolicies" + assert isinstance(image_policies.endpoints, ApiEndpoints) +def test_image_mgmt_image_policies_00002(image_policies) -> None: + """ + Function + - _init_properties -def test_refresh_return_code_200(monkeypatch, module) -> None: + Test + - Class properties are initialized to expected values """ - Properties are initialized based on 200 response from endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policies + image_policies._init_properties() + assert isinstance(image_policies.properties, dict) + assert image_policies.properties.get("policy_name") == None + assert image_policies.properties.get("response_data") == None + assert image_policies.properties.get("response") == None + assert image_policies.properties.get("result") == None + + +def test_image_mgmt_image_policies_00010(monkeypatch, image_policies) -> None: + """ + Function + - refresh + + Test + - properties are initialized to expected values + - 200 RETURN_CODE + + Endpoint + - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - key = "policymgnt_policies_get_return_code_200" + key = "test_image_mgmt_image_policies_00010a" def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") return responses_image_policies(key) monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - module.refresh() - module.policy_name = "KR5M" - assert isinstance(module.response, dict) - assert module.agnostic == False - assert module.description == "10.2.(5) with EPLD" - assert module.epld_image_name == "n9000-epld.10.2.5.M.img" - assert module.image_name == "nxos64-cs.10.2.5.M.bin" - assert module.nxos_version == "10.2.5_nxos64-cs_64bit" - assert module.package_name == None - assert module.platform == "N9K/N3K" - assert module.platform_policies == None - assert module.policy_name == "KR5M" - assert module.policy_type == "PLATFORM" - assert module.ref_count == 10 - assert module.rpm_images == None + image_policies.refresh() + image_policies.policy_name = "KR5M" + assert isinstance(image_policies.response, dict) + assert image_policies.agnostic == False + assert image_policies.description == "10.2.(5) with EPLD" + assert image_policies.epld_image_name == "n9000-epld.10.2.5.M.img" + assert image_policies.image_name == "nxos64-cs.10.2.5.M.bin" + assert image_policies.nxos_version == "10.2.5_nxos64-cs_64bit" + assert image_policies.package_name == None + assert image_policies.platform == "N9K/N3K" + assert image_policies.platform_policies == None + assert image_policies.policy_name == "KR5M" + assert image_policies.policy_type == "PLATFORM" + assert image_policies.ref_count == 10 + assert image_policies.rpm_images == None + + +def test_image_mgmt_image_policies_00020(monkeypatch, image_policies) -> None: + """ + Function + - refresh + Test + - result contains expected key/values on 200 response from endpoint. -def test_result_return_code_200(monkeypatch, module) -> None: + Endpoint + - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - result contains expected key/values on 200 response from endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policies - """ - key = "policymgnt_policies_get_return_code_200" + key = "test_image_mgmt_image_policies_00020a" def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -93,18 +124,24 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - module.refresh() - assert isinstance(module.result, dict) - assert module.result.get("found") == True - assert module.result.get("success") == True + image_policies.refresh() + assert isinstance(image_policies.result, dict) + assert image_policies.result.get("found") == True + assert image_policies.result.get("success") == True -def test_result_return_code_404(monkeypatch, module) -> None: +def test_image_mgmt_image_policies_00021(monkeypatch, image_policies) -> None: """ - fail_json is called on 404 response from malformed endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on 404 RETURN_CODE in response. + + Endpoint + - /bad/path """ - key = "policymgnt_policies_get_return_code_404" + key = "test_image_mgmt_image_policies_00021a" def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -112,18 +149,24 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - error_message = "ImagePolicies.refresh: Bad response when retrieving " - error_message += "image policy information from the controller." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "ImagePolicies.refresh: Bad response when retrieving " + match += "image policy information from the controller." + with pytest.raises(AnsibleFailJson, match=match): + image_policies.refresh() -def test_result_return_code_200_empty_data(monkeypatch, module) -> None: +def test_image_mgmt_image_policies_00022(monkeypatch, image_policies) -> None: """ - fail_json is called on 200 response with empty DATA key. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on 200 RETURN_CODE with empty DATA key. + + Endpoint + - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - key = "policymgnt_policies_get_return_code_200_empty_DATA" + key = "test_image_mgmt_image_policies_00022a" def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -131,21 +174,27 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - error_message = "ImagePolicies.refresh: Bad response when retrieving " - error_message += "image policy information from the controller." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "ImagePolicies.refresh: Bad response when retrieving " + match += "image policy information from the controller." + with pytest.raises(AnsibleFailJson, match=match): + image_policies.refresh() -def test_result_return_code_200_controller_has_no_defined_image_policies( - monkeypatch, module +def test_image_mgmt_image_policies_00023( + monkeypatch, image_policies ) -> None: """ - fail_json is called on 200 response with DATA.lastOperDataObject length 0. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called when DATA.lastOperDataObject length == 0 + - 200 response + + Endpoint + - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - key = "policymgnt_policies_get_return_code_200" - key += "_controller_has_no_defined_image_policies" + key = "test_image_mgmt_image_policies_00023a" def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -153,18 +202,25 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - error_message = "ImagePolicies.refresh: " - error_message += "the controller has no defined image policies." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "ImagePolicies.refresh: " + match += "the controller has no defined image policies." + with pytest.raises(AnsibleFailJson, match=match): + image_policies.refresh() -def test_policy_name_not_found(monkeypatch, module) -> None: +def test_image_mgmt_image_policies_00024(monkeypatch, image_policies) -> None: """ - fail_json() is called if response does not contain policy_name. - i.e. image policy with name FOO has not yet been created on NDFC. + Function + - refresh + + Test + - fail_json() is called if response does not contain policy_name. + - i.e. image policy with name FOO has not yet been created on NDFC. + + Endpoint + - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - key = "policymgnt_policies_get_return_code_200" + key = "test_image_mgmt_image_policies_00024a" def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -172,36 +228,38 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - module.refresh() - module.policy_name = "FOO" - error_message = "ImagePolicies._get: " - error_message += "policy_name FOO is not defined on the controller." - with pytest.raises(AnsibleFailJson, match=error_message): - module.policy_type == "PLATFORM" + image_policies.refresh() + image_policies.policy_name = "FOO" + match = "ImagePolicies._get: " + match += "policy_name FOO is not defined on the controller." + with pytest.raises(AnsibleFailJson, match=match): + image_policies.policy_type == "PLATFORM" -def test_get_with_policy_name_None(module) -> None: - """ - fail_json is called when _get() is called prior to setting policy_name. - """ - error_message = "ImagePolicies._get: instance.policy_name must be " - error_message += "set before accessing property imageName." - with pytest.raises(AnsibleFailJson, match=error_message): - module._get("imageName") -def test_result_return_code_200_policy_name_missing_in_response( - monkeypatch, module +def test_image_mgmt_image_policies_00025( + monkeypatch, image_policies ) -> None: """ - fail_json is called on 200 response with missing policyName key. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on response with missing policyName key. + - 200 RETURN_CODE - NOTE: This is to cover a check in ImagePolicies.refresh() for a scenario that should never happen. - TODO: Consider removing this check, and this testcase. + Endpoint + - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies + + NOTES + - This is to cover a check in ImagePolicies.refresh() + - This scenario should never happen. + + TODO + - Consider removing this check, and this testcase. """ - key = "policymgnt_policies_get_return_code_200" - key += "_policyName_missing_in_response" + key = "test_image_mgmt_image_policies_00025a" def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -209,7 +267,21 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - error_message = "ImagePolicies.refresh: " - error_message += "Cannot parse policy information from the controller." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "ImagePolicies.refresh: " + match += "Cannot parse policy information from the controller." + with pytest.raises(AnsibleFailJson, match=match): + image_policies.refresh() + +def test_image_mgmt_image_policies_00040(image_policies) -> None: + """ + Function + - _get + + Test + - fail_json is called when _get() is called prior to setting policy_name. + """ + match = "ImagePolicies._get: instance.policy_name must be " + match += "set before accessing property imageName." + with pytest.raises(AnsibleFailJson, match=match): + image_policies._get("imageName") + From 370a737ea7e711bedfa073793ea053b9ba77a979 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 10 Nov 2023 09:52:33 -1000 Subject: [PATCH 092/300] Run thru black/isort --- .../module_utils/image_mgmt/image_policies.py | 17 ++++++++--------- .../test_image_upgrade_ImagePolicies.py | 18 +++++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 2b0887d01..9b4e5fa5b 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -1,10 +1,12 @@ import inspect -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send + class ImagePolicies(ImageUpgradeCommon): """ @@ -108,9 +110,7 @@ def _get(self, item): self.module.fail_json(msg) return self.make_boolean( - self.make_none( - self.properties["response_data"][self.policy_name][item] - ) + self.make_none(self.properties["response_data"][self.policy_name][item]) ) @property @@ -256,4 +256,3 @@ def agnostic(self): Return None otherwise """ return self._get("agnostic") - diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py index 27be5f673..a8fa5e0e4 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py @@ -6,10 +6,10 @@ from typing import Any, Dict import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ - ApiEndpoints from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ ImagePolicies @@ -53,6 +53,7 @@ def test_image_mgmt_image_policies_00001(image_policies) -> None: assert image_policies.class_name == "ImagePolicies" assert isinstance(image_policies.endpoints, ApiEndpoints) + def test_image_mgmt_image_policies_00002(image_policies) -> None: """ Function @@ -180,9 +181,7 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: image_policies.refresh() -def test_image_mgmt_image_policies_00023( - monkeypatch, image_policies -) -> None: +def test_image_mgmt_image_policies_00023(monkeypatch, image_policies) -> None: """ Function - refresh @@ -213,7 +212,7 @@ def test_image_mgmt_image_policies_00024(monkeypatch, image_policies) -> None: Function - refresh - Test + Test - fail_json() is called if response does not contain policy_name. - i.e. image policy with name FOO has not yet been created on NDFC. @@ -237,10 +236,7 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: image_policies.policy_type == "PLATFORM" - -def test_image_mgmt_image_policies_00025( - monkeypatch, image_policies -) -> None: +def test_image_mgmt_image_policies_00025(monkeypatch, image_policies) -> None: """ Function - refresh @@ -272,6 +268,7 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: with pytest.raises(AnsibleFailJson, match=match): image_policies.refresh() + def test_image_mgmt_image_policies_00040(image_policies) -> None: """ Function @@ -284,4 +281,3 @@ def test_image_mgmt_image_policies_00040(image_policies) -> None: match += "set before accessing property imageName." with pytest.raises(AnsibleFailJson, match=match): image_policies._get("imageName") - From 3872d84bd1738182608dc8e1ab6318ddf8946313 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 10 Nov 2023 13:42:34 -1000 Subject: [PATCH 093/300] Standardize test names, more... image_upgrade_responsees_SwitchDetails.json - Remove key/values which are not used in tests. --- ...image_upgrade_responses_SwitchDetails.json | 658 +++--------------- .../test_image_upgrade_SwitchDetails.py | 286 ++++---- 2 files changed, 241 insertions(+), 703 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json index eb8225236..212dc7fb5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json @@ -1,602 +1,134 @@ { - "SwitchDetails_get_return_code_200": { + "test_image_mgmt_switch_details_00020a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "DATA.ipAddress" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [ + { + "ipAddress": "172.22.150.110" + } + ] + }, + "test_image_mgmt_switch_details_00021a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "DATA: key/values removed which are not needed by the test" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "MESSAGE": "OK", "DATA": [ { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, "ipAddress": "172.22.150.110", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1111-bgw", - "switchDbID": 158560, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "15 days, 03:37:10", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FOX2109PGCT", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", - "hostName": "cvd-1111-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1400, - "swUUID": "DCNM-UUID-1400", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Minor", - "intentedpeerName": "" + "hostName": "cvd-1111-bgw" }, { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.111", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, "fabricName": "easy", - "modelType": 0, + "hostName": "cvd-1112-bgw", + "ipAddress": "172.22.150.111", "logicalName": "cvd-1112-bgw", - "switchDbID": 160170, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "15 days, 03:36:55", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", + "model": "N9K-C9504", "serialNumber": "FOX2109PGD1", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", - "hostName": "cvd-1112-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1200, - "swUUID": "DCNM-UUID-1200", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Healthy", - "intentedpeerName": "" + "switchRole": "border gateway" } ] }, - "SwitchDetails_get_return_code_500": { - "RETURN_CODE": 500, + "test_image_mgmt_switch_details_00022a": { + "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", - "MESSAGE": "Internal Server Error", - "DATA": {} + "MESSAGE": "OK", + "DATA": [ + { + "ipAddress": "172.22.150.110" + } + ] }, - "SwitchDetails_get_return_code_404": { + "test_image_mgmt_switch_details_00022b": { "RETURN_CODE": 404, "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitchess", + "REQUEST_PATH": "https://172.22.150.244:443/bad/path", "MESSAGE": "Not Found", "DATA": {} }, - "test_image_mgmt_image_policy_action_00021a": { + "test_image_mgmt_switch_details_00022c": { + "RETURN_CODE": 500, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "Internal Server Error", + "DATA": {} + }, + "test_image_mgmt_switch_details_00023a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "DATA: key/values removed which are not needed by the test" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "MESSAGE": "OK", "DATA": [ { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.110", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1111-bgw", - "switchDbID": 158560, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "15 days, 03:37:10", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO211218GC", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", "hostName": "cvd-1111-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1400, - "swUUID": "DCNM-UUID-1400", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.111", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", + "ipAddress": "172.22.150.110", "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1112-bgw", - "switchDbID": 160170, - "uid": 0, - "release": "10.2(5)", "location": "null", - "contact": "null", - "upTimeStr": "15 days, 03:36:55", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO21120U5D", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", - "hostName": "cvd-1112-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1200, - "swUUID": "DCNM-UUID-1200", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Healthy", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, + "logicalName": "cvd-1111-bgw", + "managable": "true", "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.113", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1111-bgw", - "switchDbID": 160170, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "15 days, 03:36:55", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FOX2109PGD0", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", - "hostName": "cvd-1112-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1200, - "swUUID": "DCNM-UUID-1200", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Healthy", - "intentedpeerName": "" + "serialNumber": "FOX2109PGCT", + "switchRole": "border gateway" + } + ] + }, + "test_image_mgmt_switch_details_00024a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "DATA: Contains ipAddress which is not known" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [ + { + "ipAddress": "5.5.5.5" + } + ] + }, + "test_image_mgmt_switch_details_00025a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "DATA: Does not contain a key named FOO", + "DATA.ipAddress is needed for the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [ + { + "ipAddress": "172.22.150.110" } - ] + ] + }, + "test_image_mgmt_image_policy_action_00021a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [{}] } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py index 7afe8db09..9e47434eb 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py @@ -40,116 +40,91 @@ def fail_json(msg) -> AnsibleFailJson: @pytest.fixture -def module(): +def switch_details(): return SwitchDetails(MockAnsibleModule) -def test_init(module) -> None: - module.__init__(MockAnsibleModule) - assert isinstance(module, SwitchDetails) - assert module.class_name == "SwitchDetails" - - -def test_init_properties(module) -> None: +def test_image_mgmt_switch_details_00001(switch_details) -> None: """ - Properties are initialized to expected values - """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("ip_address") == None - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None - + Function + - __init__ -# test_ip_address - - -@pytest.mark.parametrize( - "ip_address_is_set, expected", - [ - (True, "1.2.3.4"), - (False, None), - ], -) -def test_ip_address(module, ip_address_is_set, expected) -> None: + Test + - Class attributes are initialized to expected values """ - Function description: + switch_details.__init__(MockAnsibleModule) + assert isinstance(switch_details, SwitchDetails) + assert switch_details.class_name == "SwitchDetails" - SwitchDetails.ip_address returns: - - IP Address, if the user has set ip_address - - None, if the user has not already set ip_address - Expected results: - - 1. instance.ip_address will return the value set by the user - 2. instance.ip_address will return None +def test_image_mgmt_switch_details_00002(switch_details) -> None: """ - if ip_address_is_set: - module.ip_address = "1.2.3.4" - assert module.ip_address == expected + Function + - _init_properties - -def test_refresh(monkeypatch, module) -> None: + Test + - Class properties are initialized to expected values """ - Function description: + switch_details._init_properties() + assert isinstance(switch_details.properties, dict) + assert switch_details.properties.get("ip_address") == None + assert switch_details.properties.get("response_data") == None + assert switch_details.properties.get("response") == None + assert switch_details.properties.get("result") == None - SwitchDetails.refresh sets the following properties: - - response_data - - response - - result - Expected results: +def test_image_mgmt_switch_details_00020(monkeypatch, switch_details) -> None: + """ + Function + - refresh - 1. instance.response_data is a dictionary - 2. instance.response is a dictionary - 3. instance.response_data is a dictionary + Test + - response_data, response, result are dictionaries """ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "SwitchDetails_get_return_code_200" + key = "test_image_mgmt_switch_details_00020a" return responses_switch_details(key) monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) - module.refresh() - assert isinstance(module.response_data, dict) - assert isinstance(module.result, dict) - assert isinstance(module.response, dict) + switch_details.refresh() + assert isinstance(switch_details.response_data, dict) + assert isinstance(switch_details.result, dict) + assert isinstance(switch_details.response, dict) -def test_refresh_response_data(monkeypatch, module) -> None: +def test_image_mgmt_switch_details_00021(monkeypatch, switch_details) -> None: """ - Function description: + Function + - refresh - See test_refresh - - Expected results: - - 1. instance.response_data is a dictionary - 2. When instance.ip_address is set, getter properties will return values specific to ip_address + Test + - response_data is a dictionary + - ip_address is set + - getter properties will return values specific to ip_address """ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "SwitchDetails_get_return_code_200" + key = "test_image_mgmt_switch_details_00021a" return responses_switch_details(key) monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) - module.refresh() - assert isinstance(module.response_data, dict) - module.ip_address = "172.22.150.110" - assert module.hostname == "cvd-1111-bgw" - module.ip_address = "172.22.150.111" + switch_details.refresh() + assert isinstance(switch_details.response_data, dict) + switch_details.ip_address = "172.22.150.110" + assert switch_details.hostname == "cvd-1111-bgw" + switch_details.ip_address = "172.22.150.111" # We use the above IP address to test the remaining properties - assert module.fabric_name == "easy" - assert module.hostname == "cvd-1112-bgw" - assert module.logical_name == "cvd-1112-bgw" - assert module.model == "N9K-C9504" - # This is derived from "model" and is not in the NDFC response - assert module.platform == "N9K" - assert module.role == "border gateway" - assert module.serial_number == "FOX2109PGD1" + assert switch_details.fabric_name == "easy" + assert switch_details.hostname == "cvd-1112-bgw" + assert switch_details.logical_name == "cvd-1112-bgw" + assert switch_details.model == "N9K-C9504" + # This is derived from "model" and is not in the controller response + assert switch_details.platform == "N9K" + assert switch_details.role == "border gateway" + assert switch_details.serial_number == "FOX2109PGD1" match = "Unable to retrieve switch information from the controller. " @@ -158,40 +133,36 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key,expected", [ - ("SwitchDetails_get_return_code_200", does_not_raise()), + ("test_image_mgmt_switch_details_00022a", does_not_raise()), ( - "SwitchDetails_get_return_code_404", + "test_image_mgmt_switch_details_00022b", pytest.raises(AnsibleFailJson, match=match), ), ( - "SwitchDetails_get_return_code_500", + "test_image_mgmt_switch_details_00022c", pytest.raises(AnsibleFailJson, match=match), ), ], ) -def test_result(monkeypatch, module, key, expected) -> None: +def test_image_mgmt_switch_details_00022( + monkeypatch, switch_details, key, expected +) -> None: """ - Function description: - - SwitchDetails.result returns the result of its superclass - method ImageUpgradeCommon._handle_response() - - Expectations: - - 1. 200 RETURN_CODE, MESSAGE == "OK", - SwitchDetails.result == {'found': True, 'success': True} - - 2. 404 RETURN_CODE, MESSAGE == "Not Found", - SwitchDetails.result == {'found': False, 'success': True} - - 3. 500 RETURN_CODE, MESSAGE ~= "Internal Server Error", - SwitchDetails.result == {'found': False, 'success': False} - - Expected results: - - 1. SwitchDetails_result_200 == {'found': True, 'success': True} - 2. SwitchDetails_result_404 == {'found': False, 'success': True} - 3. SwitchDetails_result_500 == {'found': False, 'success': False} + Function + - switch_details.refresh + - switch_details.result + - ImageUpgradeCommon._handle_response + + Test + - test_image_mgmt_switch_details_00022a + - 200 RETURN_CODE, MESSAGE == "OK" + - result == {'found': True, 'success': True} + - test_image_mgmt_switch_details_00022b + - 404 RETURN_CODE, MESSAGE == "Not Found" + - result == {'found': False, 'success': True} + - test_image_mgmt_switch_details_00022c + - 500 RETURN_CODE, MESSAGE ~= "Internal Server Error" + - result == {'found': False, 'success': False} """ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: @@ -200,7 +171,7 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) with expected: - module.refresh() + switch_details.refresh() @pytest.mark.parametrize( @@ -218,9 +189,19 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: ("switchRole", "border gateway"), ], ) -def test_get_with_ip_address_set(monkeypatch, module, item, expected) -> None: +def test_image_mgmt_switch_details_00023( + monkeypatch, switch_details, item, expected +) -> None: """ - Function description: + Function + - switch_details.refresh + - switch_details.ip_address + - switch_details._get + + Test + - _get returns correct property values + + Description SwitchDetails._get is called by all getter properties. @@ -230,80 +211,105 @@ def test_get_with_ip_address_set(monkeypatch, module, item, expected) -> None: It returns the value of the requested property if the user has set ip_address and the property name is known. - The property value is passed to both make_boolean() and make_none(), which - either: - - converts it to a boolean - - converts it to NoneType - - returns the value unchanged - - Expected results: - - 1. Property values are returned as expected + Property values are passed to make_boolean() and make_none(), which either: + - converts value to a boolean + - converts value to NoneType + - returns value unchanged """ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "SwitchDetails_get_return_code_200" + key = "test_image_mgmt_switch_details_00023a" return responses_switch_details(key) monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) - module.refresh() - module.ip_address = "172.22.150.110" - assert module._get(item) == expected + switch_details.refresh() + switch_details.ip_address = "172.22.150.110" + assert switch_details._get(item) == expected -def test_get_with_unknown_ip_address(monkeypatch, module) -> None: +def test_image_mgmt_switch_details_00024(monkeypatch, switch_details) -> None: """ - Function description: + Function + - switch_details.refresh + - switch_details.ip_address + - switch_details._get + Test + - _get calls fail_json when switch_details.ip_address is unknown + + Description SwitchDetails._get is called by all getter properties. It raises AnsibleFailJson if the user has not set ip_address or if the ip_address is unknown, or if an unknown property name is queried. It returns the value of the requested property if the user has set a known ip_address. - - Expected results: - - 1. fail_json is called with appropriate error message since an unknown - ip_address is set. """ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "SwitchDetails_get_return_code_200" + key = "test_image_mgmt_switch_details_00024a" return responses_switch_details(key) monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) - module.refresh() - module.ip_address = "1.1.1.1" + switch_details.refresh() + switch_details.ip_address = "1.1.1.1" match = "SwitchDetails._get: 1.1.1.1 does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): - module._get("hostName") + switch_details._get("hostName") -def test_get_with_unknown_property_name(monkeypatch, module) -> None: +def test_image_mgmt_switch_details_00025(monkeypatch, switch_details) -> None: """ - Function description: + Function + - switch_details.refresh + - switch_details.ip_address + - switch_details._get + Test + - _get calls fail_json when an unknown property name is queried + + Description SwitchDetails._get is called by all getter properties. It raises AnsibleFailJson if the user has not set ip_address or if the ip_address is unknown, or if an unknown property name is queried. - - Expected results: - - 1. fail_json is called with appropriate error message since an - unknown property name is queried. """ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "SwitchDetails_get_return_code_200" + key = "test_image_mgmt_switch_details_00025a" return responses_switch_details(key) monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) - module.refresh() - module.ip_address = "172.22.150.110" + switch_details.refresh() + switch_details.ip_address = "172.22.150.110" match = "SwitchDetails._get: 172.22.150.110 does not have a key named FOO." with pytest.raises(AnsibleFailJson, match=match): - module._get("FOO") + switch_details._get("FOO") + + +# setters + + +@pytest.mark.parametrize( + "ip_address_is_set, expected", + [ + (True, "1.2.3.4"), + (False, None), + ], +) +def test_image_mgmt_switch_details_00060( + switch_details, ip_address_is_set, expected +) -> None: + """ + Function + - ip_address.setter + + Test + - return IP address, if set + - return None, if not set + """ + if ip_address_is_set: + switch_details.ip_address = "1.2.3.4" + assert switch_details.ip_address == expected From 88668a904d325f6cebd154e2a51b08523a60948a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 10 Nov 2023 13:55:29 -1000 Subject: [PATCH 094/300] Run thru black/isort, doctstring cleanup --- .../test_image_upgrade_ImagePolicyAction.py | 99 ++++++++++--------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index 4523292c3..05a3c7d6f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -1,6 +1,6 @@ """ controller_version: 12 -description: Verify functionality of ImageStage +Description: Verify functionality of ImagePolicyAction """ from contextlib import contextmanager @@ -87,8 +87,11 @@ def image_policies() -> ImagePolicies: def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: """ - Function: __init__ - Test: All class attributes initialized to expected values + Function + - __init__ + + Test + - Class attributes initialized to expected values """ image_policy_action.__init__(MockAnsibleModule) assert isinstance(image_policy_action, ImagePolicyAction) @@ -100,8 +103,11 @@ def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: """ - Function: _init_properties - Test: All class properties initialized to expected values + Function + - _init_properties + + Test + - Class properties are initialized to expected values """ image_policy_action._init_properties() assert isinstance(image_policy_action.properties, dict) @@ -117,15 +123,15 @@ def test_image_mgmt_image_policy_action_00003( monkeypatch, image_policy_action, issu_details ) -> None: """ - Function: - build_payload + Function + - build_payload - Test: - - fail_json is not called - - image_policy_action.payloads is a list - - image_policy_action.payloads has length 5 + Test + - fail_json is not called + - image_policy_action.payloads is a list + - image_policy_action.payloads has length 5 - Description: + Description build_payload builds the payload to send in the POST request to attach policies to devices """ @@ -157,15 +163,14 @@ def test_image_mgmt_image_policy_action_00004( monkeypatch, image_policy_action, issu_details ) -> None: """ - Function: - build_payload + Function + - build_payload - Test: - - fail_json is called since deviceName is null in the issu_details - response - - The error message is matched + Test + - fail_json is called since deviceName is null in the issu_details response + - The error message is matched - Description: + Description build_payload builds the payload to send in the POST request to attach policies to devices. If any key in the payload has a value of None, the function calls fail_json. @@ -196,14 +201,14 @@ def test_image_mgmt_image_policy_action_00010( image_policy_action, issu_details ) -> None: """ - Function: - validate_request + Function + - validate_request - Test: - - fail_json is called because image_policy_action.action is None - - The error message is matched + Test + - fail_json is called because image_policy_action.action is None + - The error message is matched - Description: + Description validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a validation-specific error message. @@ -236,14 +241,14 @@ def test_image_mgmt_image_policy_action_00011( action, expected, image_policy_action, issu_details ) -> None: """ - Function: - validate_request + Function + - validate_request - Test: - - fail_json is called because image_policy_action.policy_name is None - - The error message is matched + Test + - fail_json is called because image_policy_action.policy_name is None + - The error message is matched - Description: + Description validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a validation-specific error message. @@ -275,10 +280,10 @@ def test_image_mgmt_image_policy_action_00012( action, expected, image_policy_action, issu_details ) -> None: """ - Function: - validate_request + Function + - validate_request - Test: + Test - fail_json is called for action == attach because image_policy_action.serial_numbers is None - fail_json is called for action == detach because @@ -287,7 +292,7 @@ def test_image_mgmt_image_policy_action_00012( validate_request is exited early for action == "query" - The error message, if any, is matched - Description: + Description validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a validation-specific error message. @@ -305,16 +310,16 @@ def test_image_mgmt_image_policy_action_00013( monkeypatch, image_policy_action, issu_details, image_policies ) -> None: """ - Function: - validate_request + Function + - validate_request - Test: + Test - fail_json is called because policy KR5M supports playform N9K/N3K and the response from ImagePolicies contains platform TEST_UNKNOWN_PLATFORM - The error message is matched - Description: + Description validate_request performs a number of validations prior to calling commit validate_request performs a number of validations prior to calling commit If any of these validations fail, the function calls fail_json with a @@ -349,14 +354,14 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_image_policy_action_00020(monkeypatch, image_policy_action) -> None: """ - Function: - commit + Function + - commit - Test: + Test - fail_json is called because action is unknown - The error message is matched - Description: + Description commit calls validate_request() and then calls one of the following functions based on the value of action: action == "attach" : _attach_policy @@ -405,16 +410,16 @@ def mock_validate_request(*args, **kwargs) -> None: def test_image_mgmt_image_policy_action_00021(monkeypatch, image_policy_action) -> None: """ - Function: - commit + Function + - commit - Test: + Test - action is "detach", so ImagePolicyAction._detach_policy is called - commit is successful given a 200 response from the controller in ImagePolicyAction._detach_policy - ImagePolicyAction.response contains RESULT_CODE 200 - Description: + Description commit calls validate_request() and then calls one of the following functions based on the value of action: action == "attach" : _attach_policy From 3f8b03f911215e2c063cc56e548ae695677c24a0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 10 Nov 2023 15:26:03 -1000 Subject: [PATCH 095/300] Add property vpc_role2, more... Two properties exist for vpc_role in the controller response. - vpc_role2 corresponds to vpc_role. - vpc_role corresponds to vpcRole method_name made local in both __init__ and _init_properties --- .../image_mgmt/switch_issu_details.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index e0ed5fe45..21fedfda9 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -605,6 +605,8 @@ def vpc_role(self): Return the vpcRole of the switch with ip_address, if it exists. Return None otherwise + NOTE: Two properties exist for vpc_role in the controller response. + vpc_role corresponds to vpcRole. Possible values: vpc role e.g.: "primary" @@ -616,6 +618,24 @@ def vpc_role(self): """ return self._get("vpcRole") + @property + def vpc_role2(self): + """ + Return the vpc_role of the switch with ip_address, if it exists. + Return None otherwise + + NOTE: Two properties exist for vpc_role in the controller response. + vpc_role2 corresponds to vpc_role. + Possible values: + vpc role e.g.: + "primary" + "secondary" + "none" -> This will be translated to None + "none established" (TODO:3 verify this) + "primary, operational secondary" (TODO:3 verify this) + None + """ + return self._get("vpc_role") class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): """ @@ -812,12 +832,12 @@ class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self._init_properties() def _init_properties(self): super()._init_properties() - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.properties["device_name"] = None def refresh(self): From bced27661748af3ea48c47aabe29925a6b70ddda Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 10 Nov 2023 15:27:03 -1000 Subject: [PATCH 096/300] Standardize test names --- ...e_upgrade_responses_SwitchIssuDetails.json | 180 ++++++++++ ...e_upgrade_SwitchIssuDetailsByDeviceName.py | 310 +++++++++++------- 2 files changed, 371 insertions(+), 119 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 3c9f59216..5595b2139 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -1,4 +1,184 @@ { + "test_image_mgmt_switch_issu_details_by_device_name_00020a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.deviceName is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "deviceName": "leaf1" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_device_name_00021a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "Two switches present", + "First switch requires only deviceName and serialNumber", + "Second switch requires all fields" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "serialNumber": "FDO21120U5D" + }, + { + "deviceName": "cvd-2313-leaf", + "ethswitchid": 39890, + "serialNumber": "FDO2112189M", + "fabric": "hard", + "fcoEEnabled": "False", + "group": "hard", + "id": 2, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.108", + "ip_address": "172.22.150.108", + "issuAllowed": "", + "lastUpgAction": "2023-Oct-06 03:43", + "mds": "False", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": "null", + "platform": "N9K", + "policy": "KR5M", + "reason": "Upgrade", + "role": "leaf", + "status": "In-Sync", + "statusPercent": 100, + "sys_name": "cvd-2313-leaf", + "systemMode": "Normal", + "upgGroups": "null", + "upgrade": "Success", + "upgradePercent": 100, + "validated": "Success", + "validatedPercent": 100, + "version": "10.2(5)", + "vdcId": 0, + "vdc_id": -1, + "vpcPeer": "null", + "vpcRole": "FOO", + "vpc_role": "BAR" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_device_name_00022a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.deviceName is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "deviceName": "leaf1" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_device_name_00023a": { + "TEST_NOTES": [ + "RETURN_CODE 404", + "MESSAGE != OK", + "REQUEST_PATH does not exist on the controller" + ], + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/bad/path", + "MESSAGE": "Not Found", + "DATA": { + "timestamp": "2023-10-22T21:01:17.375+00:00", + "status": 404, + "error": "Not Found", + "path": "/bad/path" + } + }, + "test_image_mgmt_switch_issu_details_by_device_name_00024a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA is empty" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": {} + }, + "test_image_mgmt_switch_issu_details_by_device_name_00025a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.lastOperDataObject is empty" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_device_name_00040a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.deviceName != FOO is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "deviceName": "leaf1" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_device_name_00041a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.deviceName == leaf1 is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "deviceName": "leaf1" + } + ], + "message": "" + } + }, "packagemgnt_issu_get_return_code_200_one_switch": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py index dd4dba1a7..155e577a2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py @@ -2,7 +2,7 @@ controller_version: 12 description: Verify functionality of subclass SwitchIssuDetailsByDeviceName """ - +from contextlib import contextmanager from typing import Any, Dict import pytest @@ -13,6 +13,12 @@ from .fixture import load_fixture + +@contextmanager +def does_not_raise(): + yield + + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." @@ -34,32 +40,59 @@ def responses_switch_issu_details(key: str) -> Dict[str, str]: @pytest.fixture -def module(): +def issu_details(): return SwitchIssuDetailsByDeviceName(MockAnsibleModule) -def test_init_properties(module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00001(issu_details) -> None: + """ + Function + - __init__ + + Test + - fail_json is not called + - issu_details.properties is a dict + """ + with does_not_raise(): + issu_details.__init__(MockAnsibleModule) + assert isinstance(issu_details.properties, dict) + + +def test_image_mgmt_switch_issu_details_by_device_name_00002(issu_details) -> None: """ - Properties are initialized to expected values + Function + - _init_properties + + Test + - Class properties initialized to expected values + - issu_details.properties is a dict + - issu_details.action_keys is a set + - action_keys contains expected values """ action_keys = {"imageStaged", "upgrade", "validated"} - module._init_properties() - assert isinstance(module.properties, dict) - assert isinstance(module.properties.get("action_keys"), set) - assert module.properties.get("action_keys") == action_keys - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None - assert module.properties.get("device_name") == None + issu_details._init_properties() + assert isinstance(issu_details.properties, dict) + assert isinstance(issu_details.properties.get("action_keys"), set) + assert issu_details.properties.get("action_keys") == action_keys + assert issu_details.properties.get("response_data") == None + assert issu_details.properties.get("response") == None + assert issu_details.properties.get("result") == None + assert issu_details.properties.get("device_name") == None -def test_refresh_return_code_200(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00020( + monkeypatch, issu_details +) -> None: """ - NDFC response data for 200 response has expected types. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + + Test + - issu_details.response is a dict + - issu_details.response_data is a list """ - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_device_name_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -67,17 +100,23 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - assert isinstance(module.response, dict) - assert isinstance(module.response_data, list) + issu_details.refresh() + assert isinstance(issu_details.response, dict) + assert isinstance(issu_details.response_data, list) -def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00021( + monkeypatch, issu_details +) -> None: """ - Properties are set based on device_name setter value. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + + Test + - Properties are set based on device_name + - Expected property values are returned """ - key = "packagemgnt_issu_get_return_code_200_many_switch" + key = "test_image_mgmt_switch_issu_details_by_device_name_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -85,64 +124,77 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.device_name = "leaf1" - assert module.device_name == "leaf1" - assert module.serial_number == "FDO21120U5D" + issu_details.refresh() + issu_details.device_name = "leaf1" + assert issu_details.device_name == "leaf1" + assert issu_details.serial_number == "FDO21120U5D" # change device_name to a different switch, expect different information - module.device_name = "cvd-2313-leaf" - assert module.device_name == "cvd-2313-leaf" - assert module.serial_number == "FDO2112189M" + issu_details.device_name = "cvd-2313-leaf" + assert issu_details.device_name == "cvd-2313-leaf" + assert issu_details.serial_number == "FDO2112189M" # verify remaining properties using current device_name - assert module.eth_switch_id == 39890 - assert module.fabric == "hard" - assert module.fcoe_enabled == False - assert module.group == "hard" + assert issu_details.eth_switch_id == 39890 + assert issu_details.fabric == "hard" + assert issu_details.fcoe_enabled == False + assert issu_details.group == "hard" # NOTE: For "id" see switch_id below - assert module.image_staged == "Success" - assert module.image_staged_percent == 100 - assert module.ip_address == "172.22.150.108" - assert module.issu_allowed == None - assert module.last_upg_action == "2023-Oct-06 03:43" - assert module.mds == False - assert module.mode == "Normal" - assert module.model == "N9K-C93180YC-EX" - assert module.model_type == 0 - assert module.peer == None - assert module.platform == "N9K" - assert module.policy == "KR5M" - assert module.reason == "Upgrade" - assert module.role == "leaf" - assert module.status == "In-Sync" - assert module.status_percent == 100 + assert issu_details.image_staged == "Success" + assert issu_details.image_staged_percent == 100 + assert issu_details.ip_address == "172.22.150.108" + assert issu_details.issu_allowed == None + assert issu_details.last_upg_action == "2023-Oct-06 03:43" + assert issu_details.mds == False + assert issu_details.mode == "Normal" + assert issu_details.model == "N9K-C93180YC-EX" + assert issu_details.model_type == 0 + assert issu_details.peer == None + assert issu_details.platform == "N9K" + assert issu_details.policy == "KR5M" + assert issu_details.reason == "Upgrade" + assert issu_details.role == "leaf" + assert issu_details.status == "In-Sync" + assert issu_details.status_percent == 100 # NOTE: switch_id appears in the response data as "id" # NOTE: "id" is a python reserved keyword, so we changed the property name - assert module.switch_id == 2 - assert module.sys_name == "cvd-2313-leaf" - assert module.system_mode == "Normal" - assert module.upg_groups == None - assert module.upgrade == "Success" - assert module.upgrade_percent == 100 - assert module.validated == "Success" - assert module.validated_percent == 100 - assert module.version == "10.2(5)" + assert issu_details.switch_id == 2 + assert issu_details.sys_name == "cvd-2313-leaf" + assert issu_details.system_mode == "Normal" + assert issu_details.upg_groups == None + assert issu_details.upgrade == "Success" + assert issu_details.upgrade_percent == 100 + assert issu_details.validated == "Success" + assert issu_details.validated_percent == 100 + assert issu_details.version == "10.2(5)" # NOTE: Two vdc_id values exist in the response data for each switch. # NOTE: Namely, "vdcId" and "vdc_id" # NOTE: Properties are provided for both, as follows. # NOTE: vdc_id == vdcId # NOTE: vdc_id2 == vdc_id - assert module.vdc_id == 0 - assert module.vdc_id2 == -1 - assert module.vpc_peer == None - assert module.vpc_role == None + assert issu_details.vdc_id == 0 + assert issu_details.vdc_id2 == -1 + assert issu_details.vpc_peer == None + # NOTE: Two vpc role keys exist in the response data for each switch. + # NOTE: Namely, "vpcRole" and "vpc_role" + # NOTE: Properties are provided for both, as follows. + # NOTE: vpc_role == vpcRole + # NOTE: vpc_role2 == vpc_role + # NOTE: Values are synthesized in the response for this test + assert issu_details.vpc_role == "FOO" + assert issu_details.vpc_role2 == "BAR" -def test_result_return_code_200(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00022( + monkeypatch, issu_details +) -> None: """ - result contains expected key/values on 200 response from endpoint. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + + Test + - issu_details.result is a dict + - issu_details.result contains expected key/values for 200 RESULT_CODE """ - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_device_name_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -150,56 +202,71 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - assert isinstance(module.result, dict) - assert module.result.get("found") == True - assert module.result.get("success") == True + issu_details.refresh() + assert isinstance(issu_details.result, dict) + assert issu_details.result.get("found") == True + assert issu_details.result.get("success") == True -def test_result_return_code_404(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00023( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 404 response from malformed endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - refresh calls handle_response, which calls json_fail on 404 response + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_404" + key = "test_image_mgmt_switch_issu_details_by_device_name_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "Bad result when retriving switch information from the controller" - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "Bad result when retriving switch information from the controller" + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_result_return_code_200_empty_data(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00024( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 200 response with empty DATA key. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on 200 response with empty DATA key + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_200_empty_DATA" + key = "test_image_mgmt_switch_issu_details_by_device_name_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "SwitchIssuDetailsByDeviceName.refresh: " - error_message += "The controller has no switch ISSU information." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "SwitchIssuDetailsByDeviceName.refresh: " + match += "The controller has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_result_return_code_200_switch_issu_info_length_0(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00025( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 200 response with DATA.lastOperDataObject length 0. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on 200 response with DATA.lastOperDataObject length 0 + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_200" - key += "_switch_issu_info_length_0" + key = "test_image_mgmt_switch_issu_details_by_device_name_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -207,67 +274,72 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "SwitchIssuDetailsByDeviceName.refresh: " - error_message += "The controller has no switch ISSU information." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "SwitchIssuDetailsByDeviceName.refresh: " + match += "The controller has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_get_with_unknown_device_name(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00040( + monkeypatch, issu_details +) -> None: """ - Function description: + Function + - _get + + Test + - fail_json is called due to unknown device_name is set + - Error message matches expectation SwitchIssuDetailsByDeviceName._get is called by all getter properties. It raises AnsibleFailJson if the user has not set device_name or if device_name is unknown, or if an unknown property name is queried. It returns the value of the requested property if the user has set a known device_name. - - Expected results: - - 1. fail_json is called with appropriate error message since an unknown - device_name is set. """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_device_name_00040a" return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.device_name = "FOO" + issu_details.refresh() + issu_details.device_name = "FOO" match = "SwitchIssuDetailsByDeviceName._get: FOO does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): - module._get("serialNumber") + issu_details._get("serialNumber") -def test_get_with_unknown_property_name(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00041( + monkeypatch, issu_details +) -> None: """ - Function description: + Function + - _get + Test + - fail_json is called on access of unknown property name + - Error message matches expectation + + Description SwitchIssuDetailsByDeviceName._get is called by all getter properties. It raises AnsibleFailJson if the user has not set device_name or if device_name is unknown, or if an unknown property name is queried. It returns the value of the requested property if the user has set a known ip_address. - - Expected results: - - 1. fail_json is called with appropriate error message since an unknown - property is queried. """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_device_name_00041a" return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.device_name = "leaf1" + issu_details.refresh() + issu_details.device_name = "leaf1" match = "SwitchIssuDetailsByDeviceName._get: leaf1 unknown " match += f"property name: FOO" with pytest.raises(AnsibleFailJson, match=match): - module._get("FOO") + issu_details._get("FOO") From c540c1d0d066bec362308ea1aa92a9a49791cac9 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 10 Nov 2023 17:09:47 -1000 Subject: [PATCH 097/300] Standardize test names, run thru black/isort --- ...e_upgrade_responses_SwitchIssuDetails.json | 361 ++++++++++++++++++ ...ge_upgrade_SwitchIssuDetailsByIpAddress.py | 283 +++++++++----- ...upgrade_SwitchIssuDetailsBySerialNumber.py | 282 +++++++++----- 3 files changed, 715 insertions(+), 211 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 5595b2139..e09b256c5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -179,6 +179,367 @@ "message": "" } }, + "test_image_mgmt_switch_issu_details_by_ip_address_00020a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.lastOperDataObject.ipAddress is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "ipAddress": "172.22.150.102" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_ip_address_00021a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "Two switches present", + "First switch requires only ipAddress and deviceName", + "Second switch requires all fields" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "ipAddress": "172.22.150.102", + "deviceName": "leaf1", + "serialNumber": "FDO21120U5D" + }, + { + "deviceName": "cvd-2313-leaf", + "ethswitchid": 39890, + "serialNumber": "FDO2112189M", + "fabric": "hard", + "fcoEEnabled": "False", + "group": "hard", + "id": 2, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.108", + "ip_address": "172.22.150.108", + "issuAllowed": "", + "lastUpgAction": "2023-Oct-06 03:43", + "mds": "False", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": "null", + "platform": "N9K", + "policy": "KR5M", + "reason": "Upgrade", + "role": "leaf", + "status": "In-Sync", + "statusPercent": 100, + "sys_name": "cvd-2313-leaf", + "systemMode": "Normal", + "upgGroups": "null", + "upgrade": "Success", + "upgradePercent": 100, + "validated": "Success", + "validatedPercent": 100, + "version": "10.2(5)", + "vdcId": 0, + "vdc_id": -1, + "vpcPeer": "null", + "vpcRole": "FOO", + "vpc_role": "BAR" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_ip_address_00022a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.ipAddress is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "ipAddress": "172.22.150.102" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_ip_address_00023a": { + "TEST_NOTES": [ + "RETURN_CODE 404", + "MESSAGE != OK", + "REQUEST_PATH does not exist on the controller" + ], + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/bad/path", + "MESSAGE": "Not Found", + "DATA": { + "timestamp": "2023-10-22T21:01:17.375+00:00", + "status": 404, + "error": "Not Found", + "path": "/bad/path" + } + }, + "test_image_mgmt_switch_issu_details_by_ip_address_00024a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA is empty" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": {} + }, + "test_image_mgmt_switch_issu_details_by_ip_address_00025a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.lastOperDataObject is empty" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_ip_address_00040a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.ipAddress != 1.1.1.1 is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "ipAddress": "5.5.5.5" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_ip_address_00041a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.ipAddress == 172.22.150.102 is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "ipAddress": "172.22.150.102" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_serial_number_00020a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.lastOperDataObject.serialNumber is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_serial_number_00021a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "Two switches present", + "First switch requires only serialNumber and deviceName", + "Second switch requires all fields" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "serialNumber": "FDO21120U5D" + }, + { + "deviceName": "cvd-2313-leaf", + "ethswitchid": 39890, + "serialNumber": "FDO2112189M", + "fabric": "hard", + "fcoEEnabled": "False", + "group": "hard", + "id": 2, + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.108", + "ip_address": "172.22.150.108", + "issuAllowed": "", + "lastUpgAction": "2023-Oct-06 03:43", + "mds": "False", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "peer": "null", + "platform": "N9K", + "policy": "KR5M", + "reason": "Upgrade", + "role": "leaf", + "status": "In-Sync", + "statusPercent": 100, + "sys_name": "cvd-2313-leaf", + "systemMode": "Normal", + "upgGroups": "null", + "upgrade": "Success", + "upgradePercent": 100, + "validated": "Success", + "validatedPercent": 100, + "version": "10.2(5)", + "vdcId": 0, + "vdc_id": -1, + "vpcPeer": "null", + "vpcRole": "FOO", + "vpc_role": "BAR" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_serial_number_00022a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.serialNumber is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_serial_number_00023a": { + "TEST_NOTES": [ + "RETURN_CODE 404", + "MESSAGE != OK", + "REQUEST_PATH does not exist on the controller" + ], + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/bad/path", + "MESSAGE": "Not Found", + "DATA": { + "timestamp": "2023-10-22T21:01:17.375+00:00", + "status": 404, + "error": "Not Found", + "path": "/bad/path" + } + }, + "test_image_mgmt_switch_issu_details_by_serial_number_00024a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA is empty" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": {} + }, + "test_image_mgmt_switch_issu_details_by_serial_number_00025a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.lastOperDataObject is empty" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_serial_number_00040a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.serialNumber != FOO00000BAR is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D" + } + ], + "message": "" + } + }, + "test_image_mgmt_switch_issu_details_by_serial_number_00041a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "DATA.serialNumber == FDO21120U5D is required by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D" + } + ], + "message": "" + } + }, "packagemgnt_issu_get_return_code_200_one_switch": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py index 71da71580..075316d63 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py @@ -2,7 +2,7 @@ controller_version: 12 description: Verify functionality of subclass SwitchIssuDetailsByIpAddress """ - +from contextlib import contextmanager from typing import Any, Dict import pytest @@ -13,6 +13,12 @@ from .fixture import load_fixture + +@contextmanager +def does_not_raise(): + yield + + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." @@ -34,33 +40,59 @@ def responses_switch_issu_details(key: str) -> Dict[str, str]: @pytest.fixture -def module(): +def issu_details(): return SwitchIssuDetailsByIpAddress(MockAnsibleModule) -def test_init_properties(module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00001(issu_details) -> None: + """ + Function + - __init__ + + Test + - fail_json is not called + - issu_details.properties is a dict """ - Properties are initialized to expected values + with does_not_raise(): + issu_details.__init__(MockAnsibleModule) + assert isinstance(issu_details.properties, dict) + + +def test_image_mgmt_switch_issu_details_by_ip_address_00002(issu_details) -> None: + """ + Function + - _init_properties + + Test + - Class properties initialized to expected values + - issu_details.properties is a dict + - issu_details.action_keys is a set + - action_keys contains expected values """ action_keys = {"imageStaged", "upgrade", "validated"} - module._init_properties() - assert isinstance(module.properties, dict) - assert isinstance(module.properties.get("action_keys"), set) - assert module.properties.get("action_keys") == action_keys - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None - assert module.properties.get("ip_address") == None + issu_details._init_properties() + assert isinstance(issu_details.properties, dict) + assert isinstance(issu_details.properties.get("action_keys"), set) + assert issu_details.properties.get("action_keys") == action_keys + assert issu_details.properties.get("response_data") == None + assert issu_details.properties.get("response") == None + assert issu_details.properties.get("result") == None + assert issu_details.properties.get("ip_address") == None -def test_refresh_return_code_200(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00020( + monkeypatch, issu_details +) -> None: """ - NDFC response data for 200 response has expected types. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + Test + - issu_details.response is a dict + - issu_details.response_data is a list """ - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_ip_address_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -68,17 +100,23 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - assert isinstance(module.response, dict) - assert isinstance(module.response_data, list) + issu_details.refresh() + assert isinstance(issu_details.response, dict) + assert isinstance(issu_details.response_data, list) -def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00021( + monkeypatch, issu_details +) -> None: """ - Properties are set based on ip_address setter value. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + + Test + - Properties are set based on device_name + - Expected property values are returned """ - key = "packagemgnt_issu_get_return_code_200_many_switch" + key = "test_image_mgmt_switch_issu_details_by_ip_address_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -86,64 +124,77 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.ip_address = "172.22.150.102" - assert module.device_name == "leaf1" - assert module.serial_number == "FDO21120U5D" + issu_details.refresh() + issu_details.ip_address = "172.22.150.102" + assert issu_details.device_name == "leaf1" + assert issu_details.serial_number == "FDO21120U5D" # change ip_address to a different switch, expect different information - module.ip_address = "172.22.150.108" - assert module.device_name == "cvd-2313-leaf" - assert module.serial_number == "FDO2112189M" + issu_details.ip_address = "172.22.150.108" + assert issu_details.device_name == "cvd-2313-leaf" + assert issu_details.serial_number == "FDO2112189M" # verify remaining properties using current ip_address - assert module.eth_switch_id == 39890 - assert module.fabric == "hard" - assert module.fcoe_enabled == False - assert module.group == "hard" + assert issu_details.eth_switch_id == 39890 + assert issu_details.fabric == "hard" + assert issu_details.fcoe_enabled == False + assert issu_details.group == "hard" # NOTE: For "id" see switch_id below - assert module.image_staged == "Success" - assert module.image_staged_percent == 100 - assert module.ip_address == "172.22.150.108" - assert module.issu_allowed == None - assert module.last_upg_action == "2023-Oct-06 03:43" - assert module.mds == False - assert module.mode == "Normal" - assert module.model == "N9K-C93180YC-EX" - assert module.model_type == 0 - assert module.peer == None - assert module.platform == "N9K" - assert module.policy == "KR5M" - assert module.reason == "Upgrade" - assert module.role == "leaf" - assert module.status == "In-Sync" - assert module.status_percent == 100 + assert issu_details.image_staged == "Success" + assert issu_details.image_staged_percent == 100 + assert issu_details.ip_address == "172.22.150.108" + assert issu_details.issu_allowed == None + assert issu_details.last_upg_action == "2023-Oct-06 03:43" + assert issu_details.mds == False + assert issu_details.mode == "Normal" + assert issu_details.model == "N9K-C93180YC-EX" + assert issu_details.model_type == 0 + assert issu_details.peer == None + assert issu_details.platform == "N9K" + assert issu_details.policy == "KR5M" + assert issu_details.reason == "Upgrade" + assert issu_details.role == "leaf" + assert issu_details.status == "In-Sync" + assert issu_details.status_percent == 100 # NOTE: switch_id appears in the response data as "id" # NOTE: "id" is a python reserved keyword, so we changed the property name - assert module.switch_id == 2 - assert module.sys_name == "cvd-2313-leaf" - assert module.system_mode == "Normal" - assert module.upg_groups == None - assert module.upgrade == "Success" - assert module.upgrade_percent == 100 - assert module.validated == "Success" - assert module.validated_percent == 100 - assert module.version == "10.2(5)" + assert issu_details.switch_id == 2 + assert issu_details.sys_name == "cvd-2313-leaf" + assert issu_details.system_mode == "Normal" + assert issu_details.upg_groups == None + assert issu_details.upgrade == "Success" + assert issu_details.upgrade_percent == 100 + assert issu_details.validated == "Success" + assert issu_details.validated_percent == 100 + assert issu_details.version == "10.2(5)" # NOTE: Two vdc_id values exist in the response data for each switch. # NOTE: Namely, "vdcId" and "vdc_id" # NOTE: Properties are provided for both, as follows. # NOTE: vdc_id == vdcId # NOTE: vdc_id2 == vdc_id - assert module.vdc_id == 0 - assert module.vdc_id2 == -1 - assert module.vpc_peer == None - assert module.vpc_role == None + assert issu_details.vdc_id == 0 + assert issu_details.vdc_id2 == -1 + assert issu_details.vpc_peer == None + # NOTE: Two vpc role keys exist in the response data for each switch. + # NOTE: Namely, "vpcRole" and "vpc_role" + # NOTE: Properties are provided for both, as follows. + # NOTE: vpc_role == vpcRole + # NOTE: vpc_role2 == vpc_role + # NOTE: Values are synthesized in the response for this test + assert issu_details.vpc_role == "FOO" + assert issu_details.vpc_role2 == "BAR" -def test_result_return_code_200(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00022( + monkeypatch, issu_details +) -> None: """ - result contains expected key/values on 200 response from endpoint. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + + Test + - issu_details.result is a dict + - issu_details.result contains expected key/values for 200 RESULT_CODE """ - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_ip_address_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -151,18 +202,24 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - assert isinstance(module.result, dict) - assert module.result.get("found") == True - assert module.result.get("success") == True + issu_details.refresh() + assert isinstance(issu_details.result, dict) + assert issu_details.result.get("found") == True + assert issu_details.result.get("success") == True -def test_result_return_code_404(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00023( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 404 response from malformed endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - refresh calls handle_response, which calls json_fail on 404 response + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_404" + key = "test_image_mgmt_switch_issu_details_by_ip_address_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -170,37 +227,47 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "Bad result when retriving switch information from the controller" - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "Bad result when retriving switch information from the controller" + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_result_return_code_200_empty_data(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00024( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 200 response with empty DATA key. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on 200 response with empty DATA key + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_200_empty_DATA" + key = "test_image_mgmt_switch_issu_details_by_ip_address_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "SwitchIssuDetailsByIpAddress.refresh: " - error_message += "The controller has no switch ISSU information." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "SwitchIssuDetailsByIpAddress.refresh: " + match += "The controller has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_result_return_code_200_switch_issu_info_length_0(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00025( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 200 response with DATA.lastOperDataObject length 0. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on 200 response with DATA.lastOperDataObject length 0 + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_200" - key += "_switch_issu_info_length_0" + key = "test_image_mgmt_switch_issu_details_by_ip_address_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -208,13 +275,15 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "SwitchIssuDetailsByIpAddress.refresh: " - error_message += "The controller has no switch ISSU information." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "SwitchIssuDetailsByIpAddress.refresh: " + match += "The controller has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_get_with_unknown_ip_address(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00040( + monkeypatch, issu_details +) -> None: """ Function description: @@ -231,20 +300,22 @@ def test_get_with_unknown_ip_address(monkeypatch, module) -> None: """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_ip_address_00040a" return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.ip_address = "1.1.1.1" + issu_details.refresh() + issu_details.ip_address = "1.1.1.1" match = "SwitchIssuDetailsByIpAddress._get: 1.1.1.1 does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): - module._get("serialNumber") + issu_details._get("serialNumber") -def test_get_with_unknown_property_name(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00041( + monkeypatch, issu_details +) -> None: """ Function description: @@ -261,14 +332,14 @@ def test_get_with_unknown_property_name(monkeypatch, module) -> None: """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_ip_address_00041a" return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.ip_address = "172.22.150.102" + issu_details.refresh() + issu_details.ip_address = "172.22.150.102" match = "SwitchIssuDetailsByIpAddress._get: 172.22.150.102 unknown " match += f"property name: FOO" with pytest.raises(AnsibleFailJson, match=match): - module._get("FOO") + issu_details._get("FOO") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py index f6547d7e9..0ae4337ea 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py @@ -2,6 +2,7 @@ controller_version: 12 description: Verify functionality of subclass SwitchIssuDetailsBySerialNumber """ +from contextlib import contextmanager from typing import Any, Dict import pytest @@ -12,6 +13,12 @@ from .fixture import load_fixture + +@contextmanager +def does_not_raise(): + yield + + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." @@ -33,33 +40,59 @@ def responses_switch_issu_details(key: str) -> Dict[str, str]: @pytest.fixture -def module(): +def issu_details(): return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) -def test_init_properties(module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00001(issu_details) -> None: """ - Properties are initialized to expected values + Function + - __init__ + + Test + - fail_json is not called + - issu_details.properties is a dict + """ + with does_not_raise(): + issu_details.__init__(MockAnsibleModule) + assert isinstance(issu_details.properties, dict) + + +def test_image_mgmt_switch_issu_details_by_serial_number_00002(issu_details) -> None: + """ + Function + - _init_properties + + Test + - Class properties initialized to expected values + - issu_details.properties is a dict + - issu_details.action_keys is a set + - action_keys contains expected values """ action_keys = {"imageStaged", "upgrade", "validated"} - module._init_properties() - assert isinstance(module.properties, dict) - assert isinstance(module.properties.get("action_keys"), set) - assert module.properties.get("action_keys") == action_keys - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None - assert module.properties.get("serial_number") == None + issu_details._init_properties() + assert isinstance(issu_details.properties, dict) + assert isinstance(issu_details.properties.get("action_keys"), set) + assert issu_details.properties.get("action_keys") == action_keys + assert issu_details.properties.get("response_data") == None + assert issu_details.properties.get("response") == None + assert issu_details.properties.get("result") == None + assert issu_details.properties.get("serial_number") == None -def test_refresh_return_code_200(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00020( + monkeypatch, issu_details +) -> None: """ - NDFC response data for 200 response has expected types. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + Test + - issu_details.response is a dict + - issu_details.response_data is a list """ - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_serial_number_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -67,17 +100,23 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - assert isinstance(module.response, dict) - assert isinstance(module.response_data, list) + issu_details.refresh() + assert isinstance(issu_details.response, dict) + assert isinstance(issu_details.response_data, list) -def test_properties_are_set_to_expected_values(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00021( + monkeypatch, issu_details +) -> None: """ - Properties are set based on serial_number setter value. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + + Test + - Properties are set based on device_name + - Expected property values are returned """ - key = "packagemgnt_issu_get_return_code_200_many_switch" + key = "test_image_mgmt_switch_issu_details_by_serial_number_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -85,64 +124,77 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.serial_number = "FDO21120U5D" - assert module.device_name == "leaf1" - assert module.serial_number == "FDO21120U5D" + issu_details.refresh() + issu_details.serial_number = "FDO21120U5D" + assert issu_details.device_name == "leaf1" + assert issu_details.serial_number == "FDO21120U5D" # change serial_number to a different switch, expect different information - module.serial_number = "FDO2112189M" - assert module.device_name == "cvd-2313-leaf" - assert module.serial_number == "FDO2112189M" + issu_details.serial_number = "FDO2112189M" + assert issu_details.device_name == "cvd-2313-leaf" + assert issu_details.serial_number == "FDO2112189M" # verify remaining properties using current serial_number - assert module.eth_switch_id == 39890 - assert module.fabric == "hard" - assert module.fcoe_enabled == False - assert module.group == "hard" + assert issu_details.eth_switch_id == 39890 + assert issu_details.fabric == "hard" + assert issu_details.fcoe_enabled == False + assert issu_details.group == "hard" # NOTE: For "id" see switch_id below - assert module.image_staged == "Success" - assert module.image_staged_percent == 100 - assert module.ip_address == "172.22.150.108" - assert module.issu_allowed == None - assert module.last_upg_action == "2023-Oct-06 03:43" - assert module.mds == False - assert module.mode == "Normal" - assert module.model == "N9K-C93180YC-EX" - assert module.model_type == 0 - assert module.peer == None - assert module.platform == "N9K" - assert module.policy == "KR5M" - assert module.reason == "Upgrade" - assert module.role == "leaf" - assert module.status == "In-Sync" - assert module.status_percent == 100 + assert issu_details.image_staged == "Success" + assert issu_details.image_staged_percent == 100 + assert issu_details.ip_address == "172.22.150.108" + assert issu_details.issu_allowed == None + assert issu_details.last_upg_action == "2023-Oct-06 03:43" + assert issu_details.mds == False + assert issu_details.mode == "Normal" + assert issu_details.model == "N9K-C93180YC-EX" + assert issu_details.model_type == 0 + assert issu_details.peer == None + assert issu_details.platform == "N9K" + assert issu_details.policy == "KR5M" + assert issu_details.reason == "Upgrade" + assert issu_details.role == "leaf" + assert issu_details.status == "In-Sync" + assert issu_details.status_percent == 100 # NOTE: switch_id appears in the response data as "id" # NOTE: "id" is a python reserved keyword, so we changed the property name - assert module.switch_id == 2 - assert module.sys_name == "cvd-2313-leaf" - assert module.system_mode == "Normal" - assert module.upg_groups == None - assert module.upgrade == "Success" - assert module.upgrade_percent == 100 - assert module.validated == "Success" - assert module.validated_percent == 100 - assert module.version == "10.2(5)" + assert issu_details.switch_id == 2 + assert issu_details.sys_name == "cvd-2313-leaf" + assert issu_details.system_mode == "Normal" + assert issu_details.upg_groups == None + assert issu_details.upgrade == "Success" + assert issu_details.upgrade_percent == 100 + assert issu_details.validated == "Success" + assert issu_details.validated_percent == 100 + assert issu_details.version == "10.2(5)" # NOTE: Two vdc_id values exist in the response data for each switch. # NOTE: Namely, "vdcId" and "vdc_id" # NOTE: Properties are provided for both, as follows. # NOTE: vdc_id == vdcId # NOTE: vdc_id2 == vdc_id - assert module.vdc_id == 0 - assert module.vdc_id2 == -1 - assert module.vpc_peer == None - assert module.vpc_role == None + assert issu_details.vdc_id == 0 + assert issu_details.vdc_id2 == -1 + assert issu_details.vpc_peer == None + # NOTE: Two vpc role keys exist in the response data for each switch. + # NOTE: Namely, "vpcRole" and "vpc_role" + # NOTE: Properties are provided for both, as follows. + # NOTE: vpc_role == vpcRole + # NOTE: vpc_role2 == vpc_role + # NOTE: Values are synthesized in the response for this test + assert issu_details.vpc_role == "FOO" + assert issu_details.vpc_role2 == "BAR" -def test_result_return_code_200(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00022( + monkeypatch, issu_details +) -> None: """ - result contains expected key/values on 200 response from endpoint. - endpoint: .../api/v1/imagemanagement/rest/packagemgnt/issu + Function + - refresh + + Test + - issu_details.result is a dict + - issu_details.result contains expected key/values for 200 RESULT_CODE """ - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_serial_number_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -150,18 +202,24 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - assert isinstance(module.result, dict) - assert module.result.get("found") == True - assert module.result.get("success") == True + issu_details.refresh() + assert isinstance(issu_details.result, dict) + assert issu_details.result.get("found") == True + assert issu_details.result.get("success") == True -def test_result_return_code_404(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00023( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 404 response from malformed endpoint. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - refresh calls handle_response, which calls json_fail on 404 response + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_404" + key = "test_image_mgmt_switch_issu_details_by_serial_number_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -169,37 +227,47 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "Bad result when retriving switch information from the controller" - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "Bad result when retriving switch information from the controller" + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_result_return_code_200_empty_data(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00024( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 200 response with empty DATA key. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on 200 response with empty DATA key + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_200_empty_DATA" + key = "test_image_mgmt_switch_issu_details_by_serial_number_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "SwitchIssuDetailsBySerialNumber.refresh: " - error_message += "The controller has no switch ISSU information." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "SwitchIssuDetailsBySerialNumber.refresh: " + match += "The controller has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_result_return_code_200_switch_issu_info_length_0(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00025( + monkeypatch, issu_details +) -> None: """ - fail_json is called on 200 response with DATA.lastOperDataObject length 0. - endpoint: .../api/v1/imagemanagement/rest/policymgnt/policiess + Function + - refresh + + Test + - fail_json is called on 200 response with DATA.lastOperDataObject length 0 + - Error message matches expectation """ - key = "packagemgnt_issu_get_return_code_200" - key += "_switch_issu_info_length_0" + key = "test_image_mgmt_switch_issu_details_by_serial_number_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -207,13 +275,15 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - error_message = "SwitchIssuDetailsBySerialNumber.refresh: " - error_message += "The controller has no switch ISSU information." - with pytest.raises(AnsibleFailJson, match=error_message): - module.refresh() + match = "SwitchIssuDetailsBySerialNumber.refresh: " + match += "The controller has no switch ISSU information." + with pytest.raises(AnsibleFailJson, match=match): + issu_details.refresh() -def test_get_with_unknown_serial_number(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00040( + monkeypatch, issu_details +) -> None: """ Function description: @@ -230,20 +300,22 @@ def test_get_with_unknown_serial_number(monkeypatch, module) -> None: """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_serial_number_00040a" return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.serial_number = "FOO00000BAR" + issu_details.refresh() + issu_details.serial_number = "FOO00000BAR" match = "SwitchIssuDetailsBySerialNumber._get: FOO00000BAR does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): - module._get("serialNumber") + issu_details._get("serialNumber") -def test_get_with_unknown_property_name(monkeypatch, module) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00041( + monkeypatch, issu_details +) -> None: """ Function description: @@ -260,14 +332,14 @@ def test_get_with_unknown_property_name(monkeypatch, module) -> None: """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "packagemgnt_issu_get_return_code_200_one_switch" + key = "test_image_mgmt_switch_issu_details_by_serial_number_00041a" return responses_switch_issu_details(key) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - module.refresh() - module.serial_number = "FDO21120U5D" + issu_details.refresh() + issu_details.serial_number = "FDO21120U5D" match = "SwitchIssuDetailsBySerialNumber._get: FDO21120U5D unknown " match += f"property name: FOO" with pytest.raises(AnsibleFailJson, match=match): - module._get("FOO") + issu_details._get("FOO") From 4a138908ae64515b50c653830315863ee9fae497 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 11 Nov 2023 15:27:25 -1000 Subject: [PATCH 098/300] Standardize docstrings --- .../test_image_upgrade_ImageStage.py | 289 +++++++++--------- 1 file changed, 141 insertions(+), 148 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py index e53f8f1f0..72e83c612 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py @@ -39,12 +39,14 @@ def responses_controller_version(key: str) -> Dict[str, str]: print(f"responses_controller_version: {key} : {response}") return response + def responses_image_stage(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_ImageStage" response = load_fixture(response_file).get(key) print(f"responses_image_stage: {key} : {response}") return response + def responses_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -69,13 +71,13 @@ def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) -# test_image_mgmt_stage_00001 -# test_init (former names) - - def test_image_mgmt_stage_00001(module) -> None: """ - class attributes are initialized to expected values + Function + - __init__ + + Test + - Class attributes are initialized to expected values """ module.__init__(MockAnsibleModule) assert module.module == MockAnsibleModule @@ -89,13 +91,14 @@ class attributes are initialized to expected values assert isinstance(module.issu_detail, SwitchIssuDetailsBySerialNumber) assert isinstance(module.endpoints, ApiEndpoints) -# test_image_mgmt_stage_00002 -# test_init_properties (former name) - def test_image_mgmt_stage_00002(module) -> None: """ - Properties are initialized to expected values + Function + - _init_properties + + Test + - Class properties are initialized to expected values """ module._init_properties() assert isinstance(module.properties, dict) @@ -107,10 +110,6 @@ def test_image_mgmt_stage_00002(module) -> None: assert module.properties.get("check_timeout") == 1800 -# test_image_mgmt_stage_00003 -# test_populate_controller_version (former name) - - @pytest.mark.parametrize( "key, expected", [ @@ -120,17 +119,18 @@ def test_image_mgmt_stage_00002(module) -> None: ) def test_image_mgmt_stage_00003(monkeypatch, module, key, expected) -> None: """ + Function + - _populate_controller_version + + Test + - test_image_mgmt_stage_00003a -> module.controller_version == "12.1.2e" + - test_image_mgmt_stage_00003b -> module.controller_version == "12.1.3b" + + Description _populate_controller_version retrieves the controller version from the controller. This is used in commit() to populate the payload with either a misspelled "sereialNum" key/value (12.1.2e) or a correctly-spelled "serialNumbers" key/value (12.1.3b). - - Expectations: - 1. module.controller_version should be set - - Expected results: - 1. test_image_mgmt_stage_00003a -> module.controller_version == "12.1.2e" - 2. test_image_mgmt_stage_00003b -> module.controller_version == "12.1.3b" """ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: @@ -142,24 +142,20 @@ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: assert module.controller_version == expected -# test_image_mgmt_stage_00004 -# test_prune_serial_numbers (former name) - - def test_image_mgmt_stage_00004(monkeypatch, module, mock_issu_details) -> None: """ - prune_serial_numbers removes serial numbers from the list for which - imageStaged == "Success" (TODO: AND policy == ) + Function + - prune_serial_numbers - Expectations: - 1. module.serial_numbers should contain only serial numbers for which - imageStaged == "none" - 2. module.serial_numbers should not contain serial numbers for which - imageStaged == "Success" + Test + - module.serial_numbers contains only serial numbers + for which imageStaged == "none" (FDO2112189M, FDO211218AX, FDO211218B5) + - module.serial_numbers does not contain serial numbers + for which imageStaged == "Success" (FDO211218FV, FDO211218GC) - Expected results: - 1. module.serial_numbers == ["FDO2112189M", "FDO211218AX", "FDO211218B5"] - 2. module.serial_numbers != ["FDO211218FV", "FDO211218GC"] + Description + prune_serial_numbers removes serial numbers from the list for which + imageStaged == "Success" (TODO: AND policy == ) """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -186,18 +182,19 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO211218GC" not in module.serial_numbers -# test_image_mgmt_stage_00005 -# test_validate_serial_numbers_failed (former name) - - def test_image_mgmt_stage_00005(monkeypatch, module, mock_issu_details) -> None: """ - fail_json is called when imageStaged == "Failed". + Function + - validate_serial_numbers - Expectations: + Test + - fail_json is not called when imageStaged == "Success" + - fail_json is called when imageStaged == "Failed" - FDO21120U5D should pass since imageStaged == "Success" - FDO2112189M should fail since imageStaged == "Failed" + Description + validate_serial_numbers checks the imageStaged status for each serial + number and raises fail_json if imageStaged == "Failed" for any serial + number. """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -209,38 +206,35 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.issu_detail = mock_issu_details module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] - error_message = "Image staging is failing for the following switch: " - error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. " - error_message += "Check the switch connectivity to the controller " - error_message += "and try again." - with pytest.raises(AnsibleFailJson, match=error_message): + match = "Image staging is failing for the following switch: " + match += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. " + match += "Check the switch connectivity to the controller " + match += "and try again." + with pytest.raises(AnsibleFailJson, match=match): module.validate_serial_numbers() -# test_commit_serial_numbers - -match = r"ImageStage.commit: call instance.serial_numbers " -match += r"before calling commit." +match_00006 = "ImageStage.commit: call instance.serial_numbers " +match_00006 += "before calling commit." @pytest.mark.parametrize( "serial_numbers_is_set, expected", [ (True, does_not_raise()), - (False, pytest.raises(AnsibleFailJson, match=match)), + (False, pytest.raises(AnsibleFailJson, match=match_00006)), ], ) def test_image_mgmt_stage_00006( monkeypatch, module, serial_numbers_is_set, expected ) -> None: """ - fail_json is called when ImageStage.commit() is called without - setting instance.serial_numbers. - - Expectations: + Function + commit - 1. fail_json is called when serial_numbers is None - 2. fail_json is not called when serial_numbers is set + Test + - fail_json is called when serial_numbers is None + - fail_json is not called when serial_numbers is set """ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: @@ -265,21 +259,15 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.commit() -# test_image_mgmt_stage_00007 -# test_commit_path_verb (former name) - - def test_image_mgmt_stage_00007(monkeypatch, module) -> None: """ - ImageStage.path should be set to: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image + Function + - commit - ImageStage.verb should be set to: - POST - - Expectations: - - 1. both self.path and self.verb should be set, per above + Test + - ImageStage.verb is set to POST + - ImageStage.path is set to: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image """ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: @@ -299,12 +287,12 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" + module_path += "stagingmanagement/stage-image" + module.serial_numbers = ["FDO21120U5D"] module.commit() - assert ( - module.path - == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image" - ) + assert module.path == module_path assert module.verb == "POST" @@ -323,15 +311,16 @@ def test_image_mgmt_stage_00008( monkeypatch, module, controller_version, expected_serial_number_key ) -> None: """ - commit() will set the payload key name for the serial number - based on the NDFC version, per Expected Results below: + Function + - commit - Expectations: - 1. The correct serial number key name should be used based on NDFC version + Test + - controller_version 12.1.2e -> key name "sereialNum" (yes, misspelled) + - controller_version 12.1.3b -> key name "serialNumbers - Expected results: - controller_version 12.1.2e -> key name "sereialNum" (yes, misspelled) - controller_version 12.1.3b -> key name "serialNumbers + Description + commit() will set the payload key name for the serial number + based on the controller version, per Expected Results below """ def mock_controller_version(*args, **kwargs) -> None: @@ -342,12 +331,10 @@ def mock_controller_version(*args, **kwargs) -> None: controller_version_patch += "ImageStage._populate_controller_version" monkeypatch.setattr(controller_version_patch, mock_controller_version) - # Needed only for the 200 return code def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00008a" return responses_image_stage(key) - # Needed only for the 200 response def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00008a" return responses_issu_details(key) @@ -360,24 +347,23 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert expected_serial_number_key in module.payload.keys() -# test_image_mgmt_stage_00009 -# test_wait_for_image_stage_to_complete (former name) +def test_image_mgmt_stage_00009(monkeypatch, module, mock_issu_details) -> None: + """ + Function + - _wait_for_image_stage_to_complete + Test + - imageStaged == "Success" for all serial numbers so + fail_json is not called + - instance.serial_numbers_done is a set() + - instance.serial_numbers_done has length 2 + - instance.serial_numbers_done == module.serial_numbers -def test_image_mgmt_stage_00009( - monkeypatch, module, mock_issu_details -) -> None: - """ + Description _wait_for_image_stage_to_complete looks at the imageStaged status for each serial number and waits for it to be "Success" or "Failed". In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. - - Expectations: - 1. module.serial_numbers_done should be a set() - 2. module.serial_numbers_done should be length 2 - 3. module.serial_numbers_done should contain all serial numbers module.serial_numbers - 4. The module should return without calling fail_json. """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -399,24 +385,25 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" in module.serial_numbers_done -# test_image_mgmt_stage_00010 -# test_wait_for_image_stage_to_complete_stage_failed (former name) - - -def test_image_mgmt_stage_00010( - monkeypatch, module, mock_issu_details -) -> None: +def test_image_mgmt_stage_00010(monkeypatch, module, mock_issu_details) -> None: """ + Function + - _wait_for_image_stage_to_complete + + Test + - module.serial_numbers_done is a set() + - module.serial_numbers_done has length 1 + - module.serial_numbers_done contains FDO21120U5D + because imageStaged is "Success" + - fail_json is called on serial number FDO2112189M + because imageStaged is "Failed" + - error message matches expected + + Description _wait_for_image_stage_to_complete looks at the imageStaged status for each serial number and waits for it to be "Success" or "Failed". In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. - - Expectations: - 1. module.serial_numbers_done is a set() - 2. module.serial_numbers_done has length 1 - 3. module.serial_numbers_done contains FDO21120U5D, imageStaged is "Success" - 4. Call fail_json on serial number FDO2112189M, imageStaged is "Failed" """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -442,22 +429,23 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" not in module.serial_numbers_done -# test_image_mgmt_stage_00011 -# test_wait_for_image_stage_to_complete_timout - - -def test_image_mgmt_stage_00011( - monkeypatch, module, mock_issu_details -) -> None: +def test_image_mgmt_stage_00011(monkeypatch, module, mock_issu_details) -> None: """ + Function + - _wait_for_image_stage_to_complete + + Test + - module.serial_numbers_done is a set() + - module.serial_numbers_done has length 1 + - module.serial_numbers_done contains FDO21120U5D + because imageStaged == "Success" + - module.serial_numbers_done does not contain FDO2112189M + - fail_json is called due to timeout because FDO2112189M + imageStaged == "In-Progress" + - error message matches expected + + Description See test_wait_for_image_stage_to_complete for functional details. - - Expectations: - 1. module.serial_numbers_done should be a set() - 2. module.serial_numbers_done should be length 1 - 3. module.serial_numbers_done should contain FDO21120U5D - 3. module.serial_numbers_done should not contain FDO2112189M - 4. The function should call fail_json due to timeout """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -478,6 +466,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "Timed out waiting for image stage to complete. " match += "serial_numbers_done: FDO21120U5D, " match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + with pytest.raises(AnsibleFailJson, match=match): module._wait_for_image_stage_to_complete() assert isinstance(module.serial_numbers_done, set) @@ -490,24 +479,27 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: # test_wait_for_current_actions_to_complete (former name) -def test_image_mgmt_stage_00012( - monkeypatch, module, mock_issu_details -) -> None: +def test_image_mgmt_stage_00012(monkeypatch, module, mock_issu_details) -> None: """ + Function + - _wait_for_current_actions_to_complete + + Test + - instance.serial_numbers_done is a set() + - instance.serial_numbers_done has length 2 + - instance.serial_numbers_done contains all serial numbers + in instance.serial_numbers + - fail_json is not called + + Description _wait_for_current_actions_to_complete waits until staging, validation, and upgrade actions are complete for all serial numbers. It calls SwitchIssuDetailsBySerialNumber.actions_in_progress() and expects this to return False. actions_in_progress() returns True until none of the following keys has a value of "In-Progress": - - ["imageStaged", "upgrade", "validated"] - - Expectations: - 1. module.serial_numbers_done should be a set() - 2. module.serial_numbers_done should be length 2 - 3. module.serial_numbers_done should contain all serial numbers in - module.serial_numbers - 4. The function should return without calling fail_json. + - imageStaged + - upgrade + - validated """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -528,22 +520,23 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO21120U5D" in module.serial_numbers_done assert "FDO2112189M" in module.serial_numbers_done -# test_image_mgmt_stage_00013 -# test_wait_for_current_actions_to_complete_timout (former name) - -def test_image_mgmt_stage_00013( - monkeypatch, module, mock_issu_details -) -> None: +def test_image_mgmt_stage_00013(monkeypatch, module, mock_issu_details) -> None: """ - See test_wait_for_current_actions_to_complete for functional details. - - Expectations: - 1. module.serial_numbers_done should be a set() - 2. module.serial_numbers_done should be length 1 - 3. module.serial_numbers_done should contain FDO21120U5D - 3. module.serial_numbers_done should not contain FDO2112189M - 4. The function should call fail_json due to timeout + Function + - _wait_for_current_actions_to_complete + + Test + - module.serial_numbers_done is a set() + - module.serial_numbers_done has length 1 + - module.serial_numbers_done contains FDO21120U5D + because imageStaged == "Success" + - module.serial_numbers_done does not contain FDO2112189M + - fail_json is called due to timeout because FDO2112189M + imageStaged == "In-Progress" + + Description + See test_image_mgmt_stage_00012 """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: From 00f5f1a857c2b29d2ad150e936ecdc0cb906c54d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 11 Nov 2023 15:28:07 -1000 Subject: [PATCH 099/300] Run thru black/isort --- .../test_image_upgrade_ImageInstallOptions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index 58e0480b9..93867b411 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -70,6 +70,7 @@ def test_image_mgmt_install_options_00002(module) -> None: # test_image_mgmt_install_options_00003 # test_policy_name_not_defined (former name) + def test_image_mgmt_install_options_00003(module) -> None: """ fail_json() is called if policy_name is not set when refresh() is called. @@ -81,9 +82,11 @@ def test_image_mgmt_install_options_00003(module) -> None: with pytest.raises(AnsibleFailJson, match=match): module.refresh() + # test_image_mgmt_install_options_00004 # test_serial_number_not_defined (former name) + def test_image_mgmt_install_options_00004(module) -> None: """ fail_json() is called if serial_number is not set when refresh() is called. @@ -142,6 +145,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: # test_image_mgmt_install_options_00006 # test_refresh_return_code_500 (former name) + def test_image_mgmt_install_options_00006(monkeypatch, module) -> None: """ fail_json() should be called if the response RETURN_CODE != 200 @@ -162,7 +166,6 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: module.refresh() - def test_image_mgmt_install_options_00007(monkeypatch, module) -> None: """ Properties are updated based on: @@ -306,6 +309,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert module.comp_disp == None assert module.result.get("success") == True + def test_image_mgmt_install_options_00010(monkeypatch, module) -> None: """ Properties are updated based on: @@ -338,6 +342,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: # test_image_mgmt_install_options_00020 + def test_image_mgmt_install_options_00020(module) -> None: """ Payload contains defaults if not specified by the user. From 42a61511cb9d43c1d71cab64a415b7edf72354c7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 11 Nov 2023 15:33:07 -1000 Subject: [PATCH 100/300] Add TEST_NOTES and remove unused entries --- .../image_upgrade_responses_ImageStage.json | 6 +- ...e_upgrade_responses_SwitchIssuDetails.json | 1677 ++--------------- 2 files changed, 182 insertions(+), 1501 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json index 703d0794d..140c3bac4 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json @@ -16,7 +16,8 @@ }, "test_image_mgmt_stage_00007a": { "TEST_NOTES": [ - "Needed only for the 200 return code" + "RETURN_CODE == 200", + "MESSAGE == OK" ], "RETURN_CODE": 200, "METHOD": "GET", @@ -31,7 +32,8 @@ }, "test_image_mgmt_stage_00008a": { "TEST_NOTES": [ - "Needed only for the 200 return code" + "RETURN_CODE == 200", + "MESSAGE == OK" ], "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index e09b256c5..de5f6dff8 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -540,1150 +540,14 @@ "message": "" } }, - "packagemgnt_issu_get_return_code_200_one_switch": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - } - ], - "message": "" - } - }, - "packagemgnt_issu_get_return_code_200_empty_DATA": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": {} - }, - "packagemgnt_issu_get_return_code_200_switch_issu_info_length_0": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [], - "message": "" - } - }, - "packagemgnt_issu_get_return_code_404": { - "RETURN_CODE": 404, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issuu", - "MESSAGE": "Not Found", - "DATA": { - "timestamp": "2023-10-22T21:01:17.375+00:00", - "status": 404, - "error": "Not Found", - "path": "/rest/packagemgnt/issuu" - } - }, - "packagemgnt_issu_get_return_code_200_many_switch": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO2112189M", - "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218AX", - "deviceName": "cvd-2312-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.107", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.107", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2312-leaf", - "id": 3, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218B5", - "deviceName": "cvd-2314-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.109", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39840, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.109", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2314-leaf", - "id": 4, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218FV", - "deviceName": "cvd-1314-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:40", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.105", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 153350, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.105", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1314-leaf", - "id": 5, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218GC", - "deviceName": "cvd-1312-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-23 01:43", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.103", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 150610, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.103", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1312-leaf", - "id": 6, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218HB", - "deviceName": "cvd-2311-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.106", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39790, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.106", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2311-leaf", - "id": 7, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218HH", - "deviceName": "cvd-1313-leaf", - "version": "10.2(5)", - "policy": "null", - "status": "null", - "reason": "null", - "imageStaged": "null", - "validated": "null", - "upgrade": "null", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "Never", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.104", - "issuAllowed": "", - "statusPercent": 0, - "imageStagedPercent": 0, - "validatedPercent": 0, - "upgradePercent": 0, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 151490, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.104", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1313-leaf", - "id": 8, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO22180ASJ", - "deviceName": "cvd-2111-bgw", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "border gateway", - "lastUpgAction": "2023-Oct-06 03:42", - "model": "N9K-C9336C-FX2", - "fabric": "hard", - "ipAddress": "172.22.150.100", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39990, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.100", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2111-bgw", - "id": 9, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO23021QYJ", - "deviceName": "cvd-2112-bgw", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "border gateway", - "lastUpgAction": "2023-Oct-06 17:25", - "model": "N9K-C9336C-FX2", - "fabric": "hard", - "ipAddress": "172.22.150.101", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39650, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.101", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2112-bgw", - "id": 10, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO2443096H", - "deviceName": "cvd-rs-111", - "version": "10.3(2)", - "policy": "null", - "status": "null", - "reason": "null", - "imageStaged": "null", - "validated": "null", - "upgrade": "null", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "aggregation", - "lastUpgAction": "Never", - "model": "N9K-C9336C-FX2", - "fabric": "easy", - "ipAddress": "172.22.150.99", - "issuAllowed": "", - "statusPercent": 0, - "imageStagedPercent": 0, - "validatedPercent": 0, - "upgradePercent": 0, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 161530, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.99", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-rs-111", - "id": 11, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FOX2109PGCS", - "deviceName": "cvd-1211-spine", - "version": "10.2(5)", - "policy": "null", - "status": "null", - "reason": "null", - "imageStaged": "null", - "validated": "null", - "upgrade": "null", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "spine", - "lastUpgAction": "Never", - "model": "N9K-C9504", - "fabric": "easy", - "ipAddress": "172.22.150.112", - "issuAllowed": "", - "statusPercent": 0, - "imageStagedPercent": 0, - "validatedPercent": 0, - "upgradePercent": 0, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 154230, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.112", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1211-spine", - "id": 12, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FOX2109PGCT", - "deviceName": "cvd-1111-bgw", - "version": "10.2(5)", - "policy": "null", - "status": "null", - "reason": "null", - "imageStaged": "null", - "validated": "null", - "upgrade": "null", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "border gateway", - "lastUpgAction": "Never", - "model": "N9K-C9504", - "fabric": "easy", - "ipAddress": "172.22.150.110", - "issuAllowed": "", - "statusPercent": 0, - "imageStagedPercent": 0, - "validatedPercent": 0, - "upgradePercent": 0, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 158560, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.110", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1111-bgw", - "id": 13, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FOX2109PGD0", - "deviceName": "cvd-1212-spine", - "version": "10.2(5)", - "policy": "null", - "status": "null", - "reason": "null", - "imageStaged": "null", - "validated": "null", - "upgrade": "null", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "spine", - "lastUpgAction": "Never", - "model": "N9K-C9504", - "fabric": "easy", - "ipAddress": "172.22.150.113", - "issuAllowed": "", - "statusPercent": 0, - "imageStagedPercent": 0, - "validatedPercent": 0, - "upgradePercent": 0, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 155930, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.113", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1212-spine", - "id": 14, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FOX2109PGD1", - "deviceName": "cvd-1112-bgw", - "version": "10.2(5)", - "policy": "null", - "status": "null", - "reason": "null", - "imageStaged": "null", - "validated": "null", - "upgrade": "null", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "border gateway", - "lastUpgAction": "Never", - "model": "N9K-C9504", - "fabric": "easy", - "ipAddress": "172.22.150.111", - "issuAllowed": "", - "statusPercent": 0, - "imageStagedPercent": 0, - "validatedPercent": 0, - "upgradePercent": 0, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 160170, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.111", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1112-bgw", - "id": 15, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FOX2109PHDD", - "deviceName": "cvd-2211-spine", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "spine", - "lastUpgAction": "2023-Oct-06 04:00", - "model": "N9K-C9504", - "fabric": "hard", - "ipAddress": "172.22.150.114", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39690, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.114", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2211-spine", - "id": 16, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FOX2109PHDQ", - "deviceName": "cvd-2212-spine", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "spine", - "lastUpgAction": "2023-Oct-06 04:00", - "model": "N9K-C9504", - "fabric": "hard", - "ipAddress": "172.22.150.115", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39940, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.115", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2212-spine", - "id": 17, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - } - ], - "message": "" - } - }, - "test_image_mgmt_stage_00004a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO2112189M", - "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "none", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218AX", - "deviceName": "cvd-2312-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "none", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.107", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.107", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2312-leaf", - "id": 3, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218B5", - "deviceName": "cvd-2314-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "none", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.109", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39840, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.109", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2314-leaf", - "id": 4, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218FV", - "deviceName": "cvd-1314-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:40", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.105", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 153350, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.105", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1314-leaf", - "id": 5, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO211218GC", - "deviceName": "cvd-1312-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-23 01:43", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.103", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 150610, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.103", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-1312-leaf", - "id": 6, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - } - ], - "message": "" - } - }, - "test_image_mgmt_stage_00005a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO2112189M", - "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Failed", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - } - ], - "message": "" - } - }, - "test_image_mgmt_stage_00006a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO2112189M", - "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Failed", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - } - ], - "message": "" - } - }, - "test_image_mgmt_stage_00007a": { + "test_image_mgmt_stage_00004a": { + "TEST_NOTES": [ + "FDO2112189M: imageStaged == none", + "FDO211218AX: imageStaged == none", + "FDO211218B5: imageStaged == none", + "FDO211218FV: imageStaged == Success", + "FDO211218GC: imageStaged == Success" + ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1692,232 +556,57 @@ "status": "SUCCESS", "lastOperDataObject": [ { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" + "serialNumber": "FDO2112189M", + "imageStaged": "none" }, { - "serialNumber": "FDO2112189M", - "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Failed", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" + "serialNumber": "FDO211218AX", + "imageStaged": "none" + }, + { + "serialNumber": "FDO211218B5", + "imageStaged": "none" + }, + { + "serialNumber": "FDO211218FV", + "imageStaged": "Success" + }, + { + "serialNumber": "FDO211218GC", + "imageStaged": "Success" } ], "message": "" } }, - "test_image_mgmt_stage_00008a": { + "test_image_mgmt_stage_00005a": { "TEST_NOTES": [ - "Needed only for the 200 return code" + "FDO21120U5D: imageStaged == Success", + "FDO2112189M: imageStaged == Failed", + "FDO2112189M: requires deviceName, ipAddress, used in the fail_json message" ], "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "MESSAGE": "OK", "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - } - ], - "message": "" - } - }, - "test_image_mgmt_stage_00009a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO2112189M", - "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success" + }, + { + "serialNumber": "FDO2112189M", + "imageStaged": "Failed", + "deviceName": "cvd-2313-leaf", + "ipAddress": "172.22.150.108" } ], "message": "" } }, - "test_image_mgmt_stage_00010a": { + "test_image_mgmt_stage_00006a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1986,7 +675,7 @@ "ipAddress": "172.22.150.108", "issuAllowed": "", "statusPercent": 100, - "imageStagedPercent": 90, + "imageStagedPercent": 100, "validatedPercent": 100, "upgradePercent": 100, "modelType": 0, @@ -2007,11 +696,7 @@ "message": "" } }, - "test_image_mgmt_stage_00011a": { - "TEST_NOTES": [ - "FDO21120U5D imageStaged: Success", - "FDO2112189M imageStaged: In-Progress" - ], + "test_image_mgmt_stage_00007a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2065,7 +750,7 @@ "policy": "KR5M", "status": "In-Sync", "reason": "Upgrade", - "imageStaged": "In-Progress", + "imageStaged": "Failed", "validated": "Success", "upgrade": "Success", "upgGroups": "null", @@ -2080,7 +765,7 @@ "ipAddress": "172.22.150.108", "issuAllowed": "", "statusPercent": 100, - "imageStagedPercent": 50, + "imageStagedPercent": 100, "validatedPercent": 100, "upgradePercent": 100, "modelType": 0, @@ -2101,10 +786,36 @@ "message": "" } }, - "test_image_mgmt_stage_00012a": { + "test_image_mgmt_stage_00008a": { "TEST_NOTES": [ - "FDO21120U5D validated, upgrade, imageStaged: Success", - "FDO2112189M validated, upgrade, imageStaged: Success" + "RETURN_CODE == 200", + "DATA.lastOperDataObject.imageStaged == Success", + "DATA.lastOperDataObject.serialNumber is present" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success" + } + ], + "message": "" + } + }, + "test_image_mgmt_stage_00009a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "DATA.lastOperDataObject.serialNumber is present and is this specific value", + "DATA.lastOperDataObject.imageStaged == Success", + "DATA.lastOperDataObject.imageStagedPercent is present", + "DATA.lastOperDataObject.ipAddress is present", + "DATA.lastOperDataObject.deviceName is present", + "Entries for both serial numbers FDO21120U5D FDO2112189M are present" ], "RETURN_CODE": 200, "METHOD": "GET", @@ -2115,81 +826,115 @@ "lastOperDataObject": [ { "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", + "imageStagedPercent": 100, "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, + "deviceName": "leaf1" + }, + { + "deviceName": "cvd-2313-leaf", + "serialNumber": "FDO2112189M", + "imageStaged": "Success", "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" + "ipAddress": "172.22.150.108" + } + ], + "message": "" + } + }, + "test_image_mgmt_stage_00010a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "Entries for both serial numbers FDO21120U5D FDO2112189M are present", + "FDO21120U5D imageStaged == Success", + "FDO2112189M imageStage == Failed", + "DATA.lastOperDataObject.imageStagedPercent is present", + "DATA.lastOperDataObject.ipAddress is present", + "DATA.lastOperDataObject.deviceName is present" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "deviceName": "leaf1" }, { + "deviceName": "cvd-2313-leaf", "serialNumber": "FDO2112189M", + "imageStaged": "Failed", + "imageStagedPercent": 90, + "ipAddress": "172.22.150.108" + } + ], + "message": "" + } + }, + "test_image_mgmt_stage_00011a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "Entries for both serial numbers FDO21120U5D FDO2112189M are present", + "FDO21120U5D imageStaged == Success", + "FDO2112189M imageStage == In-Progress", + "DATA.lastOperDataObject.imageStagedPercent is present", + "DATA.lastOperDataObject.ipAddress is present", + "DATA.lastOperDataObject.deviceName is present" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "deviceName": "leaf1" + }, + { "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", + "serialNumber": "FDO2112189M", + "imageStaged": "In-Progress", + "imageStagedPercent": 90, + "ipAddress": "172.22.150.108" + } + ], + "message": "" + } + }, + "test_image_mgmt_stage_00012a": { + "TEST_NOTES": [ + "FDO21120U5D upgrade, validated, imageStaged == Success", + "FDO2112189M upgrade, validated, imageStaged == Success" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", "imageStaged": "Success", - "validated": "Success", "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" + "validated": "Success" + }, + { + "serialNumber": "FDO2112189M", + "imageStaged": "Success", + "upgrade": "Success", + "validated": "Success" } ], "message": "" @@ -2197,9 +942,9 @@ }, "test_image_mgmt_stage_00013a": { "TEST_NOTES": [ - "FDO21120U5D imageStaged: Success", - "FDO2112189M imageStaged: In-Progress", - "FDO2112189M imageStagedPercent: 50" + "FDO21120U5D upgrade, validated, imageStaged == Success", + "FDO2112189M upgrade, validated == Success", + "FDO2112189M imageStaged == In-Progress" ], "RETURN_CODE": 200, "METHOD": "GET", @@ -2210,81 +955,15 @@ "lastOperDataObject": [ { "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", "imageStaged": "Success", - "validated": "Success", "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" + "validated": "Success" }, { "serialNumber": "FDO2112189M", - "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", "imageStaged": "In-Progress", - "validated": "Success", "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 50, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" + "validated": "Success" } ], "message": "" From c120512bae87c1d4d3821f845af45733e3977817 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 11 Nov 2023 15:47:39 -1000 Subject: [PATCH 101/300] Make global var name unique --- .../dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py index 9e47434eb..c3b7b86f9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py @@ -127,7 +127,7 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: assert switch_details.serial_number == "FOX2109PGD1" -match = "Unable to retrieve switch information from the controller. " +match_00022 = "Unable to retrieve switch information from the controller." @pytest.mark.parametrize( @@ -136,11 +136,11 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: ("test_image_mgmt_switch_details_00022a", does_not_raise()), ( "test_image_mgmt_switch_details_00022b", - pytest.raises(AnsibleFailJson, match=match), + pytest.raises(AnsibleFailJson, match=match_00022), ), ( "test_image_mgmt_switch_details_00022c", - pytest.raises(AnsibleFailJson, match=match), + pytest.raises(AnsibleFailJson, match=match_00022), ), ], ) From 1525e7b4dfd866d0a9d7469e0a7140cc77c29718 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 07:43:29 -1000 Subject: [PATCH 102/300] Add copyright and attribution --- plugins/modules/dcnm_image_upgrade.py | 3 +- .../test_image_upgrade_ApiEndpoints.py | 22 ++++++++++++--- .../test_image_upgrade_ControllerVersion.py | 28 ++++++++++++++++--- .../test_image_upgrade_ImageInstallOptions.py | 27 +++++++++++++++--- .../test_image_upgrade_ImagePolicies.py | 27 +++++++++++++++--- .../test_image_upgrade_ImagePolicyAction.py | 27 +++++++++++++++--- .../test_image_upgrade_ImageStage.py | 27 +++++++++++++++--- .../test_image_upgrade_ImageUpgrade.py | 28 ++++++++++++++++--- .../test_image_upgrade_ImageUpgradeCommon.py | 27 +++++++++++++++--- .../test_image_upgrade_ImageValidate.py | 27 +++++++++++++++--- .../test_image_upgrade_SwitchDetails.py | 28 ++++++++++++++++--- ...e_upgrade_SwitchIssuDetailsByDeviceName.py | 28 ++++++++++++++++--- ...ge_upgrade_SwitchIssuDetailsByIpAddress.py | 28 ++++++++++++++++--- ...upgrade_SwitchIssuDetailsBySerialNumber.py | 28 ++++++++++++++++--- 14 files changed, 302 insertions(+), 53 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 80197a3c3..a0a3638eb 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -54,7 +54,8 @@ dcnm_send, validate_list_of_dicts) __metaclass__ = type -__author__ = "Cisco Systems, Inc." +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" DOCUMENTATION = """ --- diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py index 4dc33bd0e..381c141d0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py @@ -1,11 +1,25 @@ -""" -controller_version: 12 -description: Verify that class ApiEndpoints returns the correct API endpoints -""" +# Copyright (c) 2020-2024 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 from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + def test_image_mgmt_api_00001() -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py index 1fb362dc6..4a66b926a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py @@ -1,7 +1,18 @@ -""" -controller_version: 12 -description: Verify functionality of ControllerVersion -""" +# Copyright (c) 2020-2024 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 from typing import Any, Dict @@ -13,6 +24,15 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of ControllerVersion +""" + + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_common = patch_module_utils + "common." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index 93867b411..ffe70bbea 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -1,7 +1,18 @@ -""" -controller_version: 12 -description: Verify functionality of class ImageInstallOptions -""" +# Copyright (c) 2020-2024 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 from typing import Any, Dict @@ -15,6 +26,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of class ImageInstallOptions +""" + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py index a8fa5e0e4..67d451961 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py @@ -1,7 +1,18 @@ -""" -controller_version: 12 -description: Verify functionality of class ImagePolicies -""" +# Copyright (c) 2020-2024 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 from typing import Any, Dict @@ -15,6 +26,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of class ImagePolicies +""" + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index 05a3c7d6f..d696d2958 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -1,7 +1,18 @@ -""" -controller_version: 12 -Description: Verify functionality of ImagePolicyAction -""" +# Copyright (c) 2020-2024 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 from contextlib import contextmanager from typing import Any, Dict @@ -20,6 +31,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2023 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +Description: Verify functionality of ImagePolicyAction +""" + @contextmanager def does_not_raise(): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py index 72e83c612..9439cb801 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py @@ -1,7 +1,18 @@ -""" -controller_version: 12 -description: Verify functionality of ImageStage -""" +# Copyright (c) 2020-2024 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 from contextlib import contextmanager from typing import Any, Dict @@ -18,6 +29,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of ImageStage +""" + @contextmanager def does_not_raise(): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 89c9fa625..eed63c278 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -1,7 +1,19 @@ -""" -controller_version: 12 -description: Verify functionality of NdfcSwitchUpgrade -""" +# Copyright (c) 2020-2024 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 + from contextlib import contextmanager from typing import Any, Dict @@ -15,6 +27,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of NdfcSwitchUpgrade +""" + @contextmanager def does_not_raise(): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py index e8e1ac419..e25362e3e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py @@ -1,7 +1,18 @@ -""" -controller_version: 12 -description: Verify functionality of class ImageUpgradeCommon -""" +# Copyright (c) 2020-2024 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 from typing import Dict @@ -13,6 +24,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of class ImageUpgradeCommon +""" + class MockAnsibleModule: params = {} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index 6041fb522..d6c12986c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -1,7 +1,18 @@ -""" -controller_version: 12 -description: Verify functionality of ImageValidate -""" +# Copyright (c) 2020-2024 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 from typing import Any, Dict @@ -17,6 +28,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of ImageValidate +""" + patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py index c3b7b86f9..149c1e339 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py @@ -1,7 +1,19 @@ -""" -controller_version: 12 -description: Verify functionality of SwitchDetails -""" +# Copyright (c) 2020-2024 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 + from contextlib import contextmanager from typing import Any, Dict @@ -13,6 +25,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of SwitchDetails +""" + @contextmanager def does_not_raise(): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py index 155e577a2..561c87e2a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py @@ -1,7 +1,19 @@ -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsByDeviceName -""" +# Copyright (c) 2020-2024 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 + from contextlib import contextmanager from typing import Any, Dict @@ -13,6 +25,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsByDeviceName +""" + @contextmanager def does_not_raise(): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py index 075316d63..f33122c87 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py @@ -1,7 +1,19 @@ -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsByIpAddress -""" +# Copyright (c) 2020-2024 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 + from contextlib import contextmanager from typing import Any, Dict @@ -13,6 +25,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsByIpAddress +""" + @contextmanager def does_not_raise(): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py index 0ae4337ea..2c6ad1206 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py @@ -1,7 +1,19 @@ -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsBySerialNumber -""" +# Copyright (c) 2020-2024 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 + from contextlib import contextmanager from typing import Any, Dict @@ -13,6 +25,14 @@ from .fixture import load_fixture +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of subclass SwitchIssuDetailsBySerialNumber +""" + @contextmanager def does_not_raise(): From c45c9a033bc1cf5e0d8b5522a2108ffbf0fa431b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 07:44:55 -1000 Subject: [PATCH 103/300] Remove unused import --- .../test_image_upgrade_ImagePolicyAction.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index d696d2958..b463035a1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -24,14 +24,12 @@ ImagePolicies from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ ImagePolicyAction -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ - SwitchDetails from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture -__copyright__ = "Copyright (c) 2023 Cisco and/or its affiliates." +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" """ From b5457cbaec7edba440510aa664102b43119c6d08 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 09:18:37 -1000 Subject: [PATCH 104/300] Standardize test names, remove unused fixtures, more... image_upgrade_common.py - Changed self.debug to False - Changed logfile name to match collection name --- .../image_mgmt/image_upgrade_common.py | 4 +- ..._upgrade_responses_ImageUpgradeCommon.json | 119 ++++++- .../test_image_upgrade_ImageUpgradeCommon.py | 312 ++++++++++++++---- 3 files changed, 357 insertions(+), 78 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 841c14626..cd7d9c0ec 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -18,9 +18,9 @@ def __init__(self, module): self.module = module self.params = module.params - self.debug = True + self.debug = False self.fd = None - self.logfile = "/tmp/ndfc.log" + self.logfile = "/tmp/ansible_dcnm.log" self.module = module def _handle_response(self, response, verb): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json index 44b8c53c4..a11d46347 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json @@ -1,58 +1,149 @@ { - "mock_get_return_code_200_MESSAGE_OK": { + "test_image_mgmt_image_upgrade_common_00020a": { "RETURN_CODE": 200, + "METHOD": "DELETE", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "OK", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00020b": { + "RETURN_CODE": 400, + "METHOD": "DELETE", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "error: image not found", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00020c": { + "RETURN_CODE": 200, + "METHOD": "DELETE", + "REQUEST_PATH": "https://blah_noop", + "ERROR": "image not found", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00030a": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "OK", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00030b": { + "RETURN_CODE": 400, + "METHOD": "POST", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "error: image not found", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00030c": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://blah_noop", + "ERROR": "image not found", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00040a": { + "RETURN_CODE": 200, + "METHOD": "PUT", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "OK", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00040b": { + "RETURN_CODE": 400, + "METHOD": "PUT", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "error: image not found", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00040c": { + "RETURN_CODE": 200, + "METHOD": "PUT", + "REQUEST_PATH": "https://blah_noop", + "ERROR": "image not found", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00050a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "OK", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00050b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "not OK", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00050c": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "Not Found", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00050d": { + "RETURN_CODE": 500, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "mock_get_return_code_200_MESSAGE_not_OK": { + "test_image_mgmt_image_upgrade_common_00060a": { + "RETURN_CODE": 200, + "METHOD": "FOO", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "not OK", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00070a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://blah_noop", + "MESSAGE": "OK", + "DATA": {} + }, + "test_image_mgmt_image_upgrade_common_00070b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "not OK", "DATA": {} }, - "mock_get_return_code_404_MESSAGE_not_found": { + "test_image_mgmt_image_upgrade_common_00070c": { "RETURN_CODE": 404, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "Not Found", "DATA": {} }, - "mock_get_return_code_500_MESSAGE_OK": { + "test_image_mgmt_image_upgrade_common_00070d": { "RETURN_CODE": 500, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "mock_post_return_code_200_MESSAGE_OK": { + "test_image_mgmt_image_upgrade_common_00080a": { "RETURN_CODE": 200, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "mock_post_return_code_400_MESSAGE_NOT_OK": { + "test_image_mgmt_image_upgrade_common_00080b": { "RETURN_CODE": 400, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "error: image not found", "DATA": {} }, - "mock_post_return_code_200_ERROR_key_present": { + "test_image_mgmt_image_upgrade_common_00080c": { "RETURN_CODE": 200, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", "ERROR": "image not found", "DATA": {} - }, - "mock_unknown_response_verb": { - "RETURN_CODE": 200, - "METHOD": "FOO", - "REQUEST_PATH": "https://blah_noop", - "MESSAGE": "not OK", - "DATA": {} } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py index e25362e3e..df19210bf 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py @@ -14,6 +14,7 @@ from __future__ import absolute_import, division, print_function +from contextlib import contextmanager from typing import Dict import pytest @@ -33,6 +34,11 @@ """ +@contextmanager +def does_not_raise(): + yield + + class MockAnsibleModule: params = {} @@ -41,7 +47,7 @@ def fail_json(msg) -> dict: @pytest.fixture -def module(): +def image_upgrade_common(): return ImageUpgradeCommon(MockAnsibleModule) @@ -53,38 +59,63 @@ def responses_image_upgrade_common(key: str) -> Dict[str, str]: return {"response": response, "verb": verb} -def test_init_(module) -> None: +def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: """ - __init__ sets expected values + Function + - __init__ + + Test + - fail_json is not called + - image_upgrade_common.params is a dict + - image_upgrade_common.debug is False + - image_upgrade_common.fd is None + - image_upgrade_common.logfile is /tmp/ansible_dcnm.log """ - module.__init__(MockAnsibleModule) - assert module.params == {} - assert module.debug == True - assert module.fd == None - assert module.logfile == "/tmp/ndfc.log" + with does_not_raise(): + image_upgrade_common.__init__(MockAnsibleModule) + assert image_upgrade_common.params == {} + assert image_upgrade_common.debug == False + assert image_upgrade_common.fd == None + assert image_upgrade_common.logfile == "/tmp/ansible_dcnm.log" @pytest.mark.parametrize( "key, expected", [ - ("mock_post_return_code_200_MESSAGE_OK", {"success": True, "changed": True}), ( - "mock_post_return_code_400_MESSAGE_NOT_OK", + "test_image_mgmt_image_upgrade_common_00020a", + {"success": True, "changed": True}, + ), + ( + "test_image_mgmt_image_upgrade_common_00020b", {"success": False, "changed": False}, ), ( - "mock_post_return_code_200_ERROR_key_present", + "test_image_mgmt_image_upgrade_common_00020c", {"success": False, "changed": False}, ), ], ) -def test_handle_response_post(module, key, expected) -> None: +def test_image_mgmt_image_upgrade_common_00020( + image_upgrade_common, key, expected +) -> None: """ - verify _handle_reponse() return values for 200/OK response - to POST request + Function + - _handle_response + + Test + - json_fail is not called + - success and changed are returned as expected for DELETE requests + + Description + _handle_reponse() calls either _handle_get_reponse if verb is "GET" or + _handle_post_put_delete_response if verb is "DELETE", "POST", or "PUT" """ data = responses_image_upgrade_common(key) - result = module._handle_response(data.get("response"), data.get("verb")) + with does_not_raise(): + result = image_upgrade_common._handle_response( + data.get("response"), data.get("verb") + ) assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") @@ -92,54 +123,172 @@ def test_handle_response_post(module, key, expected) -> None: @pytest.mark.parametrize( "key, expected", [ - ("mock_get_return_code_200_MESSAGE_OK", {"success": True, "found": True}), - ("mock_get_return_code_200_MESSAGE_not_OK", {"success": False, "found": False}), ( - "mock_get_return_code_404_MESSAGE_not_found", + "test_image_mgmt_image_upgrade_common_00030a", + {"success": True, "changed": True}, + ), + ( + "test_image_mgmt_image_upgrade_common_00030b", + {"success": False, "changed": False}, + ), + ( + "test_image_mgmt_image_upgrade_common_00030c", + {"success": False, "changed": False}, + ), + ], +) +def test_image_mgmt_image_upgrade_common_00030( + image_upgrade_common, key, expected +) -> None: + """ + Function + - _handle_response + + Test + - json_fail is not called + - success and changed are returned as expected for POST requests + + Description + _handle_reponse() calls either _handle_get_reponse if verb is "GET" or + _handle_post_put_delete_response if verb is "DELETE", "POST", or "PUT" + """ + data = responses_image_upgrade_common(key) + with does_not_raise(): + result = image_upgrade_common._handle_response( + data.get("response"), data.get("verb") + ) + assert result.get("success") == expected.get("success") + assert result.get("changed") == expected.get("changed") + + +@pytest.mark.parametrize( + "key, expected", + [ + ( + "test_image_mgmt_image_upgrade_common_00040a", + {"success": True, "changed": True}, + ), + ( + "test_image_mgmt_image_upgrade_common_00040b", + {"success": False, "changed": False}, + ), + ( + "test_image_mgmt_image_upgrade_common_00040c", + {"success": False, "changed": False}, + ), + ], +) +def test_image_mgmt_image_upgrade_common_00040( + image_upgrade_common, key, expected +) -> None: + """ + Function + - _handle_response + + Test + - json_fail is not called + - success and changed are returned as expected for PUT requests + + Description + _handle_reponse() calls either _handle_get_reponse if verb is "GET" or + _handle_post_put_delete_response if verb is "DELETE", "POST", or "PUT" + """ + data = responses_image_upgrade_common(key) + with does_not_raise(): + result = image_upgrade_common._handle_response( + data.get("response"), data.get("verb") + ) + assert result.get("success") == expected.get("success") + assert result.get("changed") == expected.get("changed") + + +@pytest.mark.parametrize( + "key, expected", + [ + ( + "test_image_mgmt_image_upgrade_common_00050a", + {"success": True, "found": True}, + ), + ( + "test_image_mgmt_image_upgrade_common_00050b", + {"success": False, "found": False}, + ), + ( + "test_image_mgmt_image_upgrade_common_00050c", {"success": True, "found": False}, ), - ("mock_get_return_code_500_MESSAGE_OK", {"success": False, "found": False}), + ( + "test_image_mgmt_image_upgrade_common_00050d", + {"success": False, "found": False}, + ), ], ) -def test_handle_response_get(module, key, expected) -> None: +def test_image_mgmt_image_upgrade_common_00050( + image_upgrade_common, key, expected +) -> None: """ - verify _handle_reponse() return values for GET requests + Function + - _handle_response + + Test + - _handle_reponse returns expected values for GET requests """ data = responses_image_upgrade_common(key) - result = module._handle_response(data.get("response"), data.get("verb")) + result = image_upgrade_common._handle_response( + data.get("response"), data.get("verb") + ) assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") -def test_handle_response_unknown_response_verb(module) -> None: +def test_image_mgmt_image_upgrade_common_00060(image_upgrade_common) -> None: """ - verify that fail_json() is called if a unknown request verb is provided + Function + - _handle_response + + Test + - fail_json is called because an unknown request verb is provided """ - data = responses_image_upgrade_common("mock_unknown_response_verb") + data = responses_image_upgrade_common("test_image_mgmt_image_upgrade_common_00060a") with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): - module._handle_response(data.get("response"), data.get("verb")) + image_upgrade_common._handle_response(data.get("response"), data.get("verb")) @pytest.mark.parametrize( "key, expected", [ - ("mock_get_return_code_200_MESSAGE_OK", {"success": True, "found": True}), - ("mock_get_return_code_200_MESSAGE_not_OK", {"success": False, "found": False}), ( - "mock_get_return_code_404_MESSAGE_not_found", + "test_image_mgmt_image_upgrade_common_00070a", + {"success": True, "found": True}, + ), + ( + "test_image_mgmt_image_upgrade_common_00070b", + {"success": False, "found": False}, + ), + ( + "test_image_mgmt_image_upgrade_common_00070c", {"success": True, "found": False}, ), - ("mock_get_return_code_500_MESSAGE_OK", {"success": False, "found": False}), + ( + "test_image_mgmt_image_upgrade_common_00070d", + {"success": False, "found": False}, + ), ], ) -def test_handle_get_response(module, key, expected) -> None: +def test_image_mgmt_image_upgrade_common_00070( + image_upgrade_common, key, expected +) -> None: """ - verify _handle_get_reponse() return values for GET requests + Function + - _handle_get_response - NOTE: Adding this test increases coverage by 2% according to pytest-cov + Test + - fail_json is not called + - _handle_get_reponse() returns expected values for GET requests """ data = responses_image_upgrade_common(key) - result = module._handle_get_response(data.get("response")) + with does_not_raise(): + result = image_upgrade_common._handle_get_response(data.get("response")) assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") @@ -148,25 +297,36 @@ def test_handle_get_response(module, key, expected) -> None: @pytest.mark.parametrize( "key, expected", [ - ("mock_post_return_code_200_MESSAGE_OK", {"success": True, "changed": True}), ( - "mock_post_return_code_400_MESSAGE_NOT_OK", + "test_image_mgmt_image_upgrade_common_00080a", + {"success": True, "changed": True}, + ), + ( + "test_image_mgmt_image_upgrade_common_00080b", {"success": False, "changed": False}, ), ( - "mock_post_return_code_200_ERROR_key_present", + "test_image_mgmt_image_upgrade_common_00080c", {"success": False, "changed": False}, ), ], ) -def test_handle_post_put_delete_response(module, key, expected) -> None: +def test_image_mgmt_image_upgrade_common_00080( + image_upgrade_common, key, expected +) -> None: """ - _handle_post_put_delete_response() return expected values for POST requests - NOTE: This method is covered in test_handle_response_post() above, but... - NOTE: Adding this test increases coverage by 2% according to pytest-cov + Function + - _handle_post_put_delete_response + + Test + - return expected values for POST requests + - fail_json is not called """ data = responses_image_upgrade_common(key) - result = module._handle_post_put_delete_response(data.get("response")) + with does_not_raise(): + result = image_upgrade_common._handle_post_put_delete_response( + data.get("response") + ) assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") @@ -191,11 +351,17 @@ def test_handle_post_put_delete_response(module, key, expected) -> None: ([1, 2, "3"], [1, 2, "3"]), ], ) -def test_make_boolean(module, key, expected) -> None: +def test_image_mgmt_image_upgrade_common_00090( + image_upgrade_common, key, expected +) -> None: """ - verify that make_boolean() returns expected values for all cases + Function + - make_boolean + + Test + - expected values are returned for all cases """ - assert module.make_boolean(key) == expected + assert image_upgrade_common.make_boolean(key) == expected @pytest.mark.parametrize( @@ -218,49 +384,71 @@ def test_make_boolean(module, key, expected) -> None: ([1, 2, "3"], [1, 2, "3"]), ], ) -def test_make_none(module, key, expected) -> None: +def test_image_mgmt_image_upgrade_common_00100( + image_upgrade_common, key, expected +) -> None: """ - verify that make_none() returns expected values for all cases + Function + - make_none + + Test + - expected values are returned for all cases """ - assert module.make_none(key) == expected + assert image_upgrade_common.make_none(key) == expected -def test_log_msg_disabled(module) -> None: +def test_image_mgmt_image_upgrade_common_00110(image_upgrade_common) -> None: """ - verify that make_none() returns expected values for all cases + Function + - log_msg + + Test + - log_msg returns None when debug is False """ ERROR_MESSAGE = "This is an error message" - module.debug = False - assert module.log_msg(ERROR_MESSAGE) == None + image_upgrade_common.debug = False + assert image_upgrade_common.log_msg(ERROR_MESSAGE) == None -def test_log_msg_enabled(tmp_path, module) -> None: +def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) -> None: """ - verify that make_none() returns expected values for all cases + Function + - log_msg + + Test + - log_msg writes to the logfile when debug is True """ directory = tmp_path / "test_log_msg" directory.mkdir() filename = directory / f"test_log_msg.txt" ERROR_MESSAGE = "This is an error message" - module.debug = True - module.logfile = filename - module.log_msg(ERROR_MESSAGE) + image_upgrade_common.debug = True + image_upgrade_common.logfile = filename + image_upgrade_common.log_msg(ERROR_MESSAGE) assert filename.read_text(encoding="UTF-8") == ERROR_MESSAGE + "\n" assert len(list(tmp_path.iterdir())) == 1 -def test_log_msg_enabled_fail_json(tmp_path, module) -> None: +def test_image_mgmt_image_upgrade_common_00112(tmp_path, image_upgrade_common) -> None: """ - log_msg() calls fail_json() if the logfile cannot be opened + Function + - log_msg + + Test + - log_msg calls fail_json if the logfile cannot be opened + + Description + To ensure an error is generated, we attempt a write to a filename + that is too long for the target OS. """ directory = tmp_path / "test_log_msg" directory.mkdir() filename = directory / f"test_{'a' * 2000}_log_msg.txt" ERROR_MESSAGE = "This is an error message" - module.debug = True - module.logfile = filename + image_upgrade_common.debug = True + image_upgrade_common.logfile = filename with pytest.raises(AnsibleFailJson, match=r"error opening logfile"): - module.log_msg(ERROR_MESSAGE) + image_upgrade_common.log_msg(ERROR_MESSAGE) From 127112df1b816c5762547517dd7bec4dc68d55b0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 09:40:58 -1000 Subject: [PATCH 105/300] Add TEST_NOTES to _handle_delete_post_put_response fixtures --- .../image_upgrade_responses_ImageUpgradeCommon.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json index a11d46347..55570b080 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json @@ -126,6 +126,10 @@ "DATA": {} }, "test_image_mgmt_image_upgrade_common_00080a": { + "TEST_NOTES": [ + "RETURN_CODE does not matter", + "_handle_delete_post_put_response() considers only the MESSAGE field" + ], "RETURN_CODE": 200, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", @@ -133,6 +137,10 @@ "DATA": {} }, "test_image_mgmt_image_upgrade_common_00080b": { + "TEST_NOTES": [ + "RETURN_CODE does not matter", + "_handle_delete_post_put_response() considers only the MESSAGE field" + ], "RETURN_CODE": 400, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", @@ -140,6 +148,10 @@ "DATA": {} }, "test_image_mgmt_image_upgrade_common_00080c": { + "TEST_NOTES": [ + "RETURN_CODE does not matter", + "_handle_delete_post_put_response() considers only the MESSAGE field" + ], "RETURN_CODE": 200, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", From e51874f1ccad689573330ca9952954bcb21e1b18 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 10:47:01 -1000 Subject: [PATCH 106/300] Remove unused file --- .../module_utils/image_mgmt/api_endpoints.py | 70 +- .../module_utils/image_mgmt/image_policies.py | 6 +- plugins/modules/dcnm_image_upgrade_orig.py | 5306 ----------------- 3 files changed, 65 insertions(+), 5317 deletions(-) delete mode 100644 plugins/modules/dcnm_image_upgrade_orig.py diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py index 43700a093..70c1fc0ac 100644 --- a/plugins/module_utils/image_mgmt/api_endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -1,7 +1,11 @@ +""" +Endpoints for image management API calls +""" class ApiEndpoints: """ Endpoints for image management API calls """ + def __init__(self): self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" @@ -21,12 +25,15 @@ def __init__(self): self.endpoint_policy_mgnt += "/rest/policymgnt" self.endpoint_staging_management = f"{self.endpoint_image_management}" - self.endpoint_staging_management += "/rest/stagingmanagement" - + self.endpoint_staging_management += "/rest/stagingmanagement" + @property def bootflash_info(self): + """ + return endpoint GET /rest/imagemgnt/bootFlash + """ path = f"{self.endpoint_image_management}/rest/imagemgnt/bootFlash" - path += f"/bootflash-info" + path += "/bootflash-info" endpoint = {} endpoint["path"] = path endpoint["verb"] = "GET" @@ -34,6 +41,9 @@ def bootflash_info(self): @property def install_options(self): + """ + return endpoint POST /rest/imageupgrade/install-options + """ path = f"{self.endpoint_image_upgrade}/install-options" endpoint = {} endpoint["path"] = path @@ -42,6 +52,9 @@ def install_options(self): @property def image_stage(self): + """ + return endpoint POST /rest/stagingmanagement/stage-image + """ path = f"{self.endpoint_staging_management}/stage-image" endpoint = {} endpoint["path"] = path @@ -50,6 +63,9 @@ def image_stage(self): @property def image_upgrade(self): + """ + return endpoint POST /rest/imageupgrade/upgrade-image + """ path = f"{self.endpoint_image_upgrade}/upgrade-image" endpoint = {} endpoint["path"] = path @@ -58,6 +74,9 @@ def image_upgrade(self): @property def image_validate(self): + """ + return endpoint POST /rest/stagingmanagement/validate-image + """ path = f"{self.endpoint_staging_management}/validate-image" endpoint = {} endpoint["path"] = path @@ -66,14 +85,20 @@ def image_validate(self): @property def issu_info(self): + """ + return endpoint GET /rest/packagemgnt/issu + """ path = f"{self.endpoint_package_mgnt}/issu" endpoint = {} endpoint["path"] = path endpoint["verb"] = "GET" return endpoint - + @property def controller_version(self): + """ + return endpoint GET /appcenter/cisco/ndfc/api/v1/fm/about/version + """ path = f"{self.endpoint_feature_manager}/about/version" endpoint = {} endpoint["path"] = path @@ -82,14 +107,20 @@ def controller_version(self): @property def policies_attached_info(self): + """ + return endpoint GET /rest/policymgnt/all-attached-policies + """ path = f"{self.endpoint_policy_mgnt}/all-attached-policies" endpoint = {} endpoint["path"] = path endpoint["verb"] = "GET" return endpoint - + @property def policies_info(self): + """ + return endpoint GET /rest/policymgnt/policies + """ path = f"{self.endpoint_policy_mgnt}/policies" endpoint = {} endpoint["path"] = path @@ -98,6 +129,9 @@ def policies_info(self): @property def policy_attach(self): + """ + return endpoint POST /rest/policymgnt/attach-policy + """ path = f"{self.endpoint_policy_mgnt}/attach-policy" endpoint = {} endpoint["path"] = path @@ -106,6 +140,9 @@ def policy_attach(self): @property def policy_create(self): + """ + return endpoint POST /rest/policymgnt/platform-policy + """ path = f"{self.endpoint_policy_mgnt}/platform-policy" endpoint = {} endpoint["path"] = path @@ -114,32 +151,45 @@ def policy_create(self): @property def policy_detach(self): + """ + return endpoint DELETE /rest/policymgnt/detach-policy + """ path = f"{self.endpoint_policy_mgnt}/detach-policy" endpoint = {} endpoint["path"] = path endpoint["verb"] = "DELETE" return endpoint - + @property def policy_info(self): - # Replace __POLICY_NAME__ with the policy_name to query - # e.g. path.replace("__POLICY_NAME__", "NR1F") + """ + return endpoint GET /rest/policymgnt/image-policy/__POLICY_NAME__ + + Replace __POLICY_NAME__ with the policy_name to query + e.g. path.replace("__POLICY_NAME__", "NR1F") + """ path = f"{self.endpoint_policy_mgnt}/image-policy/__POLICY_NAME__" endpoint = {} endpoint["path"] = path endpoint["verb"] = "GET" return endpoint - + @property def stage_info(self): + """ + return endpoint GET /rest/stagingmanagement/stage-info + """ path = f"{self.endpoint_staging_management}/stage-info" endpoint = {} endpoint["path"] = path endpoint["verb"] = "GET" return endpoint - + @property def switches_info(self): + """ + return endpoint GET /rest/inventory/allswitches + """ path = f"{self.endpoint_lan_fabric}/rest/inventory/allswitches" endpoint = {} endpoint["path"] = path diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 9b4e5fa5b..7462922f7 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -1,3 +1,7 @@ +""" + Retrieve image policy details from the controller and provide + property accessors for the policy attributes. +""" import inspect from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -94,7 +98,7 @@ def _get(self, item): if self.policy_name is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"instance.policy_name must be set before " + msg += "instance.policy_name must be set before " msg += f"accessing property {item}." self.module.fail_json(msg) diff --git a/plugins/modules/dcnm_image_upgrade_orig.py b/plugins/modules/dcnm_image_upgrade_orig.py deleted file mode 100644 index eaf1f912b..000000000 --- a/plugins/modules/dcnm_image_upgrade_orig.py +++ /dev/null @@ -1,5306 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2020-2024 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. -""" -Classes and methods for Ansible support of Nexus image upgrade. - -Ansible states "merged", "deleted", and "query" are implemented. - -merged: attach image policy to one or more devices -deleted: delete image policy from one or more devices -query: return image policy details for one or more devices -""" -from __future__ import absolute_import, division, print_function - -import copy -import json -from time import sleep - -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, -) - -__metaclass__ = type -__author__ = "Cisco Systems, Inc." - -DOCUMENTATION = """ ---- -module: dcnm_image_upgrade -short_description: Attach, detach, and query device image policies. -version_added: "0.9.0" -description: - - Attach, detach, and query device image policies. -author: Cisco Systems, Inc. -options: - state: - description: - - The state of DCNM after module completion. - - I(merged) and I(query) are the only states supported. - type: str - choices: - - merged - - deleted - - query - default: merged - config: - description: - - A dictionary containing the image policy configuration. - type: dict - suboptions: - policy: - description: - - Image policy name - type: str - required: true - default: False - stage: - description: - - Stage (True) or unstage (False) an image policy - type: bool - required: false - default: True - validate: - description: - - Validate (True) or do not validate (False) the image - - after staging - type: bool - required: false - default: True - reboot: - description: - - Reboot the switch after upgrade - type: bool - required: false - default: False - upgrade: - description: - - A dictionary containing upgrade toggles for nxos and epld - type: dict - suboptions: - nxos: - description: - - Enable (True) or disable (False) image upgrade - type: bool - required: false - default: True - epld: - description: - - Enable (True) or disable (False) EPLD upgrade - - If upgrade.nxos is false, epld and packages cannot both be true - - If epld is true, nxos_option must be disruptive - type: bool - required: false - default: False - options: - description: - - A dictionary containing options for each of the upgrade types - type: dict - suboptions: - nxos: - description: - - A dictionary containing nxos upgrade options - type: dict - suboptions: - mode: - description: - - nxos upgrade mode - - Choose between distruptive, non_disruptive, force_non_disruptive - type: string - required: false - default: distruptive - bios_force: - description: - - Force BIOS upgrade - type: bool - required: false - default: False - epld: - description: - - A dictionary containing epld upgrade options - type: dict - suboptions: - module: - description: - - The switch module to upgrade - - Choose between ALL, or integer values - type: string - required: false - default: ALL - golden: - description: - - Enable (True) or disable (False) reverting to the golden EPLD image - type: bool - required: false - default: False - reboot: - description: - - A dictionary containing reboot options - type: dict - suboptions: - config_reload: - description: - - Reload the configuration - type: bool - required: false - default: False - write_erase: - description: - - Erase the startup configuration - type: bool - required: false - default: False - package: - description: - - A dictionary containing package upgrade options - type: dict - suboptions: - install: - description: - - Install the package - type: bool - required: false - default: False - uninstall: - description: - - Uninstall the package - type: bool - required: false - default: False - switches: - description: - - A list of devices to attach the image policy to. - type: list - elements: dict - required: true - suboptions: - ip_address: - description: - - The IP address of the device to which the policy will be attached. - type: str - required: true - policy: - description: - - Image policy name - type: str - required: true - default: False - stage: - description: - - Stage (True) or unstage (False) an image policy - type: bool - required: false - default: True - validate: - description: - - Validate (True) or do not validate (False) the image - - after staging - type: bool - required: false - default: True - reboot: - description: - - Reboot the switch after upgrade - type: bool - required: false - default: False - upgrade: - description: - - A dictionary containing upgrade toggles for nxos and epld - type: dict - suboptions: - nxos: - description: - - Enable (True) or disable (False) image upgrade - type: bool - required: false - default: True - epld: - description: - - Enable (True) or disable (False) EPLD upgrade - - If upgrade.nxos is false, epld and packages cannot both be true - - If epld is true, nxos_option must be disruptive - type: bool - required: false - default: False - options: - description: - - A dictionary containing options for each of the upgrade types - type: dict - suboptions: - nxos: - description: - - A dictionary containing nxos upgrade options - type: dict - suboptions: - mode: - description: - - nxos upgrade mode - - Choose between distruptive, non_disruptive, force_non_disruptive - type: string - required: false - default: distruptive - bios_force: - description: - - Force BIOS upgrade - type: bool - required: false - default: False - epld: - description: - - A dictionary containing epld upgrade options - type: dict - suboptions: - module: - description: - - The switch module to upgrade - - Choose between ALL, or integer values - type: string - required: false - default: ALL - golden: - description: - - Enable (True) or disable (False) reverting to the golden EPLD image - type: bool - required: false - default: False - reboot: - description: - - A dictionary containing reboot options - type: dict - suboptions: - config_reload: - description: - - Reload the configuration - type: bool - required: false - default: False - write_erase: - description: - - Erase the startup configuration - type: bool - required: false - default: False - package: - description: - - A dictionary containing package upgrade options - type: dict - suboptions: - install: - description: - - Install the package - type: bool - required: false - default: False - uninstall: - description: - - Uninstall the package - type: bool - required: false - default: False - -""" - -EXAMPLES = """ -# This module supports the following states: -# -# merged: -# Attach image policy to one or more devices. -# -# query: -# Return image policy details for one or more devices. -# -# deleted: -# Delete image policy from one or more devices -# - -# Attach image policy NR3F to two devices -# Stage and validate the image on two devices but do not upgrade - - name: stage/validate images - cisco.dcnm.dcnm_image_upgrade: - state: merged - config: - policy: NR3F - stage: true - validate: true - upgrade: - nxos: false - epld: false - switches: - - ip_address: 192.168.1.1 - - ip_address: 192.168.1.2 - -# Attach image policy NR1F to device 192.168.1.1 -# Attach image policy NR2F to device 192.168.1.2 -# Stage the image on device 192.168.1.1, but do not upgrade -# Stage the image and upgrade device 192.168.1.2 - - name: stage/upgrade devices - cisco.dcnm.dcnm_image_upgrade: - state: merged - config: - validate: false - stage: false - upgrade: - nxos: false - epld: false - options: - nxos: - type: disruptive - epld: - module: ALL - golden: false - switches: - - ip_address: 192.168.1.1 - policy: NR1F - stage: true - validate: true - upgrade: - nxos: true - epld: false - - ip_address: 192.168.1.2 - policy: NR2F - stage: true - validate: true - upgrade: - nxos: true - epld: true - options: - nxos: - type: disruptive - epld: - module: ALL - golden: false - -# Detach image policy NR3F from two devices - - name: stage/upgrade devices - cisco.dcnm.dcnm_image_upgrade: - state: deleted - config: - policy: NR3F - switches: - - ip_address: 192.168.1.1 - - ip_address: 192.168.1.2 - -""" -class ApiEndpoints: - """ - Endpoints for NDFC API calls - """ - def __init__(self): - self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" - - self.endpoint_feature_manager = f"{self.endpoint_api_v1}/fm" - self.endpoint_lan_fabric = f"{self.endpoint_api_v1}/lan-fabric" - - self.endpoint_image_management = f"{self.endpoint_api_v1}" - self.endpoint_image_management += "/imagemanagement" - - self.endpoint_image_upgrade = f"{self.endpoint_image_management}" - self.endpoint_image_upgrade += "/rest/imageupgrade" - - self.endpoint_package_mgnt = f"{self.endpoint_image_management}" - self.endpoint_package_mgnt += "/rest/packagemgnt" - - self.endpoint_policy_mgnt = f"{self.endpoint_image_management}" - self.endpoint_policy_mgnt += "/rest/policymgnt" - - self.endpoint_staging_management = f"{self.endpoint_image_management}" - self.endpoint_staging_management += "/rest/stagingmanagement" - - @property - def bootflash_info(self): - path = f"{self.endpoint_image_management}/rest/imagemgnt/bootFlash" - path += f"/bootflash-info" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def install_options(self): - path = f"{self.endpoint_image_upgrade}/install-options" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def image_stage(self): - path = f"{self.endpoint_staging_management}/stage-image" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def image_upgrade(self): - path = f"{self.endpoint_image_upgrade}/upgrade-image" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def image_validate(self): - path = f"{self.endpoint_staging_management}/validate-image" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def issu_info(self): - path = f"{self.endpoint_package_mgnt}/issu" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def controller_version(self): - path = f"{self.endpoint_feature_manager}/about/version" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def policies_attached_info(self): - path = f"{self.endpoint_policy_mgnt}/all-attached-policies" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def policies_info(self): - path = f"{self.endpoint_policy_mgnt}/policies" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def policy_attach(self): - path = f"{self.endpoint_policy_mgnt}/attach-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def policy_create(self): - path = f"{self.endpoint_policy_mgnt}/platform-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def policy_detach(self): - path = f"{self.endpoint_policy_mgnt}/detach-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "DELETE" - return endpoint - - @property - def policy_info(self): - # Replace __POLICY_NAME__ with the policy_name to query - # e.g. path.replace("__POLICY_NAME__", "NR1F") - path = f"{self.endpoint_policy_mgnt}/image-policy/__POLICY_NAME__" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def stage_info(self): - path = f"{self.endpoint_staging_management}/stage-info" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def switches_info(self): - path = f"{self.endpoint_lan_fabric}/rest/inventory/allswitches" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - -class ImageUpgradeCommon: - """ - Base class for the other classes in this file - """ - - def __init__(self, module): - self.module = module - self.params = module.params - self.debug = True - self.fd = None - self.logfile = "/tmp/dcnm_image_upgrade.log" - self.endpoints = ApiEndpoints() - - - def _handle_response(self, response, verb): - if verb == "GET": - return self._handle_get_response(response) - if verb in {"POST", "PUT", "DELETE"}: - return self._handle_post_put_delete_response(response) - return self._handle_unknown_request_verbs(response, verb) - - def _handle_unknown_request_verbs(self, response, verb): - msg = f"Unknown request verb ({verb}) in _handle_response() for response {response}." - self.module.fail_json(msg) - - def _handle_get_response(self, response): - """ - Caller: - - self._handle_response() - Handle NDFC responses to GET requests - Returns: dict() with the following keys: - - found: - - False, if request error was "Not found" and RETURN_CODE == 404 - - True otherwise - - success: - - False if RETURN_CODE != 200 or MESSAGE != "OK" - - True otherwise - """ - result = {} - success_return_codes = {200, 404} - if ( - response.get("RETURN_CODE") == 404 - and response.get("MESSAGE") == "Not Found" - ): - result["found"] = False - result["success"] = True - return result - if ( - response.get("RETURN_CODE") not in success_return_codes - or response.get("MESSAGE") != "OK" - ): - result["found"] = False - result["success"] = False - return result - result["found"] = True - result["success"] = True - return result - - def _handle_post_put_delete_response(self, response): - """ - Caller: - - self.self._handle_response() - - Handle POST, PUT responses from NDFC. - - Returns: dict() with the following keys: - - changed: - - True if changes were made to NDFC - - False otherwise - - success: - - False if RETURN_CODE != 200 or MESSAGE != "OK" - - True otherwise - """ - result = {} - if response.get("ERROR") is not None: - result["success"] = False - result["changed"] = False - return result - if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: - result["success"] = False - result["changed"] = False - return result - result["success"] = True - result["changed"] = True - return result - - def log_msg(self, msg): - """ - used for debugging. disable this when committing to main - by setting __init__().debug to False - """ - if self.debug is False: - return - if self.fd is None: - try: - self.fd = open(f"{self.logfile}", "a+", encoding="UTF-8") - except IOError as err: - msg = f"error opening logfile {self.logfile}. " - msg += f"detail: {err}" - self.module.fail_json(msg) - - self.fd.write(msg) - self.fd.write("\n") - self.fd.flush() - - def make_boolean(self, value): - """ - Return value converted to boolean, if possible. - Return value, if value cannot be converted. - """ - if isinstance(value, bool): - return value - if isinstance(value, str): - if value.lower() in ["true", "yes"]: - return True - if value.lower() in ["false", "no"]: - return False - return value - - def make_none(self, value): - """ - Return None if value is an empty string, or a string - representation of a None type - Return value otherwise - """ - if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: - return None - return value - - -class ImageUpgradeTask(ImageUpgradeCommon): - """ - Ansible support for image policy attach, detach, and query. - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self.log_msg(f"{self.class_name}.__init__") - # populated in self._build_policy_attach_payload() - self.payloads = [] - - self.config = module.params.get("config") - self.log_msg(f"{self.class_name}.__init__() self.config {self.config}") - if not isinstance(self.config, dict): - msg = "expected dict type for self.config. " - msg = +f"got {type(self.config).__name__}" - self.module.fail_json(msg) - - self.check_mode = False - self.validated = [] - self.have_create = [] - self.want_create = [] - self.need = [] - self.diff_save = {} - self.query = [] - self.result = dict(changed=False, diff=[], response=[]) - - self.mandatory_global_keys = {"switches"} - self.mandatory_switch_keys = {"ip_address"} - - if not self.mandatory_global_keys.issubset(self.config): - msg = f"{self.class_name}.__init__: " - msg += "Missing mandatory key(s) in playbook global config. " - msg += f"expected {self.mandatory_global_keys}, " - msg += f"got {self.config.keys()}" - self.module.fail_json(msg) - - if self.config["switches"] is None: - msg = f"{self.class_name}.__init__: " - msg += "missing list of switches in playbook config." - self.module.fail_json(msg) - - for switch in self.config["switches"]: - if not self.mandatory_switch_keys.issubset(switch): - msg = f"{self.class_name}.__init__: " - msg += f"missing mandatory key(s) in playbook switch config. " - msg += f"expected {self.mandatory_switch_keys}, " - msg += f"got {switch.keys()}" - self.module.fail_json(msg) - - self.log_msg(f"{self.class_name}.__init__: instantiate SwitchDetails") - self.switch_details = SwitchDetails(self.module) - self.log_msg(f"{self.class_name}.__init__: instantiate ImagePolicies") - self.image_policies = ImagePolicies(self.module) - - def get_have(self): - """ - Caller: main() - - Determine current switch ISSU state on NDFC - """ - self.have = SwitchIssuDetailsByIpAddress(self.module) - self.have.refresh() - - def get_want(self): - """ - Caller: main() - - Update self.want_create for all switches defined in the playbook - """ - self._merge_global_and_switch_configs(self.config) - self._validate_switch_configs() - if not self.switch_configs: - return - self.log_msg(f"{self.class_name}.get_want() self.switch_configs: {self.switch_configs}") - self.want_create = self.switch_configs - - def _get_idempotent_want(self, want): - """ - Return an itempotent want item based on the have item contents. - - The have item is obtained from an instance of SwitchIssuDetails - created in self.get_have(). - - want structure passed to this method: - - { - 'policy': 'KR3F', - 'stage': True, - 'upgrade': { - 'nxos': True, - 'epld': False - }, - 'options': { - 'nxos': { - 'mode': 'non_disruptive' - 'bios_force': False - }, - 'epld': { - 'module': 'ALL', - 'golden': False - } - }, - 'validate': True, - 'ip_address': '172.22.150.102' - } - - The returned idempotent_want structure is identical to the - above structure, except that the policy_changed key is added, - and values are modified based on results from the have item, - and the information returned by ImageInstallOptions. - - Caller: self.get_need_merged() - """ - self.log_msg(f"{self.class_name}._get_idempotent_want() want: {want}") - self.have.ip_address = want["ip_address"] - - want["policy_changed"] = True - # In NDFC, the switch does not have an image policy attached - # Return the want item as-is with policy_changed = True - if self.have.serial_number is None: - self.log_msg(f"{self.class_name}._get_idempotent_want() no serial_number. returning want: {want}") - return want - # The switch has an image policy attached which is - # different from the want policy. - # Return the want item as-is with policy_changed = True - if want["policy"] != self.have.policy: - self.log_msg(f"{self.class_name}._get_idempotent_want() different policy attached. returning want: {want}") - return want - - # start with a copy of the want item - idempotent_want = copy.deepcopy(want) - # Give an indication to the caller that the policy has not changed - # We can use this later to determine if we need to do anything in - # the case where the image is already staged and/or upgraded. - idempotent_want["policy_changed"] = False - - # if the image is already staged, don't stage it again - if self.have.image_staged == "Success": - idempotent_want["stage"] = False - # if the image is already validated, don't validate it again - if self.have.validated == "Success": - idempotent_want["validate"] = False - # if the image is already upgraded, don't upgrade it again - if self.have.status == "In-Sync" and self.have.reason == "Upgrade" and self.have.policy == want["policy"]: - idempotent_want["upgrade"]["nxos"] = False - - # Get relevant install options from NDFC based on the - # options in our want item - instance = ImageInstallOptions(self.module) - instance.policy_name = want["policy"] - msg = f"REMOVE: {self.class_name}._get_idempotent_want() " - msg += f"calling ImageInstallOptions.refresh() with " - msg += f"serial_number {self.have.serial_number} " - msg += f"ip_address {self.have.ip_address} " - msg += f"device_name {self.have.device_name}" - self.log_msg(msg) - instance.serial_number = self.have.serial_number - instance.epld = want["upgrade"]["epld"] - instance.issu = want["upgrade"]["nxos"] - instance.refresh() - - if instance.epld_modules is None: - idempotent_want["upgrade"]["epld"] = False - self.log_msg(f"REMOVE: {self.class_name}._get_idempotent_want() returned idempotent_want: {idempotent_want}") - return idempotent_want - - def get_need_merged(self): - """ - Caller: main() - - For merged state, populate self.need list() with items from - our want list that are not in our have list. These items will be sent - to NDFC. - """ - need = [] - - for want_create in self.want_create: - self.have.ip_address = want_create["ip_address"] - if self.have.serial_number is not None: - idempotent_want = self._get_idempotent_want(want_create) - if ( - idempotent_want["policy_changed"] is False - and idempotent_want["stage"] is False - and idempotent_want["upgrade"]["nxos"] is False - and idempotent_want["upgrade"]["epld"] is False - ): - continue - need.append(idempotent_want) - self.need = need - msg = f"REMOVE: {self.class_name}.get_need_merged: " - msg += f"need: {self.need}" - self.log_msg(msg) - - def get_need_deleted(self): - """ - Caller: main() - - For deleted state, populate self.need list() with items from our want - list that are not in our have list. These items will be sent to NDFC. - """ - need = [] - for want in self.want_create: - self.have.ip_address = want["ip_address"] - if self.have.serial_number is None: - continue - if self.have.policy is None: - continue - need.append(want) - self.need = need - - def get_need_query(self): - """ - Caller: main() - - For query state, populate self.need list() with all items from our want - list. These items will be sent to NDFC. - """ - need = [] - for want in self.want_create: - need.append(want) - self.need = need - - @staticmethod - def _build_params_spec_for_merged_state(): - """ - Build the specs for the parameters expected when state == merged. - - Caller: _validate_input_for_merged_state() - Return: params_spec, a dictionary containing the set of - playbook parameter specifications. - """ - params_spec = {} - params_spec["policy"] = {} - params_spec["policy"]["required"] = False - params_spec["policy"]["type"] = "str" - - params_spec["stage"] = {} - params_spec["stage"]["required"] = False - params_spec["stage"]["type"] = "bool" - params_spec["stage"]["default"] = True - - params_spec["validate"] = {} - params_spec["validate"]["required"] = False - params_spec["validate"]["type"] = "bool" - params_spec["validate"]["default"] = True - - params_spec["upgrade"] = {} - params_spec["upgrade"]["required"] = False - params_spec["upgrade"]["type"] = "dict" - params_spec["upgrade"]["default"] = {} - - section = "options" - params_spec[section] = {} - params_spec[section]["required"] = False - params_spec[section]["type"] = "dict" - params_spec[section]["default"] = {} - - sub_section = "nxos" - params_spec[section][sub_section] = {} - params_spec[section][sub_section]["required"] = False - params_spec[section][sub_section]["type"] = "dict" - params_spec[section][sub_section]["default"] = {} - - params_spec[section][sub_section]["mode"] = {} - params_spec[section][sub_section]["mode"]["required"] = False - params_spec[section][sub_section]["mode"]["type"] = "str" - params_spec[section][sub_section]["mode"]["default"] = "disruptive" - - params_spec[section][sub_section]["bios_force"] = {} - params_spec[section][sub_section]["bios_force"]["required"] = False - params_spec[section][sub_section]["bios_force"]["type"] = "bool" - params_spec[section][sub_section]["bios_force"]["default"] = False - - sub_section = "epld" - params_spec[section][sub_section] = {} - params_spec[section][sub_section]["required"] = False - params_spec[section][sub_section]["type"] = "dict" - params_spec[section][sub_section]["default"] = {} - - params_spec[section][sub_section]["module"] = {} - params_spec[section][sub_section]["module"] ["required"] = False - params_spec[section][sub_section]["module"] ["type"] = "str" - params_spec[section][sub_section]["module"] ["default"] = "ALL" - - params_spec[section][sub_section]["golden"] = {} - params_spec[section][sub_section]["golden"] ["required"] = False - params_spec[section][sub_section]["golden"] ["type"] = "bool" - params_spec[section][sub_section]["golden"] ["default"] = False - - sub_section = "reboot" - params_spec[section][sub_section] = {} - params_spec[section][sub_section]["required"] = False - params_spec[section][sub_section]["type"] = "dict" - params_spec[section][sub_section]["default"] = {} - - params_spec[section][sub_section]["config_reload"] = {} - params_spec[section][sub_section]["config_reload"] ["required"] = False - params_spec[section][sub_section]["config_reload"] ["type"] = "bool" - params_spec[section][sub_section]["config_reload"] ["default"] = False - - params_spec[section][sub_section]["write_erase"] = {} - params_spec[section][sub_section]["write_erase"] ["required"] = False - params_spec[section][sub_section]["write_erase"] ["type"] = "bool" - params_spec[section][sub_section]["write_erase"] ["default"] = False - - sub_section = "package" - params_spec[section][sub_section] = {} - params_spec[section][sub_section]["required"] = False - params_spec[section][sub_section]["type"] = "dict" - params_spec[section][sub_section]["default"] = {} - - params_spec[section][sub_section]["install"] = {} - params_spec[section][sub_section]["install"] ["required"] = False - params_spec[section][sub_section]["install"] ["type"] = "bool" - params_spec[section][sub_section]["install"] ["default"] = False - - params_spec[section][sub_section]["uninstall"] = {} - params_spec[section][sub_section]["uninstall"] ["required"] = False - params_spec[section][sub_section]["uninstall"] ["type"] = "bool" - params_spec[section][sub_section]["uninstall"] ["default"] = False - - return copy.deepcopy(params_spec) - - def validate_input(self): - """ - Caller: main() - - Validate the playbook parameters - """ - state = self.params["state"] - self.log_msg(f"{self.class_name}.validate_input: state: {state}") - - if state not in ["merged", "deleted", "query"]: - msg = f"This module supports deleted, merged, and query states. Got state {state}" - self.module.fail_json(msg) - - if state == "merged": - self.log_msg(f"{self.class_name}.validate_input: call _validate_input_for_merged_state()") - self._validate_input_for_merged_state() - return - if state == "deleted": - self._validate_input_for_deleted_state() - return - if state == "query": - self._validate_input_for_query_state() - return - - def _validate_input_for_merged_state(self): - """ - Caller: self.validate_input() - - Validate that self.config contains appropriate values for merged state - """ - if not self.config: - msg = "config: element is mandatory for state merged" - self.module.fail_json(msg) - - params_spec = self._build_params_spec_for_merged_state() - - self.log_msg(f"{self.class_name}._validate_input_for_merged_state: params_spec: {params_spec}") - self.log_msg(f"{self.class_name}._validate_input_for_merged_state: self.config: {self.config}") - valid_params, invalid_params = validate_list_of_dicts( - self.config.get("switches"), params_spec, self.module - ) - # We're not using self.validated. Keeping this to avoid - # linter error due to non-use of valid_params - self.validated = copy.deepcopy(valid_params) - - if invalid_params: - msg = "Invalid parameters in playbook: " - msg += f"{','.join(invalid_params)}" - self.module.fail_json(msg) - - def _validate_input_for_deleted_state(self): - """ - Caller: self.validate_input() - - Validate that self.config contains appropriate values for deleted state - - NOTES: - 1. This is currently identical to _validate_input_for_merged_state() - 2. Adding in case there are differences in the future - """ - params_spec = self._build_params_spec_for_merged_state() - if not self.config: - msg = "config: element is mandatory for state deleted" - self.module.fail_json(msg) - - valid_params, invalid_params = validate_list_of_dicts( - self.config.get("switches"), params_spec, self.module - ) - # We're not using self.validated. Keeping this to avoid - # linter error due to non-use of valid_params - self.validated = copy.deepcopy(valid_params) - - if invalid_params: - msg = "Invalid parameters in playbook: " - msg += f"{','.join(invalid_params)}" - self.module.fail_json(msg) - - def _validate_input_for_query_state(self): - """ - Caller: self.validate_input() - - Validate that self.config contains appropriate values for query state - - NOTES: - 1. This is currently identical to _validate_input_for_merged_state() - 2. Adding in case there are differences in the future - """ - params_spec = self._build_params_spec_for_merged_state() - if not self.config: - msg = "config: element is mandatory for state query" - self.module.fail_json(msg) - - valid_params, invalid_params = validate_list_of_dicts( - self.config.get("switches"), params_spec, self.module - ) - # We're not using self.validated. Keeping this to avoid - # linter error due to non-use of valid_params - self.validated = copy.deepcopy(valid_params) - - if invalid_params: - msg = "Invalid parameters in playbook: " - msg += f"{','.join(invalid_params)}" - self.module.fail_json(msg) - - def _merge_global_and_switch_configs(self, config): - """ - Merge the global config with each switch config and return - a dict of switch configs keyed on switch ip_address. - - Merge rules: - 1. switch_config takes precedence over global_config. - 2. If switch_config is missing a parameter, use parameter - from global_config. - 3. If a switch_config has a parameter, use it. - 4. If global_config and switch_config are both missing an - optional parameter, use the parameter's default value. - 5. If global_config and switch_config are both missing a - mandatory parameter, fail. - """ - if not config.get("switches"): - msg = f"{self.class_name}._merge_global_and_switch_configs: " - msg += "playbook is missing list of switches" - self.module.fail_json(msg) - - msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " - msg += f"config: {config}" - self.log_msg(msg) - global_config = {} - global_config["policy"] = config.get("policy") - global_config["stage"] = config.get("stage") - global_config["upgrade"] = config.get("upgrade") - global_config["options"] = config.get("options") - global_config["validate"] = config.get("validate") - msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " - msg += f"global_config: {global_config}" - self.log_msg(msg) - self.switch_configs = [] - for switch in config["switches"]: - switch_config = global_config.copy() | switch.copy() - msg = f"REMOVE: {self.class_name}._merge_global_and_switch_configs: " - msg += f"merged switch_config: {switch_config}" - self.log_msg(msg) - self.switch_configs.append(switch_config) - - def _validate_switch_configs(self): - """ - Ensure mandatory parameters are present for each switch - - fail_json if this isn't the case - Set defaults for missing optional parameters - - NOTES: - 1. Final application of missing default parameters is done in - ImageUpgrade.commit() - - Callers: - - self.get_want - """ - for switch in self.switch_configs: - msg = f"REMOVE: {self.class_name}._validate_switch_configs: " - msg += f"switch: {switch}" - self.log_msg(msg) - if not switch.get("ip_address"): - msg = "playbook is missing ip_address for at least one switch" - self.module.fail_json(msg) - # for query state, the only mandatory parameter is ip_address - # so skip the remaining checks - if self.params.get("state") == "query": - continue - if switch.get("policy") is None: - msg = "playbook is missing image policy for switch " - msg += f"{switch.get('ip_address')} " - msg += "and global image policy is not defined." - self.module.fail_json(msg) - - def _build_policy_attach_payload(self): - """ - Build the payload for the policy attach request to NDFC - Verify that the image policy exists on NDFC - Verify that the image policy supports the switch platform - - Callers: - - self.handle_merged_state - """ - self.payloads = [] - # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests - self.switch_details.refresh() - self.image_policies.refresh() - for switch in self.need: - if switch.get("policy_changed") is False: - continue - self.switch_details.ip_address = switch.get("ip_address") - self.image_policies.policy_name = switch.get("policy") - - # Fail if the image policy does not exist. - # Image policy creation is handled by a different module. - if self.image_policies.name is None: - msg = f"policy {switch.get('policy')} does not exist on NDFC" - self.module.fail_json(msg) - - # Fail if the image policy does not support the switch platform - if self.switch_details.platform not in self.image_policies.platform: - msg = f"policy {switch.get('policy')} does not support platform " - msg += f"{self.switch_details.platform}. {switch.get('policy')} " - msg += "supports the following platform(s): " - msg += f"{self.image_policies.platform}" - self.module.fail_json(msg) - - payload = {} - payload["policyName"] = self.image_policies.name - # switch_details.host_name is always None in 12.1.2e - # so we're using logical_name instead - payload["hostName"] = self.switch_details.logical_name - payload["ipAddr"] = self.switch_details.ip_address - payload["platform"] = self.switch_details.platform - payload["serialNumber"] = self.switch_details.serial_number - # payload["bootstrapMode"] = switch.get('bootstrap_mode') - - for item in payload: - if payload[item] is None: - msg = f"Unable to determine {item} for switch {switch.get('ip_address')}. " - msg += "Please verify that the switch is managed by NDFC." - self.module.fail_json(msg) - self.payloads.append(payload) - - def _send_policy_attach_payload(self): - """ - Send the policy attach payload to NDFC and handle the response - - Callers: - - self.handle_merged_state - """ - if len(self.payloads) == 0: - return - path = self.endpoints.policy_attach.get("path") - verb = self.endpoints.policy_attach.get("verb") - payload = {} - payload["mappingList"] = self.payloads - response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) - result = self._handle_response(response, verb) - - if not result["success"]: - self._failure(response) - - def _stage_images(self, serial_numbers): - """ - Initiate image staging to the switch(es) associated with serial_numbers - - Callers: - - handle_merged_state - """ - instance = ImageStage(self.module) - instance.serial_numbers = serial_numbers - instance.commit() - - def _validate_images(self, serial_numbers): - """ - Validate the image staged to the switch(es) - - Callers: - - handle_merged_state - """ - instance = ImageValidate(self.module) - instance.serial_numbers = serial_numbers - # TODO:2 Discuss with Mike/Shangxin - ImageValidate.non_disruptive - # Should we add this option to the playbook? - # It's supported in ImageValidate with default of False - # instance.non_disruptive = False - instance.commit() - - def _verify_install_options(self, devices): - """ - Verify that the install options for the devices(es) are valid - - Example devices structure: - - [ - { - 'policy': 'KR3F', - 'stage': False, - 'upgrade': { - 'nxos': True, - 'epld': False - }, - 'options': { - 'nxos': { - 'mode': 'non_disruptive' - }, - 'epld': { - 'module': 'ALL', - 'golden': False - } - }, - 'validate': False, - 'ip_address': '172.22.150.102', - 'policy_changed': False - }, - etc... - ] - - Callers: - - self.handle_merged_state - """ - if len(devices) == 0: - return - install_options = ImageInstallOptions(self.module) - # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests - self.switch_details.refresh() - for device in devices: - self.log_msg(f"REMOVE: {self.class_name}._verify_install_options: device: {device}") - self.switch_details.ip_address = device.get("ip_address") - install_options.serial_number = self.switch_details.serial_number - install_options.policy_name = device["policy"] - install_options.epld = device["upgrade"]["epld"] - install_options.issu = device["upgrade"]["nxos"] - install_options.refresh() - if install_options.status not in ["Success", "Skipped"] and device["upgrade"]["nxos"] is True: - msg = f"NXOS upgrade is set to True for switch " - msg += f"{device['ip_address']}, but the image policy " - msg += f"{install_options.policy_name} does not contain an " - msg += f"NX-OS image" - self.module.fail_json(msg) - if install_options.epld_modules is None and device["upgrade"]["epld"] is True: - msg = f"EPLD upgrade is set to True for switch " - msg += f"{device['ip_address']}, but the image policy " - msg += f"{install_options.policy_name} does not contain an " - msg += f"EPLD image." - self.module.fail_json(msg) - - def _upgrade_images(self, devices): - """ - Upgrade the switch(es) to the specified image - - Callers: - - handle_merged_state - """ - self.log_msg(f"REMOVE: {self.class_name}._upgrade_images: devices: {devices}") - upgrade = ImageUpgrade(self.module) - upgrade.devices = devices - # TODO:2 Discuss with Mike/Shangxin. Upgrade option handling mutex options. - # I'm leaning toward doing this in ImageUpgrade().validate_options() - # which would cover the various scenarios and fail_json() on invalid - # combinations. - # For epld upgrade disrutive must be True and non_disruptive must be False - # upgrade.epld_upgrade = True - # upgrade.disruptive = True - # upgrade.non_disruptive = False - # upgrade.epld_module = "ALL" - upgrade.commit() - - def handle_merged_state(self): - """ - Update the switch policy if it has changed. - Stage the image if requested. - Validate the image if requested. - Upgrade the image if requested. - - Caller: main() - """ - # TODO:1 Replace these with ImagePolicyAction - # See commented code below - self._build_policy_attach_payload() - self._send_policy_attach_payload() - - # Use (or not) below for policy attach/detach - # instance = ImagePolicyAction(self.module) - # instance.policy_name = "NR3F" - # instance.action = "attach" # or detach - # instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] - # instance.commit() - # policy_attach_devices = [] - # policy_detach_devices = [] - - stage_devices = [] - validate_devices = [] - upgrade_devices = [] - - # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests - self.switch_details.refresh() - for switch in self.need: - msg = f"REMOVE: {self.class_name}.handle_merged_state: switch: {switch}" - self.log_msg(msg) - self.switch_details.ip_address = switch.get("ip_address") - device = {} - device["serial_number"] = self.switch_details.serial_number - self.have.ip_address = self.switch_details.ip_address - device["policy_name"] = switch.get("policy") - device["ip_address"] = self.switch_details.ip_address - if switch.get("stage") is not False: - stage_devices.append(device["serial_number"]) - if switch.get("validate") is not False: - validate_devices.append(device["serial_number"]) - if ( - switch.get("upgrade").get("nxos") is not False or - switch.get("upgrade").get("epld") is not False): - upgrade_devices.append(switch) - - msg = f"REMOVE: {self.class_name}.handle_merged_state: stage_devices: {stage_devices}" - self.log_msg(msg) - self._stage_images(stage_devices) - - self.log_msg( - f"REMOVE: {self.class_name}.handle_merged_state: validate_devices: {validate_devices}" - ) - self._validate_images(validate_devices) - - self.log_msg( - f"REMOVE: {self.class_name}.handle_merged_state: upgrade_devices: {upgrade_devices}" - ) - self._verify_install_options(upgrade_devices) - self._upgrade_images(upgrade_devices) - - def handle_deleted_state(self): - """ - Delete the image policy from the switch(es) - - Caller: main() - """ - msg = f"REMOVE: {self.class_name}.handle_deleted_state: " - msg += f"Entered with self.need {self.need}" - self.log_msg(msg) - detach_policy_devices = {} - # TODO:2 Need a way to call refresh() once in __init__() and mock in unit tests - self.switch_details.refresh() - self.image_policies.refresh() - for switch in self.need: - self.switch_details.ip_address = switch.get("ip_address") - self.image_policies.policy_name = switch.get("policy") - # if self.image_policies.name is None: - # continue - if self.image_policies.name not in detach_policy_devices: - detach_policy_devices[self.image_policies.policy_name] = [] - detach_policy_devices[self.image_policies.policy_name].append( - self.switch_details.serial_number - ) - msg = f"REMOVE: {self.class_name}.handle_deleted_state: " - msg += f"detach_policy_devices: {detach_policy_devices}" - self.log_msg(msg) - - if len(detach_policy_devices) == 0: - self.result = dict(changed=False, diff=[], response=[]) - return - instance = ImagePolicyAction(self.module) - for policy_name in detach_policy_devices: - msg = f"REMOVE: {self.class_name}.handle_deleted_state: " - msg += f"detach policy_name: {policy_name}" - msg += f" from devices: {detach_policy_devices[policy_name]}" - self.log_msg(msg) - instance.policy_name = policy_name - instance.action = "detach" - instance.serial_numbers = detach_policy_devices[policy_name] - instance.commit() - - def handle_query_state(self): - """ - Return the ISSU state of the switch(es) listed in the playbook - - Caller: main() - """ - instance = SwitchIssuDetailsByIpAddress(self.module) - instance.refresh() - msg = f"REMOVE: {self.class_name}.handle_query_state: " - msg += f"Entered. self.need {self.need}" - self.log_msg(msg) - query_devices = [] - for switch in self.need: - instance.ip_address = switch.get("ip_address") - if instance.filtered_data is None: - continue - query_devices.append(instance.filtered_data) - msg = f"REMOVE: {self.class_name}.handle_query_state: " - msg += f"query_policies: {query_devices}" - self.log_msg(msg) - self.result["response"] = query_devices - self.result["diff"] = [] - self.result["changed"] = False - - - def _failure(self, resp): - """ - Caller: self.attach_policies() - - This came from dcnm_inventory.py, but doesn't seem to be correct - for the case where resp["DATA"] does not exist? - - If resp["DATA"] does not exist, the contents of the - if block don't seem to actually do anything: - - data will be None - - Hence, data.get("stackTrace") will also be None - - Hence, data.update() and res.update() are never executed - - So, the only two lines that will actually ever be executed are - the happy path: - - res = copy.deepcopy(resp) - self.module.fail_json(msg=res) - """ - res = copy.deepcopy(resp) - - if not resp.get("DATA"): - data = copy.deepcopy(resp.get("DATA")) - if data.get("stackTrace"): - data.update( - {"stackTrace": "Stack trace is hidden, use '-vvvvv' to print it"} - ) - res.update({"DATA": data}) - - self.module.fail_json(msg=res) - - -class SwitchDetails(ImageUpgradeCommon): - """ - Retrieve switch details from NDFC and provide property accessors - for the switch attributes. - - Usage (where module is an instance of AnsibleModule): - - instance = SwitchDetails(module) - instance.refresh() - instance.ip_address = 10.1.1.1 - fabric_name = instance.fabric_name - serial_number = instance.serial_number - etc... - - Switch details are retrieved by calling instance.refresh(). - - Endpoint: - /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["ip_address"] = None - self.properties["response_data"] = None - self.properties["response"] = None - self.properties["result"] = None - - def refresh(self): - """ - Caller: __init__() - - Refresh switch_details with current switch details from NDFC - """ - path = self.endpoints.switches_info.get("path") - verb = self.endpoints.switches_info.get("verb") - self.log_msg(f"REMOVE: {self.class_name}.refresh: path: {path}") - self.log_msg(f"REMOVE: {self.class_name}.refresh: verb: {verb}") - self.properties["response"] = dcnm_send(self.module, verb, path) - msg = f"REMOVE: {self.class_name}.refresh: self.response {self.response}" - self.log_msg(msg) - self.properties["result"] = self._handle_response(self.response, verb) - msg = f"REMOVE: {self.class_name}.refresh: self.result {self.result}" - self.log_msg(msg) - - if self.response["RETURN_CODE"] != 200: - msg = "Unable to retrieve switch information from the controller. " - msg += f"Got response {self.response}" - self.module.fail_json(msg) - - data = self.response.get("DATA") - self.properties["response_data"] = {} - for switch in data: - self.properties["response_data"][switch["ipAddress"]] = switch - - msg = f"REMOVE: {self.class_name}.refresh: self.response_data {self.response_data}" - self.log_msg(msg) - - def _get(self, item): - if self.ip_address is None: - msg = f"{self.class_name}: set instance.ip_address " - msg += f"before accessing property {item}." - self.module.fail_json(msg) - return self.make_boolean( - self.make_none( - self.properties["response_data"][self.ip_address].get(item) - ) - ) - - @property - def ip_address(self): - """ - Set the ip_address of the switch to query. - - This needs to be set before accessing this class's properties. - """ - return self.properties.get("ip_address") - - @ip_address.setter - def ip_address(self, value): - self.properties["ip_address"] = value - - @property - def fabric_name(self): - """ - Return the fabricName of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("fabricName") - - @property - def hostname(self): - """ - Return the hostName of the switch with ip_address, if it exists. - Return None otherwise - - NOTES: - 1. This is None for 12.1.2e - 2. Better to use logical_name which is populated in both 12.1.2e and 12.1.3b - """ - return self._get("hostName") - - @property - def logical_name(self): - """ - Return the logicalName of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("logicalName") - - @property - def model(self): - """ - Return the model of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("model") - - @property - def response_data(self): - """ - Return parsed data from the GET request. - Return None otherwise - - NOTE: Keyed on ip_address - """ - return self.properties["response_data"] - - @property - def response(self): - """ - Return the raw response from the GET request. - Return None otherwise - """ - return self.properties["response"] - - @property - def result(self): - """ - Return the raw result of the GET request. - Return None otherwise - """ - return self.properties["result"] - - @property - def platform(self): - """ - Return the platform of the switch with ip_address, if it exists. - Return None otherwise - - NOTE: This is derived from "model" and is not in the NDFC response - """ - model = self._get("model") - if model is None: - return None - return model.split("-")[0] - - @property - def role(self): - """ - Return the switchRole of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("switchRole") - - @property - def serial_number(self): - """ - Return the serialNumber of the switch with ip_address, if it exists. - Return None otherwise - """ - return self._get("serialNumber") - - -class ImageInstallOptions(ImageUpgradeCommon): - """ - Retrieve install-options details for ONE switch from NDFC and - provide property accessors for the policy attributes. - - Caveats: - - This retrieves for a SINGLE switch only. - - Set serial_number and policy_name and call refresh() for - each switch separately. - - Usage (where module is an instance of AnsibleModule): - - instance = ImageInstallOptions(module) - # Mandatory - instance.policy_name = "NR3F" - instance.serial_number = "FDO211218GC" - # Optional - instance.epld = True - instance.package_install = True - instance.issu = True - # Retrieve install-options details from NDFC - instance.refresh() - if instance.device_name is None: - print("Cannot retrieve policy/serial_number combination from NDFC") - exit(1) - status = instance.status - platform = instance.platform - etc... - - install-options are retrieved by calling instance.refresh(). - - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options - Request body: - { - "devices": [ - { - "serialNumber": "FDO211218HH", - "policyName": "NR1F" - }, - { - "serialNumber": "FDO211218GC", - "policyName": "NR3F" - } - ], - "issu": true, - "epld": false, - "packageInstall": false - } - Response body: - NOTES: - 1. epldModules will be null if epld is false in the request body. - This class converts this to None (python NoneType) in this case. - - { - "compatibilityStatusList": [ - { - "deviceName": "cvd-1312-leaf", - "ipAddress": "172.22.150.103", - "policyName": "KR5M", - "platform": "N9K/N3K", - "version": "10.2.5", - "osType": "64bit", - "status": "Skipped", - "installOption": "NA", - "compDisp": "Compatibility status skipped.", - "versionCheck": "Compatibility status skipped.", - "preIssuLink": "Not Applicable", - "repStatus": "skipped", - "timestamp": "NA" - } - ], - "epldModules": { - "moduleList": [ - { - "deviceName": "cvd-1312-leaf", - "ipAddress": "172.22.150.103", - "policyName": "KR5M", - "module": 1, - "name": null, - "modelName": "N9K-C93180YC-EX", - "moduleType": "IO FPGA", - "oldVersion": "0x15", - "newVersion": "0x15" - }, - { - "deviceName": "cvd-1312-leaf", - "ipAddress": "172.22.150.103", - "policyName": "KR5M", - "module": 1, - "name": null, - "modelName": "N9K-C93180YC-EX", - "moduleType": "MI FPGA", - "oldVersion": "0x4", - "newVersion": "0x04" - } - ], - "bException": false, - "exceptionReason": null - }, - "installPacakges": null, - "errMessage": "" - } - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["epld"] = False - self.properties["issu"] = True - self.properties["response_data"] = None - self.properties["response"] = None - self.properties["result"] = None - self.properties["package_install"] = False - self.properties["policy_name"] = None - self.properties["serial_number"] = None - self.properties["epld_modules"] = None - - def refresh(self): - """ - Refresh self.data with current install-options from NDFC - """ - if self.policy_name is None: - msg = f"{self.class_name}.refresh: " - msg += "instance.policy_name must be set before " - msg += "calling refresh()" - self.module.fail_json(msg) - if self.serial_number is None: - msg = f"{self.class_name}.refresh: " - msg += f"instance.serial_number must be set before " - msg += f"calling refresh()" - self.module.fail_json(msg) - - path = self.endpoints.install_options.get("path") - verb = self.endpoints.install_options.get("verb") - self._build_payload() - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"payload: {self.payload}" - self.log_msg(msg) - self.properties["response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) - ) - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"response: {self.response}" - self.log_msg(msg) - self.properties["result"] = self._handle_response(self.response, verb) - # TODO:2 should error message contain full response or just DATA.error? - if self.result["success"] is False: - msg = f"{self.class_name}.refresh: " - msg += "Bad result when retrieving install-options from NDFC. " - msg += f"NDFC response: {self.response}" - self.module.fail_json(msg) - - self.properties["response_data"] = self.response.get("DATA") - self.data = self.properties["response_data"] - if self.data.get("compatibilityStatusList") is None: - self.compatibility_status = {} - else: - self.compatibility_status = self.data.get("compatibilityStatusList")[0] - - def _build_payload(self): - """ - { - "devices": [ - { - "serialNumber": "FDO211218HH", - "policyName": "NR1F" - } - ], - "issu": true, - "epld": false, - "packageInstall": false - } - """ - self.payload = {} - self.payload["devices"] = [] - devices = {} - devices["serialNumber"] = self.serial_number - devices["policyName"] = self.policy_name - self.payload["devices"].append(devices) - self.payload["issu"] = self.issu - self.payload["epld"] = self.epld - self.payload["packageInstall"] = self.package_install - - def _get(self, item): - return self.data.get(item) - - # Mandatory properties - @property - def policy_name(self): - """ - Set the policy_name of the policy to query. - """ - return self.properties.get("policy_name") - - @policy_name.setter - def policy_name(self, value): - self.properties["policy_name"] = value - - @property - def serial_number(self): - """ - Set the serial_number of the device to query. - """ - return self.properties.get("serial_number") - - @serial_number.setter - def serial_number(self, value): - self.properties["serial_number"] = value - - # Optional properties - @property - def issu(self): - """ - Enable (True) or disable (False) issu compatibility check. - Valid values: - True - Enable issu compatibility check - False - Disable issu compatibility check - Default: True - """ - return self.properties.get("issu") - - @issu.setter - def issu(self, value): - if not isinstance(value, bool): - msg = f"{self.class_name}.issu.setter: " - msg += "issu must be a boolean value" - self.module.fail_json(msg) - self.properties["issu"] = value - - @property - def epld(self): - """ - Enable (True) or disable (False) epld compatibility check. - - Valid values: - True - Enable epld compatibility check - False - Disable epld compatibility check - Default: False - """ - return self.properties.get("epld") - - @epld.setter - def epld(self, value): - if not isinstance(value, bool): - msg = f"{self.class_name}.epld.setter: " - msg += "epld must be a boolean value" - self.module.fail_json(msg) - self.properties["epld"] = value - - @property - def package_install(self): - """ - Enable (True) or disable (False) package_install compatibility check. - Valid values: - True - Enable package_install compatibility check - False - Disable package_install compatibility check - Default: False - """ - return self.properties.get("package_install") - - @package_install.setter - def package_install(self, value): - if not isinstance(value, bool): - msg = f"{self.class_name}.package_install.setter: " - msg += "package_install must be a boolean value" - self.module.fail_json(msg) - self.properties["package_install"] = value - - # Retrievable properties - @property - def comp_disp(self): - """ - Return the compDisp (CLI output from show install all status) - of the install-options response, if it exists. - Return None otherwise - """ - return self.compatibility_status.get("compDisp") - - @property - def device_name(self): - """ - Return the deviceName of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("deviceName") - - @property - def epld_modules(self): - """ - Return the epldModules of the install-options response, - if it exists. - Return None otherwise - - epldModules will be "null" if self.epld is False, so - make_none() will convert to NoneType in this case. - """ - return self.make_none(self._get("epldModules")) - - @property - def err_message(self): - """ - Return the errMessage of the install-options response, - if it exists. - Return None otherwise - - epldModules will be "null" if self.epld is False, so - make_none() will convert to NoneType in this case. - """ - return self._get("errMessage") - - @property - def install_option(self): - """ - Return the installOption of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("installOption") - - @property - def install_packages(self): - """ - Return the installPackages of the install-options response, - if it exists. - Return None otherwise - - NOTE: yes, installPacakges is misspelled in the response in the - following versions (at least): - 12.1.2e - 12.1.3b - """ - return self.make_none(self._get("installPacakges")) - - @property - def ip_address(self): - """ - Return the ipAddress of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("ipAddress") - - @property - def response_data(self): - """ - Return the raw data from the NDFC response. - """ - return self.properties.get("response_data") - - @property - def response(self): - """ - Return the response from NDFC of the query. - """ - return self.properties.get("response") - - @property - def result(self): - """ - Return the result from NDFC of the query. - """ - return self.properties.get("result") - - @property - def os_type(self): - """ - Return the osType of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("osType") - - @property - def platform(self): - """ - Return the platform of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("platform") - - @property - def pre_issu_link(self): - """ - Return the preIssuLink of the install-options response, if it exists. - Return None otherwise - """ - return self.compatibility_status.get("preIssuLink") - - @property - def raw_data(self): - """ - Return the raw data of the install-options response, if it exists. - """ - return self.data - - @property - def raw_response(self): - """ - Return the raw response, if it exists. - """ - return self.response - - @property - def rep_status(self): - """ - Return the repStatus of the install-options response, if it exists. - Return None otherwise - """ - return self.compatibility_status.get("repStatus") - - @property - def status(self): - """ - Return the status of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("status") - - @property - def timestamp(self): - """ - Return the timestamp of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("timestamp") - - @property - def version(self): - """ - Return the version of the install-options response, - if it exists. - Return None otherwise - """ - return self.compatibility_status.get("version") - - @property - def version_check(self): - """ - Return the versionCheck (version check CLI output) - of the install-options response, if it exists. - Return None otherwise - """ - return self.compatibility_status.get("versionCheck") - - -# ============================================================================== -class ImagePolicyAction(ImageUpgradeCommon): - """ - Perform image policy actions on NDFC on one or more switches. - - Support for the following actions: - - attach - - detach - - query - - Usage (where module is an instance of AnsibleModule): - - instance = ImagePolicyAction(module) - instance.policy_name = "NR3F" - instance.action = "attach" # or detach, or query - instance.serial_numbers = ["FDO211218GC", "FDO211218HH"] - instance.commit() - # for query only - query_result = instance.query_result - - Endpoints: - For action == attach: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy - For action == detach: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy - For action == query: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - self.image_policies = ImagePolicies(self.module) - self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.module) - self.valid_actions = {"attach", "detach", "query"} - - def _init_properties(self): - self.properties = {} - self.properties["action"] = None - self.properties["response"] = None - self.properties["result"] = None - self.properties["policy_name"] = None - self.properties["query_result"] = None - self.properties["serial_numbers"] = None - - def build_attach_payload(self): - """ - build the payload to send in the POST request - to attach policies to devices - - caller _attach_policy() - """ - self.payloads = [] - # TODO:2 this results in more calls than necessary to NDFC. Need a better way to handle this. - self.switch_issu_details.refresh() - for serial_number in self.serial_numbers: - self.switch_issu_details.serial_number = serial_number - payload = {} - payload["policyName"] = self.policy_name - payload["hostName"] = self.switch_issu_details.device_name - payload["ipAddr"] = self.switch_issu_details.ip_address - payload["platform"] = self.switch_issu_details.platform - payload["serialNumber"] = self.switch_issu_details.serial_number - for item in payload: - if payload[item] is None: - msg = f"Unable to determine {item} for switch " - msg += f"{self.switch_issu_details.ip_address}, " - msg += f"{self.switch_issu_details.serial_number}, " - msg += f"{self.switch_issu_details.device_name}. " - msg += "Please verify that the switch is managed by NDFC." - self.module.fail_json(msg) - self.payloads.append(payload) - - def validate_request(self): - """ - validations prior to commit() should be added here. - """ - self.log_msg(f"REMOVE: {self.class_name}.validate_request: Entered") - if self.action is None: - msg = f"{self.class_name}.validate_request: " - msg += "instance.action must be set before " - msg += "calling commit()" - self.module.fail_json(msg) - - if self.policy_name is None: - msg = f"{self.class_name}.validate_request: " - msg += "instance.policy_name must be set before " - msg += "calling commit()" - self.module.fail_json(msg) - - self.log_msg(f"REMOVE: {self.class_name}.validate_request: action {self.action}") - - if self.action == "query": - return - - self.log_msg(f"REMOVE: {self.class_name}.validate_request: serial_numbers {self.serial_numbers}") - - if self.serial_numbers is None: - msg = f"{self.class_name}.validate_request: " - msg += "instance.serial_numbers must be set before " - msg += "calling commit()" - self.module.fail_json(msg) - - - # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it - self.image_policies.refresh() - self.switch_issu_details.refresh() - # Fail if the image policy does not support the switch platform - self.image_policies.policy_name = self.policy_name - for serial_number in self.serial_numbers: - self.switch_issu_details.serial_number = serial_number - if self.switch_issu_details.platform not in self.image_policies.platform: - msg = f"policy {self.policy_name} does not support platform " - msg += f"{self.switch_issu_details.platform}. {self.policy_name} " - msg += "supports the following platform(s): " - msg += f"{self.image_policies.platform}" - self.module.fail_json(msg) - - def commit(self): - self.validate_request() - if self.action == "attach": - self._attach_policy() - elif self.action == "detach": - self._detach_policy() - elif self.action == "query": - self._query_policy() - else: - msg = f"{self.class_name}.commit: " - msg += f"Unknown action {self.action}." - self.module.fail_json(msg) - - def _attach_policy(self): - """ - Attach policy_name to the switch(es) associated with serial_numbers - - NOTES: - 1. This method creates a list of responses and results which - are accessible via properties response and result, - respectively. - """ - self.build_attach_payload() - path = self.endpoints.policy_attach.get("path") - verb = self.endpoints.policy_attach.get("verb") - responses = [] - results = [] - for payload in self.payloads: - response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) - result = self._handle_response(response, verb) - if not result["success"]: - msg = f"{self.class_name}._attach_policy: " - msg += f"Bad result when attaching policy {self.policy_name} " - msg += f"to switch {payload['ipAddr']}." - self.module.fail_json(msg) - responses.append(response) - results.append(result) - self.properties["response"] = responses - self.properties["result"] = results - - def _detach_policy(self): - """ - Detach policy_name from the switch(es) associated with serial_numbers - verb: DELETE - endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy - query_params: ?serialNumber=FDO211218GC,FDO21120U5D - """ - path = self.endpoints.policy_detach.get("path") - verb = self.endpoints.policy_detach.get("verb") - query_params = ",".join(self.serial_numbers) - path += f"?serialNumber={query_params}" - response = dcnm_send(self.module, verb, path) - result = self._handle_response(response, verb) - if not result["success"]: - self._failure(response) - self.properties["response"] = response - self.properties["result"] = result - - def _query_policy(self): - """ - Query the image policy - verb: GET - endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ - """ - path = self.endpoints.policy_info.get("path") - verb = self.endpoints.policy_info.get("verb") - path = path.replace("__POLICY_NAME__", self.policy_name) - response = dcnm_send(self.module, verb, path) - result = self._handle_response(response, verb) - if not result["success"]: - self._failure(response) - self.properties["query_result"] = response.get("DATA") - self.properties["response"] = response - self.properties["result"] = result - - @property - def query_result(self): - """ - Return the value of properties["query_result"]. - """ - return self.properties.get("query_result") - - @property - def action(self): - """ - Set the action to take. Either "attach" or "detach". - - Must be set prior to calling instance.commit() - """ - return self.properties.get("action") - - @action.setter - def action(self, value): - if value not in self.valid_actions: - msg = f"{self.class_name}: instance.action must be " - msg += f"one of {','.join(sorted(self.valid_actions))}" - self.module.fail_json(msg) - self.properties["action"] = value - - @property - def response(self): - """ - Return the raw response from NDFC after calling commit(). - - In the case of attach, this is a list of responses. - """ - return self.properties.get("response") - - @property - def result(self): - """ - Return the raw result from NDFC after calling commit(). - - In the case of attach, this is a list of results. - """ - return self.properties.get("result") - - @property - def policy_name(self): - """ - Set the name of the policy to attach, detach, query. - - Must be set prior to calling instance.commit() - """ - return self.properties.get("policy_name") - - @policy_name.setter - def policy_name(self, value): - self.properties["policy_name"] = value - - @property - def serial_numbers(self): - """ - Set the serial numbers of the switches to/from which - policy_name will be attached or detached. - - Must be set prior to calling instance.commit() - """ - return self.properties.get("serial_numbers") - - @serial_numbers.setter - def serial_numbers(self, value): - if not isinstance(value, list): - msg = f"{self.class_name}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." - self.module.fail_json(msg) - self.properties["serial_numbers"] = value - -# ============================================================================== - - -class ImagePolicies(ImageUpgradeCommon): - """ - Retrieve image policy details from NDFC and provide property accessors - for the policy attributes. - - Usage (where module is an instance of AnsibleModule): - - instance = ImagePolicies(module).refresh() - instance.policy_name = "NR3F" - if instance.name is None: - print("policy NR3F does not exist on NDFC") - exit(1) - policy_name = instance.name - platform = instance.platform - epd_image_name = instance.epld_image_name - etc... - - Policies are retrieved on instantiation of this class. - Policies can be refreshed by calling instance.refresh(). - - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self.log_msg(f"{self.class_name}.__init__ entered") - self._init_properties() - # TODO:2 Need a way to call refresh() in __init__ with unit-tests being able to mock it - #self.refresh() - - def _init_properties(self): - self.properties = {} - self.properties["policy_name"] = None - self.properties["response_data"] = None - self.properties["response"] = None - self.properties["result"] = None - - def refresh(self): - """ - Refresh self.image_policies with current image policies from NDFC - """ - path = self.endpoints.policies_info.get("path") - verb = self.endpoints.policies_info.get("verb") - - self.properties["response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - - self.properties["result"] = self._handle_response(self.response, verb) - self.log_msg(f"{self.class_name}.refresh: result: {self.result}") - - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"result: {self.result}" - if not self.result["success"]: - msg = f"{self.class_name}.refresh: " - msg += "Bad result when retriving image policy " - msg += "information from the controller." - self.module.fail_json(msg) - - data = self.response.get("DATA").get("lastOperDataObject") - if data is None: - msg = f"{self.class_name}.refresh: " - msg += "Bad response when retrieving image policy " - msg += "information from the controller." - self.module.fail_json(msg) - if len(data) == 0: - msg = f"{self.class_name}.refresh: " - msg += "NDFC has no defined image policies." - self.module.fail_json(msg) - self.properties["response_data"] = {} - for policy in data: - policy_name = policy.get("policyName") - if policy_name is None: - msg = f"{self.class_name}.refresh: " - msg += "Cannot parse NDFC policy information" - self.module.fail_json(msg) - self.properties["response_data"][policy_name] = policy - - def _get(self, item): - if self.policy_name is None: - msg = f"{self.class_name}._get: " - msg += f"instance.policy_name must be set before " - msg += f"accessing property {item}." - self.module.fail_json(msg) - if self.properties['response_data'].get(self.policy_name) is None: - msg = f"{self.class_name}._get: " - msg += f"policy_name {self.policy_name} is not defined in NDFC" - self.module.fail_json(msg) - return_item = self.make_boolean(self.properties["response_data"][self.policy_name].get(item)) - return_item = self.make_none(return_item) - return return_item - - @property - def description(self): - """ - Return the policyDescr of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("policyDescr") - - @property - def epld_image_name(self): - """ - Return the epldImgName of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("epldImgName") - - @property - def name(self): - """ - Return the name of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("policyName") - - @property - def response_data(self): - """ - Return the parsed data from the NDFC response as a dictionary, - keyed on policy_name. - """ - return self.properties["response_data"] - - @property - def response(self): - """ - Return the raw response from the NDFC response. - """ - return self.properties["response"] - - @property - def result(self): - """ - Return the raw result from the NDFC response. - """ - return self.properties["result"] - - @property - def policy_name(self): - """ - Set the name of the policy to query. - - This must be set prior to accessing any other properties - """ - return self.properties.get("policy_name") - - @policy_name.setter - def policy_name(self, value): - self.properties["policy_name"] = value - - @property - def policy_type(self): - """ - Return the policyType of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("policyType") - - @property - def nxos_version(self): - """ - Return the nxosVersion of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("nxosVersion") - - @property - def package_name(self): - """ - Return the packageName of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("packageName") - - @property - def platform(self): - """ - Return the platform of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("platform") - - @property - def platform_policies(self): - """ - Return the platformPolicies of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("platformPolicies") - - @property - def ref_count(self): - """ - Return the reference count of the policy matching self.policy_name, - if it exists. The reference count is the number of switches using - this policy. - Return None otherwise - """ - return self._get("ref_count") - - @property - def rpm_images(self): - """ - Return the rpmimages of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("rpmimages") - - @property - def image_name(self): - """ - Return the imageName of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("imageName") - - @property - def agnostic(self): - """ - Return the value of agnostic for the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - return self._get("agnostic") - - -class SwitchIssuDetails(ImageUpgradeCommon): - """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes. - - Usage: See subclasses. - - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu - - Response body: - { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO211218GC", - "deviceName": "cvd-1312-leaf", - "fabric": "fff", - "version": "10.3(2)", - "policy": "NR3F", - "status": "In-Sync", - "reason": "Compliance", - "imageStaged": "Success", - "validated": "None", - "upgrade": "None", - "upgGroups": "None", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": null, - "vpcPeer": null, - "role": "leaf", - "lastUpgAction": "Never", - "model": "N9K-C93180YC-EX", - "ipAddress": "172.22.150.103", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 0, - "upgradePercent": 0, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 8430, - "platform": "N9K", - "vpc_role": null, - "ip_address": "172.22.150.103", - "peer": null, - "vdc_id": -1, - "sys_name": "cvd-1312-leaf", - "id": 3, - "group": "fff", - "fcoEEnabled": false, - "mds": false - }, - {etc...} - ] - - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["response"] = None - self.properties["result"] = None - self.properties["response_data"] = None - # action_keys is used in subclasses to determine if any actions - # are in progress. - # Property actions_in_progress returns True if so, False otherwise - self.properties["action_keys"] = set() - self.properties["action_keys"].add("imageStaged") - self.properties["action_keys"].add("upgrade") - self.properties["action_keys"].add("validated") - - - def refresh(self) -> None: - """ - Refresh current issu details from NDFC - """ - path = self.endpoints.issu_info.get("path") - verb = self.endpoints.issu_info.get("verb") - - self.properties["response"] = dcnm_send(self.module, verb, path) - self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - - self.properties["result"] = self._handle_response(self.response, verb) - self.log_msg(f"{self.class_name}.refresh: result: {self.result}") - - msg = f"REMOVE: {self.class_name}.refresh: " - msg += f"result: {self.result}" - # result for 404 response - # {'found': False, 'success': True} - if self.result["success"] == False or self.result["found"] == False: - msg = f"{self.class_name}.refresh: " - msg += "Bad result when retriving switch " - msg += "information from the controller" - self.module.fail_json(msg) - - data = self.response.get("DATA").get("lastOperDataObject") - if data is None: - msg = f"{self.class_name}.refresh: " - msg += "NDFC has no switch ISSU information." - self.module.fail_json(msg) - if len(data) == 0: - msg = f"{self.class_name}.refresh: " - msg += "NDFC has no switch ISSU information." - self.module.fail_json(msg) - - self.properties["response_data"] = self.response.get("DATA", {}).get( - "lastOperDataObject", [] - ) - self.log_msg(f"{self.class_name}.refresh: response: {self.response}") - - @property - def actions_in_progress(self): - """ - Return True if any actions are in progress - Return False otherwise - """ - for action_key in self.properties["action_keys"]: - if self._get(action_key) == "In-Progress": - return True - return False - - def _get(self, item): - """ - overridden in subclasses - """ - pass - - @property - def response_data(self): - """ - Return the raw data retrieved from NDFC - """ - return self.properties["response_data"] - - @property - def response(self): - """ - Return the raw response from the GET request. - Return None otherwise - """ - return self.properties["response"] - - @property - def result(self): - """ - Return the raw result of the GET request. - Return None otherwise - """ - return self.properties["result"] - - @property - def device_name(self): - """ - Return the deviceName of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - device name, e.g. "cvd-1312-leaf" - None - """ - return self._get("deviceName") - - @property - def eth_switch_id(self): - """ - Return the ethswitchid of the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - integer - None - """ - return self._get("ethswitchid") - - @property - def fabric(self): - """ - Return the fabric of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - fabric name, e.g. "myfabric" - None - """ - return self._get("fabric") - - @property - def fcoe_enabled(self): - """ - Return whether FCOE is enabled on the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - boolean (true/false) - None - """ - return self.make_boolean(self._get("fcoEEnabled")) - - @property - def group(self): - """ - Return the group of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - group name, e.g. "mygroup" - None - """ - return self._get("group") - - @property - # id is a python keyword, so we can't use it as a property name - # so we use switch_id instead - def switch_id(self): - """ - Return the switch ID of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer - None - """ - return self._get("id") - - @property - def image_staged(self): - """ - Return the imageStaged of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Success - Failed - None - """ - return self._get("imageStaged") - - @property - def image_staged_percent(self): - """ - Return the imageStagedPercent of the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - Integer in range 0-100 - None - """ - return self._get("imageStagedPercent") - - @property - def ip_address(self): - """ - Return the ipAddress of the switch, if it exists. - Return None otherwise - - Possible values: - switch IP address - None - """ - return self._get("ipAddress") - - @property - def issu_allowed(self): - """ - Return the issuAllowed value of the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - ?? TODO:3 check this - "" - None - """ - return self._get("issuAllowed") - - @property - def last_upg_action(self): - """ - Return the last upgrade action performed on the switch - with ip_address, if it exists. - Return None otherwise - - Possible values: - ?? TODO:3 check this - Never - None - """ - return self._get("lastUpgAction") - - @property - def mds(self): - """ - Return whether the switch with ip_address is an MSD, if it exists. - Return None otherwise - - Possible values: - Boolean (True or False) - None - """ - return self.make_boolean(self._get("mds")) - - @property - def mode(self): - """ - Return the ISSU mode of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - "Normal" - None - """ - return self._get("mode") - - @property - def model(self): - """ - Return the model of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - model number e.g. "N9K-C93180YC-EX" - None - """ - return self._get("model") - - @property - def model_type(self): - """ - Return the model type of the switch with - ip_address, if it exists. - Return None otherwise - - Possible values: - Integer - None - """ - return self._get("modelType") - - @property - def peer(self): - """ - Return the peer of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - ?? TODO:3 check this - None - """ - return self._get("peer") - - @property - def platform(self): - """ - Return the platform of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - platform, e.g. "N9K" - None - """ - return self._get("platform") - - @property - def policy(self): - """ - Return the policy attached to the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - policy name, e.g. "NR3F" - None - """ - return self._get("policy") - - @property - def reason(self): - """ - Return the reason (?) of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Compliance - Validate - Upgrade - None - """ - return self._get("reason") - - @property - def role(self): - """ - Return the role of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - switch role, e.g. "leaf" - None - """ - return self._get("role") - - @property - def serial_number(self): - """ - Return the serialNumber of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - switch serial number, e.g. "AB1234567CD" - None - """ - return self._get("serialNumber") - - @property - def status(self): - """ - Return the sync status of the switch with ip_address, if it exists. - Return None otherwise - - Details: The sync status is the status of the switch with respect - to the image policy. If the switch is in sync with the image policy, - the status is "In-Sync". If the switch is out of sync with the image - policy, the status is "Out-Of-Sync". - - Possible values: - "In-Sync" - "Out-Of-Sync" - None - """ - return self._get("status") - - @property - def status_percent(self): - """ - Return the upgrade (TODO:3 verify this) percentage completion - of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer in range 0-100 - None - """ - return self._get("statusPercent") - - @property - def sys_name(self): - """ - Return the system name of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - system name, e.g. "cvd-1312-leaf" - None - """ - return self._get("sys_name") - - @property - def system_mode(self): - """ - Return the system mode of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - "Maintenance" (TODO:3 verify this) - "Normal" - None - """ - return self._get("systemMode") - - @property - def upgrade(self): - """ - Return the upgrade status of the switch with ip_address, - if it exists. - Return None otherwise - - Possible values: - Success - In-Progress - None - """ - return self._get("upgrade") - - @property - def upg_groups(self): - """ - Return the upgGroups (upgrade groups) of the switch with ip_address, - if it exists. - Return None otherwise - - Possible values: - upgrade group to which the switch belongs e.g. "LEAFS" - None - """ - return self._get("upgGroups") - - @property - def upgrade_percent(self): - """ - Return the upgrade percent complete of the switch - with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer in range 0-100 - None - """ - return self._get("upgradePercent") - - @property - def validated(self): - """ - Return the validation status of the switch with ip_address, - if it exists. - Return None otherwise - - Possible values: - Failed - Success - None - """ - return self._get("validated") - - @property - def validated_percent(self): - """ - Return the validation percent complete of the switch - with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer in range 0-100 - None - """ - return self._get("validatedPercent") - - @property - def vdc_id(self): - """ - Return the vdcId of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer - None - """ - return self._get("vdcId") - - @property - def vdc_id2(self): - """ - Return the vdc_id of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - Integer (negative values are valid) - None - """ - return self._get("vdc_id") - - @property - def version(self): - """ - Return the version of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - version, e.g. "10.3(2)" - None - """ - return self._get("version") - - @property - def vpc_peer(self): - """ - Return the vpcPeer of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - vpc peer e.g.: 10.1.1.1 - None - """ - return self._get("vpcPeer") - - @property - def vpc_role(self): - """ - Return the vpcRole of the switch with ip_address, if it exists. - Return None otherwise - - Possible values: - vpc role e.g.: - "primary" - "secondary" - "none" -> This will be translated to None - "none established" (TODO:3 verify this) - "primary, operational secondary" (TODO:3 verify this) - None - """ - return self._get("vpcRole") - - -class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): - """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes retrieved by ip address. - - Usage (where module is an instance of AnsibleModule): - - instance = SwitchIssuDetailsByIpAddress(module) - instance.refresh() - instance.ip_address = 10.1.1.1 - image_staged = instance.image_staged - image_upgraded = instance.image_upgraded - serial_number = instance.serial_number - etc... - - See SwitchIssuDetails for more details. - """ - - def __init__(self, module): - super().__init__(module) - self._init_properties() - - def _init_properties(self): - super()._init_properties() - self.properties["ip_address"] = None - - def refresh(self): - """ - Caller: __init__() - - Refresh ip_address current issu details from NDFC - """ - super().refresh() - self.data_subclass = {} - for switch in self.response_data: - msg = f"{self.class_name}.refresh: " - msg += f"switch {switch}" - self.data_subclass[switch["ipAddress"]] = switch - - def _get(self, item): - if self.ip_address is None: - msg = f"{self.class_name}: set instance.ip_address " - msg += f"before accessing property {item}." - self.module.fail_json(msg) - if self.data_subclass.get(self.ip_address) is None: - msg = f"{self.class_name}: {self.ip_address} is not " - msg += f"defined in NDFC." - self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.ip_address].get(item)) - - @property - def filtered_data(self): - """ - Return a dictionary of the switch matching self.ip_address. - Return None of the switch does not exist in NDFC. - """ - return self.data_subclass.get(self.ip_address) - - @property - def ip_address(self): - """ - Set the ip_address of the switch to query. - - This needs to be set before accessing this class's properties. - """ - return self.properties.get("ip_address") - - @ip_address.setter - def ip_address(self, value): - self.properties["ip_address"] = value - - -class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): - """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes retrieved by serial_number. - - Usage (where module is an instance of AnsibleModule): - - instance = SwitchIssuDetailsBySerialNumber(module) - instance.refresh() - instance.serial_number = "FDO211218GC" - instance.refresh() - image_staged = instance.image_staged - image_upgraded = instance.image_upgraded - ip_address = instance.ip_address - etc... - - See SwitchIssuDetails for more details. - - """ - - def __init__(self, module): - super().__init__(module) - self._init_properties() - - def _init_properties(self): - super()._init_properties() - self.properties["serial_number"] = None - - def refresh(self): - """ - Caller: __init__() - - Refresh serial_number current issu details from NDFC - """ - super().refresh() - self.data_subclass = {} - for switch in self.response_data: - self.data_subclass[switch["serialNumber"]] = switch - - def _get(self, item): - if self.serial_number is None: - msg = f"{self.class_name}: set instance.serial_number " - msg += f"before accessing property {item}." - self.module.fail_json(msg) - if self.data_subclass.get(self.serial_number) is None: - msg = f"{self.class_name}: {self.serial_number} is not " - msg += f"defined in NDFC." - self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.serial_number].get(item)) - - @property - def filtered_data(self): - """ - Return a dictionary of the switch matching self.serial_number. - Return None of the switch does not exist in NDFC. - """ - return self.data_subclass.get(self.serial_number) - - @property - def serial_number(self): - """ - Set the serial_number of the switch to query. - - This needs to be set before accessing this class's properties. - """ - return self.properties.get("serial_number") - - @serial_number.setter - def serial_number(self, value): - self.properties["serial_number"] = value - - -class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): - """ - Retrieve switch issu details from NDFC and provide property accessors - for the switch attributes retrieved by device_name. - - Usage (where module is an instance of AnsibleModule): - - instance = SwitchIssuDetailsByDeviceName(module) - instance.refresh() - instance.device_name = "leaf_1" - image_staged = instance.image_staged - image_upgraded = instance.image_upgraded - ip_address = instance.ip_address - etc... - - See SwitchIssuDetails for more details. - - """ - - def __init__(self, module): - super().__init__(module) - self._init_properties() - - def _init_properties(self): - super()._init_properties() - self.properties["device_name"] = None - - def refresh(self): - """ - Caller: __init__() - - Refresh device_name current issu details from NDFC - """ - super().refresh() - self.data_subclass = {} - for switch in self.response_data: - self.data_subclass[switch["deviceName"]] = switch - - def _get(self, item): - if self.device_name is None: - msg = f"{self.class_name}: set instance.device_name " - msg += f"before accessing property {item}." - self.module.fail_json(msg) - if self.data_subclass.get(self.device_name) is None: - msg = f"{self.class_name}: {self.device_name} is not " - msg += f"defined in NDFC." - self.module.fail_json(msg) - return self.make_none(self.data_subclass[self.device_name].get(item)) - - @property - def filtered_data(self): - """ - Return a dictionary of the switch matching self.device_name. - Return None of the switch does not exist in NDFC. - """ - return self.data_subclass.get(self.device_name) - - @property - def device_name(self): - """ - Set the device_name of the switch to query. - - This needs to be set before accessing this class's properties. - """ - return self.properties.get("device_name") - - @device_name.setter - def device_name(self, value): - self.properties["device_name"] = value - - -class ImageStage(ImageUpgradeCommon): - """ - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image - - Verb: POST - - Usage (where module is an instance of AnsibleModule): - - stage = ImageStage(module) - stage.serial_numbers = ["FDO211218HH", "FDO211218GC"] - stage.commit() - data = stage.data - - Request body (12.1.2e) (yes, serialNum is misspelled): - { - "sereialNum": [ - "FDO211218HH", - "FDO211218GC" - ] - } - Request body (12.1.3b): - { - "serialNumbers": [ - "FDO211218HH", - "FDO211218GC" - ] - } - - Response: - Unfortunately, the response does not contain consistent data. - Would be better if all responses contained serial numbers as keys so that - we could verify against a set() of serial numbers. Sigh. It is what it is. - { - 'RETURN_CODE': 200, - 'METHOD': 'POST', - 'REQUEST_PATH': 'https: //172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image', - 'MESSAGE': 'OK', - 'DATA': [ - { - 'key': 'success', - 'value': '' - }, - { - 'key': 'success', - 'value': '' - } - ] - } - - Response when there are no files to stage: - [ - { - "key": "FDO211218GC", - "value": "No files to stage" - }, - { - "key": "FDO211218HH", - "value": "No files to stage" - } - ] - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - self.serial_numbers_done = set() - self.controller_version = None - self.path = None - self.verb = None - self.payload = None - self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) - - def _init_properties(self): - self.properties = {} - self.properties["serial_numbers"] = None - self.properties["response_data"] = None - self.properties["result"] = None - self.properties["response"] = None - self.properties["check_interval"] = 10 # seconds - self.properties["check_timeout"] = 1800 # seconds - - def _populate_controller_version(self): - """ - Populate self.controller_version with the NDFC version. - - Notes: - 1. This cannot go into ImageUpgradeCommon() due to circular - imports resulting in RecursionError - """ - instance = ControllerVersion(self.module) - instance.refresh() - self.controller_version = instance.version - - def prune_serial_numbers(self): - """ - If the image is already staged on a switch, remove that switch's - serial number from the list of serial numbers to stage. - """ - serial_numbers = copy.copy(self.serial_numbers) - for serial_number in serial_numbers: - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - if self.issu_detail.image_staged == "Success": - msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " - msg += "image already staged for " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}." - self.log_msg(msg) - self.serial_numbers.remove(serial_number) - - def validate_serial_numbers(self): - """ - Fail if the image_staged state for any serial_number - is Failed. - """ - for serial_number in self.serial_numbers: - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - if self.issu_detail.image_staged == "Failed": - msg = "Image staging is failing for the following switch: " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}. " - msg += f"Check the switch connectivity to NDFC " - msg += "and try again." - self.module.fail_json(msg) - else: - self.log_msg(f"Image staging is not failing for the following switch: {self.issu_detail.serial_number}") - - def commit(self): - """ - Commit the image staging request to NDFC and wait - for the images to be staged. - """ - if self.serial_numbers is None: - msg = f"{self.class_name}.commit() call instance.serial_numbers " - msg += "before calling commit()." - self.module.fail_json(msg) - if len(self.serial_numbers) == 0: - msg = f"REMOVE: {self.class_name}.commit() no serial numbers to stage." - self.log_msg(msg) - return - self.prune_serial_numbers() - self.validate_serial_numbers() - self._wait_for_current_actions_to_complete() - - self.path = self.endpoints.image_stage.get("path") - self.verb = self.endpoints.image_stage.get("verb") - - self.log_msg(f"REMOVE: {self.class_name}.commit() path: {self.path}") - self.log_msg(f"REMOVE: {self.class_name}.commit() verb: {self.verb}") - self.payload = {} - self._populate_controller_version() - if self.controller_version == "12.1.2e": - # Yes, NDFC 12.1.2e wants serialNum to be misspelled - self.payload["sereialNum"] = self.serial_numbers - else: - self.payload["serialNumbers"] = self.serial_numbers - self.properties["response"] = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(self.payload) - ) - self.properties["result"] = self._handle_response(self.response, self.verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.response}" - ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") - if not self.result["success"]: - msg = f"{self.class_name}.commit() failed: {self.result}. " - msg += f"NDFC response was: {self.response}" - self.module.fail_json(msg) - self.properties["response_data"] = self.response.get("DATA") - self._wait_for_image_stage_to_complete() - - def _wait_for_current_actions_to_complete(self): - """ - NDFC will not stage an image if there are any actions in progress. - Wait for all actions to complete before staging image. - Actions include image staging, image upgrade, and image validation. - """ - self.serial_numbers_done = set() - serial_numbers_todo = set(copy.copy(self.serial_numbers)) - timeout = self.check_timeout - while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - for serial_number in self.serial_numbers: - if serial_number in self.serial_numbers_done: - continue - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} actions in progress: " - msg += f"{self.issu_detail.actions_in_progress}, " - msg += f"{timeout} seconds remaining." - self.log_msg(msg) - if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) - self.serial_numbers_done.add(serial_number) - if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"Timed out waiting for actions to complete. " - msg += f"serial_numbers_done: " - msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " - msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) - self.module.fail_json(msg) - - def _wait_for_image_stage_to_complete(self): - """ - # Wait for image stage to complete - """ - # We're promoting serial_numbers_done to a class-level attribute - # so that it can be used in unit test asserts. - self.serial_numbers_done = set() - timeout = self.check_timeout - serial_numbers_todo = set(copy.copy(self.serial_numbers)) - while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" - self.log_msg(msg) - for serial_number in self.serial_numbers: - if serial_number in self.serial_numbers_done: - continue - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - ip_address = self.issu_detail.ip_address - device_name = self.issu_detail.device_name - staged_percent = self.issu_detail.image_staged_percent - staged_status = self.issu_detail.image_staged - - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Seconds remaining {timeout}: " - msg += f"{device_name}, {serial_number}, {ip_address} " - msg += f"image staged percent: {staged_percent}" - self.log_msg(msg) - - if staged_status == "Failed": - msg = f"Seconds remaining {timeout}: stage image failed " - msg += f"for {device_name}, {serial_number}, {ip_address}. " - msg += f"image staged percent: {staged_percent}" - self.module.fail_json(msg) - - if staged_status == "Success": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Seconds remaining {timeout}: stage image complete for " - msg += f"for {device_name}, {serial_number}, {ip_address}. " - msg += f"image staged percent: {staged_percent}" - self.log_msg(msg) - self.serial_numbers_done.add(serial_number) - - # if staged_status == None: - # msg = f"REMOVE: {self.class_name}." - # msg += "_wait_for_image_stage_to_complete: " - # msg += f"Seconds remaining {timeout}: stage image " - # msg += "not started for " - # msg += f"{device_name}, {serial_number}, {ip_address}. " - # msg += f"image staged percent: {staged_percent}" - # self.log_msg(msg) - - # if staged_status == "In Progress": - # msg = f"REMOVE: {self.class_name}." - # msg += "_wait_for_image_stage_to_complete: " - # msg += f"Seconds remaining {timeout}: stage image " - # msg += f"{staged_status} for " - # msg += f"{device_name}, {serial_number}, {ip_address}. " - # msg += f"image staged percent: {staged_percent}" - # self.log_msg(msg) - if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_image_stage_to_complete: " - msg += f"Timed out waiting for image stage to complete. " - msg += f"serial_numbers_done: " - msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " - msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) - self.module.fail_json(msg) - - @property - def serial_numbers(self): - """ - Set the serial numbers of the switches to stage. - - This must be set before calling instance.commit() - """ - return self.properties.get("serial_numbers") - - @serial_numbers.setter - def serial_numbers(self, value): - if not isinstance(value, list): - msg = f"{self.__class__.__name__}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." - self.module.fail_json(msg) - self.properties["serial_numbers"] = value - - @property - def response_data(self): - """ - Return the result of the image staging request - for serial_numbers. - - instance.serial_numbers must be set first. - """ - return self.properties.get("response_data") - - @property - def result(self): - """ - Return the POST result from NDFC - """ - return self.properties.get("result") - - @property - def response(self): - """ - Return the POST response from NDFC - """ - return self.properties.get("response") - - @property - def check_interval(self): - """ - Return the stage check interval in seconds - """ - return self.properties.get("check_interval") - - @check_interval.setter - def check_interval(self, value): - if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_interval must " - msg += f"be an integer." - self.module.fail_json(msg) - self.properties["check_interval"] = value - - @property - def check_timeout(self): - """ - Return the stage check timeout in seconds - """ - return self.properties.get("check_timeout") - - @check_timeout.setter - def check_timeout(self, value): - if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_timeout must " - msg += f"be an integer." - self.module.fail_json(msg) - self.properties["check_timeout"] = value - -# ============================================================================== -class ImageValidate(ImageUpgradeCommon): - """ - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image - - Verb: POST - - Usage (where module is an instance of AnsibleModule): - - instance = ImageValidate(module) - instance.serial_numbers = ["FDO211218HH", "FDO211218GC"] - # non_disruptive is optional - instance.non_disruptive = True - instance.commit() - data = instance.response_data - - Request body: - { - "serialNum": ["FDO21120U5D"], - "nonDisruptive":"true" - } - - Response body when nonDisruptive is True: - [StageResponse [key=success, value=]] - - Response body when nonDisruptive is False: - [StageResponse [key=success, value=]] - - The response is not JSON, nor is it very useful. - Instead, we poll for validation status using - SwitchIssuDetailsBySerialNumber. - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) - - def _init_properties(self): - self.properties = {} - self.properties["check_interval"] = 10 # seconds - self.properties["check_timeout"] = 1800 # seconds - self.properties["response_data"] = None - self.properties["result"] = None - self.properties["response"] = None - self.properties["non_disruptive"] = False - self.properties["serial_numbers"] = None - - def _populate_controller_version(self): - """ - Populate self.controller_version with the NDFC version. - - TODO:3 Remove if 12.1.3b works with no changes to request/response payloads. - - Notes: - 1. This cannot go into ImageUpgradeCommon() due to circular - imports resulting in RecursionError - """ - instance = ControllerVersion(self.module) - instance.refresh() - self.controller_version = instance.version - - def prune_serial_numbers(self): - """ - If the image is already validated on a switch, remove that switch's - serial number from the list of serial numbers to validate. - """ - serial_numbers = copy.copy(self.serial_numbers) - for serial_number in serial_numbers: - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - if self.issu_detail.validated == "Success": - msg = f"REMOVE: {self.class_name}.prune_serial_numbers: " - msg += "image already validated for " - msg += f"{self.issu_detail.serial_number}, " - msg += f"{self.issu_detail.ip_address}" - self.log_msg(msg) - self.serial_numbers.remove(self.issu_detail.serial_number) - - def validate_serial_numbers(self): - """ - Log a warning if the validated state for any serial_number - is Failed. - - TODO:1 Need a way to compare current image_policy with the image policy in the response - TODO:3 If validate == Failed, it may have been from the last operation. - TODO:3 We can't fail here based on this until we can verify the failure is happening for the current image_policy. - TODO:3 Change this to a log message and update the unit test if we can't verify the failure is happening for the current image_policy. - """ - for serial_number in self.serial_numbers: - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - if self.issu_detail.validated == "Failed": - msg = f"{self.class_name}.validate_serial_numbers: " - msg += "image validation is failing for the following switch: " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}. " - msg += "If this persists, check the switch connectivity to NDFC and " - msg += "try again." - #self.log_msg(msg) - self.module.fail_json(msg) - - def build_payload(self): - self.payload = {} - self.payload["serialNum"] = self.serial_numbers - self.payload["nonDisruptive"] = self.non_disruptive - - def commit(self): - """ - Commit the image validation request to NDFC and wait - for the images to be validated. - """ - if self.serial_numbers is None: - msg = f"{self.class_name}.commit() call instance.serial_numbers " - msg += "before calling commit()." - self.module.fail_json(msg) - if len(self.serial_numbers) == 0: - msg = f"REMOVE: {self.class_name}.commit() no serial numbers " - msg += "to validate." - self.log_msg(msg) - return - self.prune_serial_numbers() - self.validate_serial_numbers() - self._wait_for_current_actions_to_complete() - path = self.endpoints.image_validate.get("path") - verb = self.endpoints.image_validate.get("verb") - self.build_payload() - self.properties["response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) - ) - self.properties["result"] = self._handle_response(self.response, verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.response}" - ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") - if not self.result["success"]: - msg = f"{self.class_name}.commit() failed: {self.result}. " - msg += f"NDFC response was: {self.response}" - self.module.fail_json(msg) - self.properties["response_data"] = self.response.get("DATA") - self._wait_for_image_validate_to_complete() - - def _wait_for_current_actions_to_complete(self): - """ - NDFC will not validate an image if there are any actions in progress. - Wait for all actions to complete before validating image. - Actions include image staging, image upgrade, and image validation. - """ - self.serial_numbers_done = set() - serial_numbers_todo = set(copy.copy(self.serial_numbers)) - timeout = self.check_timeout - while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - for serial_number in self.serial_numbers: - if serial_number in self.serial_numbers_done: - continue - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} actions in progress: " - msg += f"{self.issu_detail.actions_in_progress}, " - msg += f"{timeout} seconds remaining." - self.log_msg(msg) - if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{serial_number} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) - self.serial_numbers_done.add(serial_number) - if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"Timed out waiting for actions to complete. " - msg += f"serial_numbers_done: " - msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " - msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) - self.module.fail_json(msg) - - def _wait_for_image_validate_to_complete(self): - """ - Wait for image validation to complete - """ - # We're promiting serial_numbers_done to a class-level attribute - # so that it can be used in unit test asserts. - self.serial_numbers_done = set() - timeout = self.check_timeout - serial_numbers_todo = set(copy.copy(self.serial_numbers)) - while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_todo: {sorted(list(serial_numbers_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"seconds remaining: {timeout}, " - msg += f"serial_numbers_done: {sorted(list(self.serial_numbers_done))}" - self.log_msg(msg) - for serial_number in self.serial_numbers: - if serial_number in self.serial_numbers_done: - continue - self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() - ip_address = self.issu_detail.ip_address - device_name = self.issu_detail.device_name - validated_percent = self.issu_detail.validated_percent - validated_status = self.issu_detail.validated - - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"validated_percent: {validated_percent} " - msg += f"validated_state: {validated_status}" - self.log_msg(msg) - - if validated_status == "Failed": - msg = f"Seconds remaining {timeout}: validate image " - msg += f"{validated_status} for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}. " - msg += "Check the switch e.g. show install log detail, " - msg += "show incompatibility-all nxos . Or " - msg += "check NDFC Operations > Image Management > " - msg += "Devices > View Details > Validate for " - msg += "more details." - self.module.fail_json(msg) - - if validated_status == "Success": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += f"{validated_status} for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) - self.serial_numbers_done.add(serial_number) - - if validated_status == None: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += "not started for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) - - if validated_status == "In Progress": - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Seconds remaining {timeout}: validate image " - msg += f"{validated_status} for " - msg += f"{device_name}, {ip_address}, {serial_number}, " - msg += f"image validated percent: {validated_percent}" - self.log_msg(msg) - if self.serial_numbers_done != serial_numbers_todo: - msg = f"{self.class_name}." - msg += "_wait_for_image_validate_to_complete: " - msg += f"Timed out waiting for image validation to complete. " - msg += f"serial_numbers_done: " - msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " - msg += f"{','.join(sorted(serial_numbers_todo))}" - self.log_msg(msg) - self.module.fail_json(msg) - - @property - def serial_numbers(self): - """ - Set the serial numbers of the switches to stage. - - This must be set before calling instance.commit() - """ - return self.properties.get("serial_numbers") - - @serial_numbers.setter - def serial_numbers(self, value): - if not isinstance(value, list): - msg = f"{self.__class__.__name__}: instance.serial_numbers must " - msg += f"be a python list of switch serial numbers." - self.module.fail_json(msg) - self.properties["serial_numbers"] = value - - @property - def non_disruptive(self): - """ - Set the non_disruptive flag to True or False. - """ - return self.properties.get("non_disruptive") - - @non_disruptive.setter - def non_disruptive(self, value): - value = self.make_boolean(value) - if not isinstance(value, bool): - msg = f"{self.class_name}.non_disruptive: " - msg += "instance.non_disruptive must " - msg += f"be a boolean. Got {value}." - self.module.fail_json(msg) - self.properties["non_disruptive"] = value - - @property - def response_data(self): - """ - Return the result of the image staging request - for serial_numbers. - - instance.serial_numbers must be set first. - """ - return self.properties.get("response_data") - - @property - def result(self): - """ - Return the POST result from NDFC - """ - return self.properties.get("result") - - @property - def response(self): - """ - Return the POST response from NDFC - """ - return self.properties.get("response") - - @property - def check_interval(self): - """ - Return the validate check interval in seconds - """ - return self.properties.get("check_interval") - - @check_interval.setter - def check_interval(self, value): - if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_interval must " - msg += f"be an integer." - self.module.fail_json(msg) - self.properties["check_interval"] = value - - @property - def check_timeout(self): - """ - Return the validate check timeout in seconds - """ - return self.properties.get("check_timeout") - - @check_timeout.setter - def check_timeout(self, value): - if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_timeout must " - msg += f"be an integer." - self.module.fail_json(msg) - self.properties["check_timeout"] = value - - -# ============================================================================== -class ImageUpgrade(ImageUpgradeCommon): - """ - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image - Verb: POST - - TODO:3 Discuss with Mike/Shangxin. ImageUpgrade.epld_upgrade, etc - - Usage (where module is an instance of AnsibleModule): - - upgrade = ImageUpgrade(module) - upgrade.devices = devices - upgrade.commit() - data = upgrade.data - - Where devices is a list of dict. Example structure: - - [ - { - 'policy': 'KR3F', - 'ip_address': '172.22.150.102', - 'policy_changed': False - 'stage': False, - 'validate': True, - 'upgrade': { - 'nxos': True, - 'epld': False - }, - 'options': { - 'nxos': { - 'mode': 'non_disruptive' - 'bios_force': False - }, - 'epld': { - 'module': 'ALL', - 'golden': False - }, - 'reboot': { - 'config_reload': False, - 'write_erase': False - }, - 'package': { - 'install': False, - 'uninstall': False - } - }, - }, - etc... - ] - - Request body: - Yes, the keys below are misspelled in the request body: - pacakgeInstall - pacakgeUnInstall - - { - "devices": [ - { - "serialNumber": "FDO211218HH", - "policyName": "NR1F" - } - ], - "issuUpgrade": true, - "issuUpgradeOptions1": { - "nonDisruptive": true, - "forceNonDisruptive": false, - "disruptive": false - }, - "issuUpgradeOptions2": { - "biosForce": false - }, - "epldUpgrade": false, - "epldOptions": { - "moduleNumber": "ALL", - "golden": false - }, - "reboot": false, - "rebootOptions": { - "configReload": "false", - "writeErase": "false" - }, - "pacakgeInstall": false, - "pacakgeUnInstall": false - } - Response bodies: - Responses are text, not JSON, and are returned immediately. - They do not contain useful information. We need to poll NDFC - to determine when the upgrade is complete. Basically, we ignore - these responses in favor of the poll responses. - - If an action is in progress, text is returned: - "Action in progress for some of selected device(s). Please try again after completing current action." - - If an action is not in progress, text is returned: - "3" - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - # Maximum number of modules/linecards in a switch - self.max_module_number = 9 - - self._init_defaults() - self._init_properties() - self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) - - def _init_defaults(self): - self.defaults = {} - self.defaults["reboot"] = False - self.defaults["stage"] = True - self.defaults["validate"] = True - self.defaults["upgrade"] = {} - self.defaults["upgrade"]["nxos"] = True - self.defaults["upgrade"]["epld"] = False - self.defaults["options"] = {} - self.defaults["options"]["nxos"] = {} - self.defaults["options"]["nxos"]["mode"] = "disruptive" - self.defaults["options"]["nxos"]["bios_force"] = False - self.defaults["options"]["epld"] = {} - self.defaults["options"]["epld"]["module"] = "ALL" - self.defaults["options"]["epld"]["golden"] = False - self.defaults["options"]["reboot"] = {} - self.defaults["options"]["reboot"]["config_reload"] = False - self.defaults["options"]["reboot"]["write_erase"] = False - self.defaults["options"]["package"] = {} - self.defaults["options"]["package"]["install"] = False - self.defaults["options"]["package"]["uninstall"] = False - - def _init_properties(self): - # self.ip_addresses is used in: - # self._wait_for_current_actions_to_complete() - # self._wait_for_image_upgrade_to_complete() - self.ip_addresses = set() - # TODO:1 Review these properties since we are no longer - # calling this class per-switch given the payload structure - # is not amenable to that. - self.properties = {} - self.properties["bios_force"] = False - self.properties["check_interval"] = 10 # seconds - self.properties["check_timeout"] = 1800 # seconds - self.properties["config_reload"] = False - self.properties["devices"] = None - self.properties["disruptive"] = True - self.properties["epld_golden"] = False - self.properties["epld_module"] = "ALL" - self.properties["epld_upgrade"] = False - self.properties["force_non_disruptive"] = False - self.properties["response_data"] = None - self.properties["result"] = None - self.properties["response"] = None - self.properties["non_disruptive"] = False - self.properties["package_install"] = False - self.properties["package_uninstall"] = False - self.properties["reboot"] = False - self.properties["write_erase"] = False - - self.valid_epld_module = set() - self.valid_epld_module.add("ALL") - for module in range(1, self.max_module_number + 1): - self.valid_epld_module.add(str(module)) - - self.valid_nxos_mode = set() - self.valid_nxos_mode.add("disruptive") - self.valid_nxos_mode.add("non_disruptive") - self.valid_nxos_mode.add("force_non_disruptive") - - - # def prune_devices(self): - # """ - # If the image is already upgraded on a device, remove that device - # from self.devices. self.devices dict has already been validated, - # so no further error checking is needed here. - - # TODO:1 This prunes devices only based on the image upgrade state. - # TODO:1 It does not check other image states and EPLD states. - # """ - # # issu = SwitchIssuDetailsBySerialNumber(self.module) - # pruned_devices = set() - # instance = SwitchIssuDetailsByIpAddress(self.module) - # instance.refresh() - # for device in self.devices: - # msg = f"REMOVE: {self.class_name}.prune_devices() device: {device}" - # self.log_msg(msg) - # instance.ip_address = device.get("ip_address") - # instance.refresh() - # if instance.upgrade == "Success": - # msg = f"REMOVE: {self.class_name}.prune_devices: " - # msg = "image already upgraded for " - # msg += f"{instance.device_name}, " - # msg += f"{instance.serial_number}, " - # msg += f"{instance.ip_address}" - # self.log_msg(msg) - # pruned_devices.add(instance.ip_address) - # self.devices = [ - # device - # for device in self.devices - # if device.get("ip_address") not in pruned_devices - # ] - - def validate_devices(self): - """ - Fail if the upgrade state for any device is Failed. - """ - for device in self.devices: - self.issu_detail.ip_address = device.get("ip_address") - self.issu_detail.refresh() - if self.issu_detail.upgrade == "Failed": - msg = f"{self.class_name}.validate_devices: Image upgrade is " - msg += "failing for the following switch: " - msg += f"{self.issu_detail.device_name}, " - msg += f"{self.issu_detail.ip_address}, " - msg += f"{self.issu_detail.serial_number}. " - msg += "Please check the switch " - msg += "to determine the cause and try again." - self.module.fail_json(msg) - # used in self._wait_for_current_actions_to_complete() - self.ip_addresses.add(self.issu_detail.ip_address) - - def _merge_defaults_to_switch_config(self, config): - if config.get("stage") is None: - config["stage"] = self.defaults["stage"] - if config.get("reboot") is None: - config["reboot"] = self.defaults["reboot"] - if config.get("validate") is None: - config["validate"] = self.defaults["validate"] - if config.get("upgrade") is None: - config["upgrade"] = self.defaults["upgrade"] - if config.get("upgrade").get("nxos") is None: - config["upgrade"]["nxos"] = self.defaults["upgrade"]["nxos"] - if config.get("upgrade").get("epld") is None: - config["upgrade"]["epld"] = self.defaults["upgrade"]["epld"] - if config.get("options") is None: - config["options"] = self.defaults["options"] - if config["options"].get("nxos") is None: - config["options"]["nxos"] = self.defaults["options"]["nxos"] - if config["options"]["nxos"].get("mode") is None: - config["options"]["nxos"]["mode"] = self.defaults["options"]["nxos"]["mode"] - if config["options"]["nxos"].get("bios_force") is None: - config["options"]["nxos"]["bios_force"] = self.defaults["options"]["nxos"]["bios_force"] - if config["options"].get("epld") is None: - config["options"]["epld"] = self.defaults["options"]["epld"] - if config["options"]["epld"].get("module") is None: - config["options"]["epld"]["module"] = self.defaults["options"]["epld"]["module"] - if config["options"]["epld"].get("golden") is None: - config["options"]["epld"]["golden"] = self.defaults["options"]["epld"]["golden"] - if config["options"].get("reboot") is None: - config["options"]["reboot"] = self.defaults["options"]["reboot"] - if config["options"]["reboot"].get("config_reload") is None: - config["options"]["reboot"]["config_reload"] = self.defaults["options"]["reboot"]["config_reload"] - if config["options"]["reboot"].get("write_erase") is None: - config["options"]["reboot"]["write_erase"] = self.defaults["options"]["reboot"]["write_erase"] - if config["options"].get("package") is None: - config["options"]["package"] = self.defaults["options"]["package"] - if config["options"]["package"].get("install") is None: - config["options"]["package"]["install"] = self.defaults["options"]["package"]["install"] - if config["options"]["package"].get("uninstall") is None: - config["options"]["package"]["uninstall"] = self.defaults["options"]["package"]["uninstall"] - return config - - def build_payload(self, device): - """ - Build the request payload to upgrade the switches. - """ - msg = f"REMOVE: {self.class_name}.build_payload() PRE_DEFAULTS: " - msg += f"device: {device}" - self.log_msg(msg) - device = self._merge_defaults_to_switch_config(device) - msg = f"REMOVE: {self.class_name}.build_payload() POST_DEFAULTS: " - msg += f"device: {device}" - self.log_msg(msg) - - - # devices_to_upgrade must currently be a single device - devices_to_upgrade = [] - self.issu_detail.ip_address = device.get("ip_address") - self.issu_detail.refresh() - payload_device = {} - payload_device["serialNumber"] = self.issu_detail.serial_number - payload_device["policyName"] = device.get("policy") - devices_to_upgrade.append(payload_device) - - self.payload = {} - self.payload["devices"] = devices_to_upgrade - self.payload["issuUpgrade"] = device.get("upgrade").get("nxos") - - # nxos_mode: The choices for nxos_mode are mutually-exclusive. - # If one is set to True, the others must be False. - # nonDisruptive corresponds to NDFC Allow Non-Disruptive GUI option - self.payload["issuUpgradeOptions1"] = {} - self.payload["issuUpgradeOptions1"]["nonDisruptive"] = False - self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = False - self.payload["issuUpgradeOptions1"]["disruptive"] = False - - nxos_mode = device.get("options").get("nxos").get("mode") - if nxos_mode not in self.valid_nxos_mode: - msg = f"{self.class_name}.build_payload() options.nxos.mode must " - msg += f"be one of {self.valid_nxos_mode}. Got {nxos_mode}." - self.module.fail_json(msg) - if nxos_mode == "non_disruptive": - self.payload["issuUpgradeOptions1"]["nonDisruptive"] = True - if nxos_mode == "disruptive": - self.payload["issuUpgradeOptions1"]["disruptive"] = True - if nxos_mode == "force_non_disruptive": - self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True - - # biosForce corresponds to NDFC BIOS Force GUI option - bios_force = device.get("options").get("nxos").get("bios_force") - if not isinstance(bios_force, bool): - msg = f"{self.class_name}.build_payload() options.nxos.bios_force " - msg += f"must be a boolean. Got {bios_force}." - self.module.fail_json(msg) - self.payload["issuUpgradeOptions2"] = {} - self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force - - # EPLD - epld_module = device.get("options").get("epld").get("module") - epld_golden = device.get("options").get("epld").get("golden") - if epld_module not in self.valid_epld_module: - msg = f"{self.class_name}.build_payload() options.epld.module must " - msg += f"be one of {self.valid_epld_module}. Got {epld_module}." - self.module.fail_json(msg) - if not isinstance(epld_golden, bool): - msg = f"{self.class_name}.build_payload() options.epld.golden " - msg += f"must be a boolean. Got {epld_golden}." - self.module.fail_json(msg) - self.payload["epldUpgrade"] = device.get("upgrade").get("epld") - self.payload["epldOptions"] = {} - self.payload["epldOptions"]["moduleNumber"] = epld_module - self.payload["epldOptions"]["golden"] = epld_golden - - # Reboot - reboot = device.get("reboot") - if not isinstance(reboot, bool): - msg = f"{self.class_name}.build_payload() reboot must " - msg += f"be a boolean. Got {reboot}." - self.module.fail_json(msg) - self.payload["reboot"] = reboot - - # Reboot options - config_reload = device.get("options").get("reboot").get("config_reload") - write_erase = device.get("options").get("reboot").get("write_erase") - if not isinstance(config_reload, bool): - msg = f"{self.class_name}.build_payload() options.reboot.config_reload " - msg += f"must be a boolean. Got {config_reload}." - self.module.fail_json(msg) - if not isinstance(write_erase, bool): - msg = f"{self.class_name}.build_payload() options.reboot.write_erase " - msg += f"must be a boolean. Got {write_erase}." - self.module.fail_json(msg) - self.payload["rebootOptions"] = {} - self.payload["rebootOptions"]["configReload"] = config_reload - self.payload["rebootOptions"]["writeErase"] = write_erase - - # Packages - package_install = device.get("options").get("package").get("install") - package_uninstall = device.get("options").get("package").get("uninstall") - if not isinstance(package_install, bool): - msg = f"{self.class_name}.build_payload() options.package.install " - msg += f"must be a boolean. Got {package_install}." - self.module.fail_json(msg) - if not isinstance(package_uninstall, bool): - msg = f"{self.class_name}.build_payload() options.package.uninstall " - msg += f"must be a boolean. Got {package_uninstall}." - self.module.fail_json(msg) - self.payload["pacakgeInstall"] = package_install - self.payload["pacakgeUnInstall"] = package_uninstall - - def commit(self): - """ - Commit the image upgrade request to NDFC and wait - for the images to be upgraded. - """ - if self.devices is None: - msg = f"{self.class_name}.commit() call instance.devices " - msg += "before calling commit()." - self.module.fail_json(msg) - if len(self.devices) == 0: - msg = f"REMOVE: {self.class_name}.commit() no devices to upgrade." - self.log_msg(msg) - return - #self.prune_devices() - self.validate_devices() - self._wait_for_current_actions_to_complete() - path = self.endpoints.image_upgrade.get("path") - verb = self.endpoints.image_upgrade.get("verb") - for device in self.devices: - self.build_payload(device) - self.log_msg(f"REMOVE: {self.class_name}.commit() upgrade payload: {self.payload}") - self.properties["response"] = dcnm_send( - self.module, verb, path, data=json.dumps(self.payload) - ) - self.properties["result"] = self._handle_response(self.response, verb) - self.log_msg( - f"REMOVE: {self.class_name}.commit() response: {self.response}" - ) - self.log_msg(f"REMOVE: {self.class_name}.commit() result: {self.result}") - if not self.result["success"]: - msg = f"{self.class_name}.commit() failed: {self.result}. " - msg += f"NDFC response was: {self.response}" - self.module.fail_json(msg) - self.properties["response_data"] = self.response.get("DATA") - self._wait_for_image_upgrade_to_complete() - - def _wait_for_current_actions_to_complete(self): - """ - NDFC will not upgrade an image if there are any actions in progress. - Wait for all actions to complete before upgrading image. - Actions include image staging, image upgrade, and image validation. - """ - ipv4_todo = copy.copy(self.ip_addresses) - timeout = self.check_timeout - while len(ipv4_todo) > 0 and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - for ipv4 in self.ip_addresses: - if ipv4 not in ipv4_todo: - continue - self.issu_detail.ip_address = ipv4 - self.issu_detail.refresh() - if self.issu_detail.actions_in_progress is False: - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{ipv4} no actions in progress. " - msg += f"OK to proceed. {timeout} seconds remaining." - self.log_msg(msg) - ipv4_todo.remove(ipv4) - continue - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_current_actions_to_complete: " - msg += f"{ipv4} actions in progress. " - msg += f"Waiting. {timeout} seconds remaining." - self.log_msg(msg) - - def _wait_for_image_upgrade_to_complete(self): - """ - Wait for image upgrade to complete - """ - ipv4_done = set() - timeout = self.check_timeout - ipv4_todo = set(copy.copy(self.ip_addresses)) - while ipv4_done != ipv4_todo and timeout > 0: - sleep(self.check_interval) - timeout -= self.check_interval - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"seconds remaining {timeout}, " - msg += f"ipv4_todo: {sorted(list(ipv4_todo))}" - self.log_msg(msg) - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"seconds remaining {timeout}, " - msg += f"ipv4_done: {sorted(list(ipv4_done))}" - self.log_msg(msg) - for ipv4 in self.ip_addresses: - if ipv4 in ipv4_done: - continue - self.issu_detail.ip_address = ipv4 - self.issu_detail.refresh() - ip_address = self.issu_detail.ip_address - device_name = self.issu_detail.device_name - upgrade_percent = self.issu_detail.upgrade_percent - upgrade_status = self.issu_detail.upgrade - serial_number = self.issu_detail.serial_number - - if upgrade_status == "Failed": - msg = f"{self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"Seconds remaining {timeout}: upgrade image " - msg += f"{upgrade_status} for " - msg += f"{device_name}, {serial_number}, {ip_address}" - self.module.fail_json(msg) - - if upgrade_status == "Success": - ipv4_done.add(ipv4) - status = "succeeded" - if upgrade_status == None: - status = "not started" - if upgrade_status == "In-Progress": - status = "in progress" - - msg = f"REMOVE: {self.class_name}." - msg += "_wait_for_image_upgrade_to_complete: " - msg += f"Seconds remaining {timeout}, " - msg += f"Percent complete {upgrade_percent}, " - msg += f"Status {status}, " - msg += f"{device_name}, {serial_number}, {ip_address}" - self.log_msg(msg) - - if ipv4_done != ipv4_todo: - msg = f"{self.class_name}._wait_for_image_upgrade_to_complete(): " - msg += "The following device(s) did not complete upgrade: " - msg += f"{ipv4_todo.difference(ipv4_done)}. " - msg += "Try increasing issu timeout in the playbook, or check " - msg += "the device(s) to determine the cause " - msg += "(e.g. show install all status)." - self.module.fail_json(msg) - - # setter properties - @property - def bios_force(self): - """ - Set the bios_force flag to True or False. - - Default: False - """ - return self.properties.get("bios_force") - - @bios_force.setter - def bios_force(self, value): - name = "bios_force" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def config_reload(self): - """ - Set the config_reload flag to True or False. - - Default: False - """ - return self.properties.get("config_reload") - - @config_reload.setter - def config_reload(self, value): - name = "config_reload" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def devices(self): - """ - Set the devices to upgrade. - - list() of dict() with the following structure: - { - "serial_number": "FDO211218HH", - "policy_name": "NR1F" - } - - Must be set before calling instance.commit() - """ - return self.properties.get("devices") - - @devices.setter - def devices(self, value): - name = "devices" - if not isinstance(value, list): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a python list of dict." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def disruptive(self): - """ - Set the disruptive flag to True or False. - - Default: False - """ - return self.properties.get("disruptive") - - @disruptive.setter - def disruptive(self, value): - name = "disruptive" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def epld_golden(self): - """ - Set the epld_golden flag to True or False. - - Default: False - """ - return self.properties.get("epld_golden") - - @epld_golden.setter - def epld_golden(self, value): - name = "epld_golden" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def epld_upgrade(self): - """ - Set the epld_upgrade flag to True or False. - - Default: False - """ - return self.properties.get("epld_upgrade") - - @epld_upgrade.setter - def epld_upgrade(self, value): - name = "epld_upgrade" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def epld_module(self): - """ - Set the epld_module to upgrade. - - Ignored if epld_upgrade is set to False - Valid values: integer or "ALL" - Default: "ALL" - """ - return self.properties.get("epld_module") - - @epld_module.setter - def epld_module(self, value): - name = "epld_module" - try: - value = value.upper() - except AttributeError: - pass - if not isinstance(value, int) and value != "ALL": - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be an integer or 'ALL'" - self.module.fail_json(msg) - self.properties[name] = value - - @property - def force_non_disruptive(self): - """ - Set the force_non_disruptive flag to True or False. - - Default: False - """ - return self.properties.get("force_non_disruptive") - - @force_non_disruptive.setter - def force_non_disruptive(self, value): - name = "force_non_disruptive" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def non_disruptive(self): - """ - Set the non_disruptive flag to True or False. - - Default: True - """ - return self.properties.get("non_disruptive") - - @non_disruptive.setter - def non_disruptive(self, value): - name = "non_disruptive" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def package_install(self): - """ - Set the package_install flag to True or False. - - Default: False - """ - return self.properties.get("package_install") - - @package_install.setter - def package_install(self, value): - name = "package_install" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def package_uninstall(self): - """ - Set the package_uninstall flag to True or False. - - Default: False - """ - return self.properties.get("package_uninstall") - - @package_uninstall.setter - def package_uninstall(self, value): - name = "package_uninstall" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def reboot(self): - """ - Set the reboot flag to True or False. - - Default: False - """ - return self.properties.get("reboot") - - @reboot.setter - def reboot(self, value): - name = "reboot" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - @property - def write_erase(self): - """ - Set the write_erase flag to True or False. - - Default: False - """ - return self.properties.get("write_erase") - - @write_erase.setter - def write_erase(self, value): - name = "write_erase" - if not isinstance(value, bool): - msg = f"{self.class_name}.{name}.setter: " - msg += f"instance.{name} must be a boolean." - self.module.fail_json(msg) - self.properties[name] = value - - - # getter properties - @property - def check_interval(self): - """ - Return the image upgrade check interval in seconds - """ - return self.properties.get("check_interval") - - @property - def check_timeout(self): - """ - Return the image upgrade check timeout in seconds - """ - return self.properties.get("check_timeout") - - @property - def response_data(self): - """ - Return the data retrieved from NDFC for the image upgrade request. - - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("response_data") - - @property - def result(self): - """ - Return the POST result from NDFC - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("result") - - @property - def response(self): - """ - Return the POST response from NDFC - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("response") - - @property - def serial_numbers(self): - """ - Return a list of serial numbers from self.devices - """ - return [device.get("serial_number") for device in self.devices] - - -class ControllerVersion(ImageUpgradeCommon): - """ - Return image version information from the controller - - NOTES: - 1. considered using dcnm_version_supported() but it does not return - minor release info, which is needed due to key changes between - 12.1.2e and 12.1.3b. For example, see ImageStage().commit() - - Endpoint: - /appcenter/cisco/ndfc/api/v1/fm/about/version - - Usage (where module is an instance of AnsibleModule): - - instance = ControllerVersion(module) - instance.refresh() - if instance.version == "12.1.2e": - do 12.1.2e stuff - else: - do other stuff - - Response: - { - "version": "12.1.2e", - "mode": "LAN", - "isMediaController": false, - "dev": false, - "isHaEnabled": false, - "install": "EASYFABRIC", - "uuid": "f49e6088-ad4f-4406-bef6-2419de914ff1", - "is_upgrade_inprogress": false - } - """ - - def __init__(self, module): - super().__init__(module) - self.class_name = self.__class__.__name__ - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["data"] = None - self.properties["result"] = None - self.properties["response"] = None - - def refresh(self): - """ - Refresh self.response_data with current version info from NDFC - """ - path = self.endpoints.controller_version.get("path") - verb = self.endpoints.controller_version.get("verb") - self.properties["response"] = dcnm_send(self.module, verb, path) - self.properties["result"] = self._handle_response(self.response, verb) - - msg = f"REMOVE: {self.class_name}.refresh() response: {self.response}" - self.log_msg(msg) - - msg = f"REMOVE: {self.class_name}.refresh() result: {self.result}" - self.log_msg(msg) - - if self.result["success"] == False or self.result["found"] == False: - msg = f"{self.class_name}.refresh() failed: {self.result}" - self.module.fail_json(msg) - - self.properties["response_data"] = self.response.get("DATA") - if self.response_data is None: - msg = f"{self.class_name}.refresh() failed: NDFC response " - msg += "does not contain DATA key. NDFC response: " - msg += f"{self.response}" - self.module.fail_json(msg) - - msg = f"REMOVE: {self.class_name}.refresh() response_data: {self.response_data}" - self.log_msg(msg) - - def _get(self, item): - return self.make_boolean(self.make_none(self.response_data.get(item))) - - @property - def dev(self): - """ - Return True if NDFC is a development release. - Return False if NDFC is not a development release. - Return None otherwise - - Possible values: - True - False - None - """ - return self._get("dev") - - @property - def install(self): - """ - Return the value of install, if it exists. - Return None otherwise - - Possible values: - EASYFABRIC - (probably other values) - None - """ - return self._get("install") - - @property - def is_ha_enabled(self): - """ - Return True if NDFC is a media controller. - Return False if NDFC is not a media controller. - Return None otherwise - - Possible values: - True - False - None - """ - return self.make_boolean(self._get("isHaEnabled")) - - @property - def is_media_controller(self): - """ - Return True if NDFC is a media controller. - Return False if NDFC is not a media controller. - Return None otherwise - - Possible values: - True - False - None - """ - return self.make_boolean(self._get("isMediaController")) - - @property - def is_upgrade_inprogress(self): - """ - Return True if an NDFC upgrade is in progress. - Return False if an NDFC upgrade is not in progress. - Return None otherwise - - Possible values: - True - False - None - """ - return self.make_boolean(self._get("is_upgrade_inprogress")) - - @property - def response_data(self): - """ - Return the data retrieved from the request - """ - return self.properties.get("response_data") - - @property - def result(self): - """ - Return the GET result from NDFC - """ - return self.properties.get("result") - - @property - def response(self): - """ - Return the GET response from NDFC - """ - return self.properties.get("response") - - @property - def mode(self): - """ - Return the NDFC mode, if it exists. - Return None otherwise - - Possible values: - LAN - None - """ - return self._get("mode") - - @property - def uuid(self): - """ - Return the value of uuid, if it exists. - Return None otherwise - - Possible values: - uuid e.g. "f49e6088-ad4f-4406-bef6-2419de914df1" - None - """ - return self._get("uuid") - - @property - def version(self): - """ - Return the NDFC version, if it exists. - Return None otherwise - - Possible values: - version, e.g. "12.1.2e" - None - """ - return self._get("version") - - @property - def version_major(self): - """ - Return the NDFC major version, if it exists. - Return None otherwise - - We are assuming semantic versioning based on: - https://semver.org - - Possible values: - if version is 12.1.2e, return 12 - None - """ - if self.version is None: - return None - return (self._get("version").split("."))[0] - - @property - def version_minor(self): - """ - Return the NDFC minor version, if it exists. - Return None otherwise - - We are assuming semantic versioning based on: - https://semver.org - - Possible values: - if version is 12.1.2e, return 1 - None - """ - if self.version is None: - return None - return (self._get("version").split("."))[1] - - @property - def version_patch(self): - """ - Return the NDFC minor version, if it exists. - Return None otherwise - - We are assuming semantic versioning based on: - https://semver.org - - Possible values: - if version is 12.1.2e, return 2e - None - """ - if self.version is None: - return None - return (self._get("version").split("."))[2] - - -def main(): - """main entry point for module execution""" - - element_spec = dict( - config=dict(required=True, type="dict"), - state=dict(default="merged", choices=["merged", "deleted", "query"]), - ) - - module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) - dcnm_module = ImageUpgradeTask(module) - dcnm_module.validate_input() - dcnm_module.get_have() - dcnm_module.get_want() - - if module.params["state"] == "merged": - dcnm_module.get_need_merged() - elif module.params["state"] == "deleted": - dcnm_module.get_need_deleted() - elif module.params["state"] == "query": - dcnm_module.get_need_query() - - if module.params["state"] == "query": - dcnm_module.result["changed"] = False - if module.params["state"] in ["merged", "deleted"]: - if dcnm_module.need: - dcnm_module.result["changed"] = True - else: - module.exit_json(**dcnm_module.result) - - if module.check_mode: - dcnm_module.result["changed"] = False - module.exit_json(**dcnm_module.result) - - if dcnm_module.need: - if module.params["state"] == "merged": - dcnm_module.handle_merged_state() - elif module.params["state"] == "deleted": - dcnm_module.handle_deleted_state() - elif module.params["state"] == "query": - dcnm_module.handle_query_state() - - module.exit_json(**dcnm_module.result) - - -if __name__ == "__main__": - main() From 74731ef426216d4aa90866d08d7c749bdb465716 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 10:48:50 -1000 Subject: [PATCH 107/300] Detach policy only if have policy matches want policy --- plugins/modules/dcnm_image_upgrade.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index a0a3638eb..d40a29695 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -646,6 +646,8 @@ def get_need_deleted(self) -> None: For deleted state, populate self.need list() with items from our want list that are not in our have list. These items will be sent to the controller. + + Policies are detached only if the policy name matches. """ method_name = inspect.stack()[0][3] @@ -656,6 +658,8 @@ def get_need_deleted(self) -> None: continue if self.have.policy is None: continue + if self.have.policy != want["policy"]: + continue need.append(want) self.need = need @@ -665,6 +669,8 @@ def get_need_query(self) -> None: For query state, populate self.need list() with all items from our want list. These items will be sent to the controller. + + policy name is ignored for query state. """ method_name = inspect.stack()[0][3] From 1b9a52156d7bb94ffcfcd5cdbb0b9f6496b20f68 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 10:49:53 -1000 Subject: [PATCH 108/300] Run thru pylint --- plugins/module_utils/image_mgmt/image_policy_action.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 3a2bcf474..c70a6aa58 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -80,10 +80,10 @@ def build_payload(self): payload["ipAddr"] = self.switch_issu_details.ip_address payload["platform"] = self.switch_issu_details.platform payload["serialNumber"] = self.switch_issu_details.serial_number - for item in payload: - if payload[item] is None: + for key,value in payload.items(): + if value is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f" Unable to determine {item} for switch " + msg += f" Unable to determine {key} for switch " msg += f"{self.switch_issu_details.ip_address}, " msg += f"{self.switch_issu_details.serial_number}, " msg += f"{self.switch_issu_details.device_name}. " @@ -219,7 +219,7 @@ def _query_policy(self): """ Query the image policy verb: GET - endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ + endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy """ self.method_name = inspect.stack()[0][3] From b80d7659ca435811907ac2a615283a51469c0b34 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 10:50:14 -1000 Subject: [PATCH 109/300] Run thru black/isort --- .../image_mgmt/image_upgrade_common.py | 1 + .../module_utils/image_mgmt/image_validate.py | 2 +- .../module_utils/image_mgmt/switch_details.py | 18 ++++++++---------- .../image_mgmt/switch_issu_details.py | 1 + 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index cd7d9c0ec..b5d95c01a 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -1,5 +1,6 @@ import inspect + class ImageUpgradeCommon: """ Base class for the other image upgrade classes diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 1a3464d93..690b0e6e1 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -59,7 +59,7 @@ def __init__(self, module): def _init_properties(self) -> None: self.method_name = inspect.stack()[0][3] - self.properties:Dict[str, Any] = {} + self.properties: Dict[str, Any] = {} self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds self.properties["response_data"] = {} diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index 47a0bfffb..28c040b2a 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -1,10 +1,12 @@ import inspect -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send + class SwitchDetails(ImageUpgradeCommon): """ @@ -69,7 +71,6 @@ def refresh(self): for switch in data: self.properties["response_data"][switch["ipAddress"]] = switch - def _get(self, item): self.method_name = inspect.stack()[0][3] @@ -90,9 +91,7 @@ def _get(self, item): self.module.fail_json(msg) return self.make_boolean( - self.make_none( - self.properties["response_data"][self.ip_address].get(item) - ) + self.make_none(self.properties["response_data"][self.ip_address].get(item)) ) @property @@ -198,4 +197,3 @@ def serial_number(self): Return None otherwise """ return self._get("serialNumber") - diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 21fedfda9..a6c2a88a2 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -637,6 +637,7 @@ def vpc_role2(self): """ return self._get("vpc_role") + class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): """ Retrieve switch issu details from the controller and provide From 222a788bd7e64d171b3e0b469c9095571f010d26 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 Nov 2023 13:34:32 -1000 Subject: [PATCH 110/300] Run thru black/isort/pylint --- plugins/modules/dcnm_image_upgrade.py | 54 +++++++++++++++------------ 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index d40a29695..5c7ae304e 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -27,7 +27,7 @@ import copy import inspect import json -from typing import Any, Dict, List, Union +from typing import Any, Dict, List from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -451,6 +451,13 @@ def __init__(self, module): self.params = self.module.params self.endpoints = ApiEndpoints() + self.have = None + self.idempotent_want = None + # populated in self._merge_global_and_switch_configs() + self.switch_configs = [] + + self.path = None + self.verb = None # populated in self._build_policy_attach_payload() self.payloads = [] @@ -466,10 +473,10 @@ def __init__(self, module): self.check_mode = False self.validated = {} - self.want_create = [] + self.want = [] self.need = [] - self.result = dict(changed=False, diff=[], response=[]) + self.result = {"changed": False, "diff": [], "response": []} self.mandatory_global_keys = {"switches"} self.mandatory_switch_keys = {"ip_address"} @@ -512,7 +519,7 @@ def get_want(self) -> None: """ Caller: main() - Update self.want_create for all switches defined in the playbook + Update self.want for all switches defined in the playbook """ method_name = inspect.stack()[0][3] @@ -521,7 +528,7 @@ def get_want(self) -> None: if not self.switch_configs: return - self.want_create = self.switch_configs + self.want = self.switch_configs def _build_idempotent_want(self, want) -> None: """ @@ -559,6 +566,11 @@ def _build_idempotent_want(self, want) -> None: and the information returned by ImageInstallOptions. Caller: self.get_need_merged() + + NOTES: + 1. There doesn't appear to be an API to query the controller for + the current EPLD version. Hence, currently, EPLD handling + is not idempotent. """ method_name = inspect.stack()[0][3] @@ -625,7 +637,7 @@ def get_need_merged(self) -> None: method_name = inspect.stack()[0][3] need: List[Dict] = [] - for want_create in self.want_create: + for want_create in self.want: self.have.ip_address = want_create["ip_address"] if self.have.serial_number is not None: self._build_idempotent_want(want_create) @@ -652,7 +664,7 @@ def get_need_deleted(self) -> None: method_name = inspect.stack()[0][3] need = [] - for want in self.want_create: + for want in self.want: self.have.ip_address = want["ip_address"] if self.have.serial_number is None: continue @@ -675,7 +687,7 @@ def get_need_query(self) -> None: method_name = inspect.stack()[0][3] need = [] - for want in self.want_create: + for want in self.want: need.append(want) self.need = need @@ -1013,10 +1025,10 @@ def _build_policy_attach_payload(self) -> None: payload["serialNumber"] = self.switch_details.serial_number # payload["bootstrapMode"] = switch.get('bootstrap_mode') - for item in payload: - if payload[item] is None: + for key, value in payload.items(): + if value is None: msg = f"{self.class_name}.{method_name}: " - msg += f"Unable to determine {item} for switch " + msg += f"Unable to determine {key} for switch " msg += f"{switch.get('ip_address')}. " msg += "Please verify that the switch is managed by " msg += "the controller." @@ -1074,10 +1086,6 @@ def _validate_images(self, serial_numbers) -> None: instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers - # TODO:2 Discuss with Mike/Shangxin - ImageValidate.non_disruptive - # Should we add this option to the playbook? - # It's supported in ImageValidate with default of False - # instance.non_disruptive = False instance.commit() def _verify_install_options(self, devices) -> None: @@ -1232,14 +1240,14 @@ def handle_deleted_state(self) -> None: ) if len(detach_policy_devices) == 0: - self.result = dict(changed=False, diff=[], response=[]) + self.result = {"changed": False, "diff": [], "response": []} return instance = ImagePolicyAction(self.module) - for policy_name in detach_policy_devices: - instance.policy_name = policy_name + for key, value in detach_policy_devices.items(): + instance.policy_name = key instance.action = "detach" - instance.serial_numbers = detach_policy_devices[policy_name] + instance.serial_numbers = value instance.commit() def handle_query_state(self) -> None: @@ -1299,10 +1307,10 @@ def _failure(self, resp) -> None: def main(): """main entry point for module execution""" - element_spec = dict( - config=dict(required=True, type="dict"), - state=dict(default="merged", choices=["merged", "deleted", "query"]), - ) + element_spec = { + "config": {"required": True, "type": "dict"}, + "state": {"default": "merged", "choices": ["merged", "deleted", "query"]}, + } ansible_module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) task_module = ImageUpgradeTask(ansible_module) From 76bb1866b2c83f7532ce91e2c1291f1ffd76de19 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 07:24:12 -1000 Subject: [PATCH 111/300] Run thru black, isort, pylint, make method_name local to functions --- .../image_mgmt/image_policy_action.py | 69 ++++++++++++------- .../test_image_upgrade_ImagePolicyAction.py | 11 ++- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index c70a6aa58..e66184ead 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -1,3 +1,6 @@ +""" +Perform image policy actions on the controller for one or more switches. +""" import inspect import json @@ -44,15 +47,18 @@ class ImagePolicyAction(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() self._init_properties() self.image_policies = ImagePolicies(self.module) + self.path = None + self.payloads = [] self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.module) self.valid_actions = {"attach", "detach", "query"} + self.verb = None def _init_properties(self): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.properties = {} self.properties["action"] = None self.properties["response"] = None @@ -68,7 +74,7 @@ def build_payload(self): caller _attach_policy() """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.payloads = [] self.switch_issu_details.refresh() @@ -80,9 +86,9 @@ def build_payload(self): payload["ipAddr"] = self.switch_issu_details.ip_address payload["platform"] = self.switch_issu_details.platform payload["serialNumber"] = self.switch_issu_details.serial_number - for key,value in payload.items(): + for key, value in payload.items(): if value is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f" Unable to determine {key} for switch " msg += f"{self.switch_issu_details.ip_address}, " msg += f"{self.switch_issu_details.serial_number}, " @@ -96,16 +102,16 @@ def validate_request(self): """ validations prior to commit() should be added here. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if self.action is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.action must be set before " msg += "calling commit()" self.module.fail_json(msg) if self.policy_name is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.policy_name must be set before " msg += "calling commit()" self.module.fail_json(msg) @@ -114,7 +120,7 @@ def validate_request(self): return if self.serial_numbers is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_numbers must be set before " msg += "calling commit()" self.module.fail_json(msg) @@ -127,7 +133,7 @@ def validate_request(self): for serial_number in self.serial_numbers: self.switch_issu_details.serial_number = serial_number if self.switch_issu_details.platform not in self.image_policies.platform: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"policy {self.policy_name} does not support platform " msg += f"{self.switch_issu_details.platform}. {self.policy_name} " msg += "supports the following platform(s): " @@ -135,7 +141,13 @@ def validate_request(self): self.module.fail_json(msg) def commit(self): - self.method_name = inspect.stack()[0][3] + """ + Call one of the following methods to commit the action to the controller: + - _attach_policy + - _detach_policy + - _query_policy + """ + method_name = inspect.stack()[0][3] self.validate_request() if self.action == "attach": @@ -145,7 +157,7 @@ def commit(self): elif self.action == "query": self._query_policy() else: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Unknown action {self.action}." self.module.fail_json(msg) @@ -158,7 +170,7 @@ def _attach_policy(self): are accessible via properties response and result, respectively. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.build_payload() @@ -174,12 +186,12 @@ def _attach_policy(self): ) result = self._handle_response(response, self.verb) - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"response: {json.dumps(response, indent=4)}" self.log_msg(msg) if not result["success"]: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " msg += f"to switch {payload['ipAddr']}." self.module.fail_json(msg) @@ -197,7 +209,7 @@ def _detach_policy(self): endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy query_params: ?serialNumber=FDO211218GC,FDO21120U5D """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.path = self.endpoints.policy_detach.get("path") self.verb = self.endpoints.policy_detach.get("verb") @@ -208,12 +220,15 @@ def _detach_policy(self): self.properties["response"] = dcnm_send(self.module, self.verb, self.path) self.properties["result"] = self._handle_response(self.response, self.verb) - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"response: {json.dumps(self.response, indent=4)}" self.log_msg(msg) if not self.result["success"]: - self._failure(self.response) + msg = f"{self.class_name}.{method_name}: " + msg += f"Bad result when detaching policy {self.policy_name} " + msg += f"from the following device(s): {','.join(sorted(self.serial_numbers))}." + self.module.fail_json(msg) def _query_policy(self): """ @@ -221,7 +236,7 @@ def _query_policy(self): verb: GET endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.path = self.endpoints.policy_info.get("path") self.verb = self.endpoints.policy_info.get("verb") @@ -232,7 +247,9 @@ def _query_policy(self): self.properties["result"] = self._handle_response(self.response, self.verb) if not self.result["success"]: - self._failure(self.response) + msg = f"{self.class_name}.{method_name}: " + msg += f"Bad result when querying image policy {self.policy_name}." + self.module.fail_json(msg) self.properties["query_result"] = self.response.get("DATA") @@ -256,10 +273,10 @@ def action(self): @action.setter def action(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if value not in self.valid_actions: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.action must be one of " msg += f"{','.join(sorted(self.valid_actions))}. " msg += f"Got {value}." @@ -300,7 +317,7 @@ def policy_name(self): @policy_name.setter def policy_name(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] self.properties["policy_name"] = value @property @@ -315,11 +332,11 @@ def serial_numbers(self): @serial_numbers.setter def serial_numbers(self, value): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if not isinstance(value, list): - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_numbers must be a " - msg += f"python list of switch serial numbers. " + msg += "python list of switch serial numbers. " msg += f"Got {value}." self.module.fail_json(msg) self.properties["serial_numbers"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index b463035a1..b57c648fe 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -20,6 +20,8 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ ImagePolicies from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ @@ -109,13 +111,20 @@ def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: Test - Class attributes initialized to expected values + - fail_json is not called """ - image_policy_action.__init__(MockAnsibleModule) + with does_not_raise(): + image_policy_action.__init__(MockAnsibleModule) + assert image_policy_action.class_name == "ImagePolicyAction" + assert isinstance(image_policy_action.endpoints, ApiEndpoints) assert isinstance(image_policy_action, ImagePolicyAction) assert isinstance( image_policy_action.switch_issu_details, SwitchIssuDetailsBySerialNumber ) + assert image_policy_action.path == None + assert image_policy_action.payloads == [] assert image_policy_action.valid_actions == {"attach", "detach", "query"} + assert image_policy_action.verb == None def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: From 7166e3b079170a32c7ad27f89630b7cda3eb281b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 07:24:38 -1000 Subject: [PATCH 112/300] Update docstrings --- .../test_image_upgrade_ImageInstallOptions.py | 146 +++++++++++------- 1 file changed, 92 insertions(+), 54 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index ffe70bbea..4b4dd458f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -118,14 +118,15 @@ def test_image_mgmt_install_options_00004(module) -> None: module.refresh() -# test_image_mgmt_install_options_00005 -# test_refresh_return_code_200 (former name) - - def test_image_mgmt_install_options_00005(monkeypatch, module) -> None: """ - Properties are updated based on 200 response from endpoint. - endpoint: install-options + Function + - refresh + + Test + - 200 response from endpoint + - Properties are updated with expected values + - endpoint: install-options """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -150,7 +151,6 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert isinstance(module.raw_data, dict) assert isinstance(module.raw_response, dict) assert "compatibilityStatusList" in module.raw_data - print("module.raw_data: ", module.raw_data) assert module.rep_status == "skipped" assert module.serial_number == "BAR" assert module.status == "Success" @@ -161,13 +161,13 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert module.result.get("success") == True -# test_image_mgmt_install_options_00006 -# test_refresh_return_code_500 (former name) - - def test_image_mgmt_install_options_00006(monkeypatch, module) -> None: """ - fail_json() should be called if the response RETURN_CODE != 200 + Function + - refresh + + Test + - fail_json is called if the response RETURN_CODE != 200 """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -187,15 +187,22 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_install_options_00007(monkeypatch, module) -> None: """ - Properties are updated based on: - - 200 response from endpoint - - Device has no policy attached + Function + - refresh + + Setup + - Device has no policy attached - POST REQUEST - issu == True - epld == False - package_install == False - endpoint: install-options + Test + - 200 response from endpoint + - Response contains expected values + + Endpoint + - install-options """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -220,7 +227,6 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert isinstance(module.raw_data, dict) assert isinstance(module.raw_response, dict) assert "compatibilityStatusList" in module.raw_data - print("module.raw_data: ", module.raw_data) assert module.rep_status == "skipped" assert module.serial_number == "FDO21120U5D" assert module.status == "Skipped" @@ -233,15 +239,22 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_install_options_00008(monkeypatch, module) -> None: """ - Properties are updated based on: - - 200 response from endpoint - - Device has no policy attached + Function + - refresh + + Setup + - Device has no policy attached - POST REQUEST - issu == True - epld == True - package_install == False - endpoint: install-options + Test + - 200 response from endpoint + - Response contains expected values + + Endpoint + - install-options """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -282,15 +295,22 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_install_options_00009(monkeypatch, module) -> None: """ - Properties are updated based on: - - 200 response from endpoint - - Device has no policy attached + Function + - refresh + + Setup + - Device has no policy attached - POST REQUEST - issu == False - epld == True - package_install == False - endpoint: install-options + Test + - 200 response from endpoint + - Response contains expected values + + Endpoint + - install-options """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -331,16 +351,24 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_install_options_00010(monkeypatch, module) -> None: """ - Properties are updated based on: - - 500 response from endpoint due to KR5M policy has no packages defined - and package_install set to True - - KR5M policy is attached to the device - - POST REQUEST contains + Function + - refresh + + Setup + - Device has no policy attached + - POST REQUEST - issu == False - epld == True - - package_install == True (this causes the expected error) + - package_install == True (causes expected error) - endpoint: install-options + Test + - 500 response from endpoint due to + - KR5M policy has no packages defined and + - package_install set to True + - Response contains expected values + + Endpoint + - install-options """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -359,13 +387,16 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: module.refresh() -# test_image_mgmt_install_options_00020 - - def test_image_mgmt_install_options_00020(module) -> None: """ - Payload contains defaults if not specified by the user. - Defaults for issu, epld, and package_install are applied. + Function + - build_payload + + Setup + - Defaults are not specified by the user + + Test + - Default values for issu, epld, and package_install are applied """ module.policy_name = "KRM5" module.serial_number = "BAR" @@ -377,13 +408,17 @@ def test_image_mgmt_install_options_00020(module) -> None: assert module.payload.get("packageInstall") == False -# test_image_mgmt_install_options_00021 - - def test_image_mgmt_install_options_00021(module) -> None: """ - Payload contains user-specified values if the user sets them. - Defaults for issu, epld, and package_install are overridden by user values. + Function + - build_payload + + Setup + - Values are specified by the user + + Test + - Payload contains user-specified values if the user sets them + - Defaults for issu, epld, and package_install are overridden by user values. """ module.policy_name = "KRM5" module.serial_number = "BAR" @@ -398,12 +433,13 @@ def test_image_mgmt_install_options_00021(module) -> None: assert module.payload.get("packageInstall") == True -# test_image_mgmt_install_options_00022 - - def test_image_mgmt_install_options_00022(module) -> None: """ - fail_json() is called if issu is not a boolean. + Function + - issu setter + + Test + - fail_json is called if issu is not a boolean. """ match = "ImageInstallOptions.issu.setter: issu must be a " match += "boolean value" @@ -411,12 +447,13 @@ def test_image_mgmt_install_options_00022(module) -> None: module.issu = "FOO" -# test_image_mgmt_install_options_00023 - - def test_image_mgmt_install_options_00023(module) -> None: """ - fail_json() is called if epld is not a boolean. + Function + - epld setter + + Test + - fail_json is called if epld is not a boolean. """ match = "ImageInstallOptions.epld.setter: epld must be a " match += "boolean value" @@ -424,12 +461,13 @@ def test_image_mgmt_install_options_00023(module) -> None: module.epld = "FOO" -# test_image_mgmt_install_options_00024 - - def test_image_mgmt_install_options_00024(module) -> None: """ - fail_json() is called if package_install is not a boolean. + Function + - package_install setter + + Test + - fail_json is called if package_install is not a boolean. """ match = "ImageInstallOptions.package_install.setter: " match += "package_install must be a boolean value" From 82d04112b080d92a9933d611227b3ca3fc6d6386 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 07:33:44 -1000 Subject: [PATCH 113/300] Removed unused fixtures file --- .../fixtures/image_upgrade_responses.json | 2098 ----------------- 1 file changed, 2098 deletions(-) delete mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses.json deleted file mode 100644 index 8ea217326..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses.json +++ /dev/null @@ -1,2098 +0,0 @@ -{ - "policymgmt_policies_200": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "policyName": "KR5M", - "policyType": "PLATFORM", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "policyDescr": "10.2.(5) with EPLD", - "platformPolicies": "", - "epldImgName": "n9000-epld.10.2.5.M.img", - "rpmimages": "", - "imageName": "nxos64-cs.10.2.5.M.bin", - "agnostic": "false", - "ref_count": 10 - }, - { - "policyName": "OR1F", - "policyType": "PLATFORM", - "nxosVersion": "10.4.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "policyDescr": "OR1F EPLD", - "platformPolicies": "", - "epldImgName": "n9000-epld.10.4.1.F.img", - "rpmimages": "", - "imageName": "nxos64-cs.10.4.1.F.bin", - "agnostic": "false", - "ref_count": 0 - } - ], - "message": "" - } - }, - "image_stage_200": { - "RETURN_CODE": 200, - "METHOD": "POST", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image", - "MESSAGE": "OK", - "DATA": [ - { - "key": "FDO211218FV", - "value": "No files to stage" - } - ] - }, - "validate_image_200": { - "RETURN_CODE": 200, - "METHOD": "POST", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", - "MESSAGE": "OK", - "DATA": "Invalid JSON response: [StageResponse [key=success, value=]]" - }, - "imageupgrade_install_options_200": { - "RETURN_CODE": 200, - "METHOD": "POST", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", - "MESSAGE": "OK", - "DATA": { - "compatibilityStatusList": [ - { - "deviceName": "cvd-1314-leaf", - "ipAddress": "172.22.150.105", - "policyName": "KR5M", - "platform": "N9K/N3K", - "version": "10.2.5", - "osType": "64bit", - "status": "Success", - "installOption": "disruptive", - "compDisp": "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin", - "preIssuLink": "Not Applicable", - "repStatus": "skipped", - "timestamp": "NA" - } - ], - "epldModules": "null", - "installPacakges": "null", - "errMessage": "" - } - }, - "about_version_200": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "issu_detail_200": { - "status": "SUCCESS", - "RETURN_CODE": 200, - "DATA": { - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "None", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": null, - "vpcPeer": null, - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": null, - "ip_address": "172.22.150.102", - "peer": null, - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": false, - "mds": false - } - ] - }, - "message": "" - }, - "inventory_all_switches_200": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", - "MESSAGE": "OK", - "DATA": [ - { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.110", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1111-bgw", - "switchDbID": 158560, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:28:36", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FOX2109PGCT", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", - "hostName": "cvd-1111-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1400, - "swUUID": "DCNM-UUID-1400", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.111", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1112-bgw", - "switchDbID": 160170, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:28:20", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FOX2109PGD1", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", - "hostName": "cvd-1112-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1200, - "swUUID": "DCNM-UUID-1200", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Healthy", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Spine", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.112", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1211-spine", - "switchDbID": 154230, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:29:22", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FOX2109PGCS", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "spine", - "mode": "Normal", - "hostName": "cvd-1211-spine", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1100, - "swUUID": "DCNM-UUID-1100", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Major", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Spine", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.113", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1212-spine", - "switchDbID": 155930, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:28:58", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FOX2109PGD0", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "spine", - "mode": "Normal", - "hostName": "cvd-1212-spine", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1300, - "swUUID": "DCNM-UUID-1300", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Healthy", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Leaf", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C93180YC-EX", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.103", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1312-leaf", - "switchDbID": 150610, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:30:38", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO211218GC", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "leaf", - "mode": "Normal", - "hostName": "cvd-1312-leaf", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1350, - "swUUID": "DCNM-UUID-1350", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Leaf", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C93180YC-EX", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.104", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1313-leaf", - "switchDbID": 151490, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:30:23", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO211218HH", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "leaf", - "mode": "Normal", - "hostName": "cvd-1313-leaf", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1050, - "swUUID": "DCNM-UUID-1050", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Leaf", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C93180YC-EX", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.105", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-1314-leaf", - "switchDbID": 153350, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:30:09", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO211218FV", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "leaf", - "mode": "Normal", - "hostName": "cvd-1314-leaf", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1150, - "swUUID": "DCNM-UUID-1150", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VXLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 3, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9336C-FX2", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.100", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "hard", - "modelType": 0, - "logicalName": "cvd-2111-bgw", - "switchDbID": 39990, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "13 days, 21:32:36", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO22180ASJ", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", - "hostName": "cvd-2111-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 7780, - "swUUID": "DCNM-UUID-7780", - "swType": "null", - "ccStatus": "In-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "BorderGateway", - "vrf": "management", - "fabricTechnology": "VXLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 3, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9336C-FX2", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.101", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "hard", - "modelType": 0, - "logicalName": "cvd-2112-bgw", - "switchDbID": 39650, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "13 days, 21:32:54", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO23021QYJ", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "border gateway", - "mode": "Normal", - "hostName": "cvd-2112-bgw", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 7420, - "swUUID": "DCNM-UUID-7420", - "swType": "null", - "ccStatus": "In-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Spine", - "vrf": "management", - "fabricTechnology": "VXLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 3, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.114", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "hard", - "modelType": 0, - "logicalName": "cvd-2211-spine", - "switchDbID": 39690, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "13 days, 21:32:38", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FOX2109PHDD", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "spine", - "mode": "Normal", - "hostName": "cvd-2211-spine", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 39720, - "swUUID": "DCNM-UUID-39720", - "swType": "null", - "ccStatus": "In-Sync", - "operStatus": "Healthy", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Spine", - "vrf": "management", - "fabricTechnology": "VXLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 3, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9504", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.115", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "hard", - "modelType": 0, - "logicalName": "cvd-2212-spine", - "switchDbID": 39940, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "13 days, 21:32:35", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FOX2109PHDQ", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "spine", - "mode": "Normal", - "hostName": "cvd-2212-spine", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 39970, - "swUUID": "DCNM-UUID-39970", - "swType": "null", - "ccStatus": "In-Sync", - "operStatus": "Healthy", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Leaf", - "vrf": "management", - "fabricTechnology": "VXLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 3, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C93180YC-EX", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.106", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "hard", - "modelType": 0, - "logicalName": "cvd-2311-leaf", - "switchDbID": 39790, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "13 days, 21:33:04", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO211218HB", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "leaf", - "mode": "Normal", - "hostName": "cvd-2311-leaf", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 39820, - "swUUID": "DCNM-UUID-39820", - "swType": "null", - "ccStatus": "In-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Leaf", - "vrf": "management", - "fabricTechnology": "VXLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 3, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C93180YC-EX", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.107", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "hard", - "modelType": 0, - "logicalName": "cvd-2312-leaf", - "switchDbID": 39740, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "13 days, 21:33:03", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO211218AX", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "leaf", - "mode": "Normal", - "hostName": "cvd-2312-leaf", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 39770, - "swUUID": "DCNM-UUID-39770", - "swType": "null", - "ccStatus": "In-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Leaf", - "vrf": "management", - "fabricTechnology": "VXLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 3, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C93180YC-EX", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.108", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "hard", - "modelType": 0, - "logicalName": "cvd-2313-leaf", - "switchDbID": 39890, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "13 days, 21:33:08", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO2112189M", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "leaf", - "mode": "Normal", - "hostName": "cvd-2313-leaf", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 39920, - "swUUID": "DCNM-UUID-39920", - "swType": "null", - "ccStatus": "In-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Leaf", - "vrf": "management", - "fabricTechnology": "VXLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 3, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C93180YC-EX", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.109", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "hard", - "modelType": 0, - "logicalName": "cvd-2314-leaf", - "switchDbID": 39840, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "13 days, 21:33:02", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO211218B5", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "leaf", - "mode": "Normal", - "hostName": "cvd-2314-leaf", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 39870, - "swUUID": "DCNM-UUID-39870", - "swType": "null", - "ccStatus": "In-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Aggregation", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C9336C-FX2", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.99", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "cvd-rs-111", - "switchDbID": 161530, - "uid": 0, - "release": "10.3(2)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:18:51", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO2443096H", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "aggregation", - "mode": "Normal", - "hostName": "cvd-rs-111", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 7740, - "swUUID": "DCNM-UUID-7740", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - }, - { - "switchRoleEnum": "Leaf", - "vrf": "management", - "fabricTechnology": "VLANFabric", - "deviceType": "Switch_Fabric", - "fabricId": 4, - "name": "null", - "domainID": 0, - "wwn": "null", - "membership": "null", - "ports": 0, - "model": "N9K-C93180YC-EX", - "version": "null", - "upTime": 0, - "ipAddress": "172.22.150.102", - "mgmtAddress": "null", - "vendor": "Cisco", - "displayHdrs": "null", - "displayValues": "null", - "colDBId": 0, - "fid": 0, - "isLan": "false", - "is_smlic_enabled": "false", - "present": "true", - "licenseViolation": "false", - "managable": "true", - "mds": "false", - "connUnitStatus": 0, - "standbySupState": 0, - "activeSupSlot": 0, - "unmanagableCause": "", - "lastScanTime": 0, - "fabricName": "easy", - "modelType": 0, - "logicalName": "leaf1", - "switchDbID": 145740, - "uid": 0, - "release": "10.2(5)", - "location": "null", - "contact": "null", - "upTimeStr": "7 days, 03:30:48", - "upTimeNumber": 0, - "network": "null", - "nonMdsModel": "null", - "numberOfPorts": 0, - "availPorts": 0, - "usedPorts": 0, - "vsanWwn": "null", - "vsanWwnName": "null", - "swWwn": "null", - "swWwnName": "null", - "serialNumber": "FDO21120U5D", - "domain": "null", - "principal": "null", - "status": "ok", - "index": 0, - "licenseDetail": "null", - "isPmCollect": "false", - "sanAnalyticsCapable": "false", - "vdcId": 0, - "vdcName": "", - "vdcMac": "null", - "fcoeEnabled": "false", - "cpuUsage": 0, - "memoryUsage": 0, - "scope": "null", - "fex": "false", - "health": -1, - "npvEnabled": "false", - "linkName": "null", - "username": "null", - "primaryIP": "", - "primarySwitchDbID": 0, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "isEchSupport": "false", - "moduleIndexOffset": 9999, - "sysDescr": "", - "isTrapDelayed": "false", - "switchRole": "leaf", - "mode": "Normal", - "hostName": "leaf1", - "ipDomain": "", - "systemMode": "Normal", - "sourceVrf": "management", - "sourceInterface": "mgmt0", - "protoDiscSettings": "null", - "operMode": "null", - "modules": "null", - "fexMap": {}, - "isVpcConfigured": "false", - "vpcDomain": 0, - "role": "null", - "peer": "null", - "peerSerialNumber": "null", - "peerSwitchDbId": 0, - "peerlinkState": "null", - "keepAliveState": "null", - "consistencyState": "false", - "sendIntf": "null", - "recvIntf": "null", - "interfaces": "null", - "elementType": "null", - "monitorMode": "null", - "freezeMode": "null", - "cfsSyslogStatus": 1, - "isNonNexus": "false", - "swUUIDId": 1250, - "swUUID": "DCNM-UUID-1250", - "swType": "null", - "ccStatus": "Out-of-Sync", - "operStatus": "Minor", - "intentedpeerName": "" - } - ] - } -} \ No newline at end of file From 4e857073c5f2397feb340dc8ff4ba733a33dcaa6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 07:35:40 -1000 Subject: [PATCH 114/300] Remove unused payloads file --- .../fixtures/image_upgrade_payloads.json | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads.json deleted file mode 100644 index 7ef829dae..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "image_install_options": { - "devices": [ - { - "serialNumber": "FDO21120U5D", - "policyName": "KR5M" - } - ], - "issu": "true", - "epld": "false", - "packageInstall": "false" - }, - "image_install_options_epld_true": { - "devices": [ - { - "serialNumber": "FDO21120U5D", - "policyName": "KR5M" - } - ], - "issu": "true", - "epld": "true", - "packageInstall": "false" - } -} \ No newline at end of file From 326a8b27c97ae2af7b9f0e047e388901341c0a79 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 07:40:28 -1000 Subject: [PATCH 115/300] Remove unused configs file --- .../fixtures/image_upgrade_playbook_configs.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json deleted file mode 100644 index 3447438c9..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "query_config":{ - "policy": "KMR5", - "switches": [ - { - "ip_address": "172.22.150.102", - "policy": "OR1F" - } - ] - } -} \ No newline at end of file From 5fd86edb1d51c2d15dc8e815e116e85d1e392768 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 07:40:53 -1000 Subject: [PATCH 116/300] Add a TODO for a future test --- .../fixtures/image_upgrade_responses_ImageUpgrade.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json index 24118a02c..711894769 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json @@ -127,6 +127,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 500, "TEST_NOTES": [ + "TODO: Add this test", "Returned under the following conditions", "upgrade.epld == True", "upgrade.nxos == True", From f4a2a28285daddbe6166afdccacf448299521bdb Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 07:45:45 -1000 Subject: [PATCH 117/300] Update docstrings --- .../test_image_upgrade_ImagePolicyAction.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index b57c648fe..206908f6e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -509,7 +509,13 @@ def test_image_mgmt_image_policy_action_00060( image_policy_action, value, expected ) -> None: """ - ImagePolicyAction.action setter + Function + - action setter + + Test + - Expected values are set + - fail_json is called when value is not a valid action + - fail_json error message is matched """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00060): @@ -534,7 +540,13 @@ def test_image_mgmt_image_policy_action_00061( image_policy_action, value, expected ) -> None: """ - ImagePolicyAction.serial_numbers setter + Function + - serial_numbers setter + + Test + - fail_json is not called with value is a list + - fail_json is called when value is not a list + - fail_json error message is matched """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00061): From 50b84d300045c285a22e2d6812a258494ff06351 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 08:00:09 -1000 Subject: [PATCH 118/300] Update docstrings, rename error_message to match --- .../test_image_upgrade_ImageValidate.py | 195 +++++++++--------- 1 file changed, 102 insertions(+), 93 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index d6c12986c..36443b79d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -66,12 +66,13 @@ def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) -# test_image_mgmt_validate_00001 - - def test_image_mgmt_validate_00001(module) -> None: """ - __init__ + Function + - __init__ + + Test + - Class attributes are initialized to expected values """ module.__init__(MockAnsibleModule) assert module.class_name == "ImageValidate" @@ -79,13 +80,13 @@ def test_image_mgmt_validate_00001(module) -> None: assert isinstance(module.issu_detail, SwitchIssuDetailsBySerialNumber) -# test_image_mgmt_validate_00002 -# test_init_properties (former name) - - def test_image_mgmt_validate_00002(module) -> None: """ - Properties are initialized to expected values + Function + - _init_properties + + Test + - Class properties are initialized to expected values """ module._init_properties() assert isinstance(module.properties, dict) @@ -98,21 +99,21 @@ def test_image_mgmt_validate_00002(module) -> None: assert module.properties.get("serial_numbers") == [] -# test_image_mgmt_validate_00003 -# test_prune_serial_numbers (former name) - - def test_image_mgmt_validate_00003(monkeypatch, module, mock_issu_details) -> None: """ + Function + - prune_serial_numbers + + Test + - module.serial_numbers contains only serial numbers for which + "validated" == "none" + - serial_numbers does not contain serial numbers for which + "validated" == "Success" + + Description prune_serial_numbers removes serial numbers from the list for which "validated" == "Success" (TODO: AND policy == ) - Expectations: - 1. module.serial_numbers should contain only serial numbers for which - "validated" == "none" - 2. module.serial_numbers should not contain serial numbers for which - "validated" == "Success" - Expected results: 1. module.serial_numbers == ["FDO2112189M", "FDO211218AX", "FDO211218B5"] 2. module.serial_numbers != ["FDO211218FV", "FDO211218GC"] @@ -148,7 +149,12 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_validate_00004(monkeypatch, module, mock_issu_details) -> None: """ - fail_json is called when imageStaged == "Failed". + Function + - validate_serial_numbers + + Test + - fail_json is called when imageStaged == "Failed". + - fail_json error message is matched Expectations: @@ -165,32 +171,31 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.issu_detail = mock_issu_details module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] - error_message = "ImageValidate.validate_serial_numbers: " - error_message += "image validation is failing for the following switch: " - error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. If this " - error_message += "persists, check the switch connectivity to the " - error_message += "controller and try again." - with pytest.raises(AnsibleFailJson, match=error_message): + match = "ImageValidate.validate_serial_numbers: " + match += "image validation is failing for the following switch: " + match += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. If this " + match += "persists, check the switch connectivity to the " + match += "controller and try again." + with pytest.raises(AnsibleFailJson, match=match): module.validate_serial_numbers() -# test_image_mgmt_validate_00005 -# test_wait_for_image_validate_to_complete (former name) - - def test_image_mgmt_validate_00005(monkeypatch, module, mock_issu_details) -> None: """ + Function + - _wait_for_image_validate_to_complete + + Test + - serial_numbers_done is a set() + - serial_numbers_done has length 2 + - serial_numbers_done contains all serial numbers in instance.serial_numbers + - fail_json is not called + + Description _wait_for_image_validate_to_complete looks at the "validated" status for each serial number and waits for it to be "Success" or "Failed". In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. - - Expectations: - 1. module.serial_numbers_done should be a set() - 2. module.serial_numbers_done should be length 2 - 3. module.serial_numbers_done should contain all serial numbers in - module.serial_numbers - 4. The module should return without calling fail_json. """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -218,16 +223,22 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_validate_00006(monkeypatch, module, mock_issu_details) -> None: """ + Function + - _wait_for_image_validate_to_complete + + Test + - serial_numbers_done is a set() + - serial_numbers_done has length 1 + - serial_numbers_done contains FDO21120U5D since "validated" == "Success" + - serial_numbers_done does not contain FDO2112189M since "validated" == "Failed" + - fail_json is called + - fail_json error message is matched + + Description _wait_for_image_validate_to_complete looks at the "validated" status for each serial number and waits for it to be "Success" or "Failed". In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. - - Expectations: - 1. module.serial_numbers_done is a set() - 2. module.serial_numbers_done has length 1 - 3. module.serial_numbers_done contains FDO21120U5D, "validated" is "Success" - 4. Call fail_json on serial number FDO2112189M, "validated" is "Failed" """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -242,14 +253,14 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: "FDO2112189M", ] module.check_interval = 0 - error_message = "Seconds remaining 1800: validate image Failed for " - error_message += "cvd-2313-leaf, 172.22.150.108, FDO2112189M, " - error_message += "image validated percent: 100. Check the switch e.g. " - error_message += "show install log detail, show incompatibility-all nxos " - error_message += ". Or check Operations > Image Management > " - error_message += "Devices > View Details > Validate on the controller " - error_message += "GUI for more details." - with pytest.raises(AnsibleFailJson, match=error_message): + match = "Seconds remaining 1800: validate image Failed for " + match += "cvd-2313-leaf, 172.22.150.108, FDO2112189M, " + match += "image validated percent: 100. Check the switch e.g. " + match += "show install log detail, show incompatibility-all nxos " + match += ". Or check Operations > Image Management > " + match += "Devices > View Details > Validate on the controller " + match += "GUI for more details." + with pytest.raises(AnsibleFailJson, match=match): module._wait_for_image_validate_to_complete() assert isinstance(module.serial_numbers_done, set) assert len(module.serial_numbers_done) == 1 @@ -257,22 +268,21 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" not in module.serial_numbers_done -# test_image_mgmt_validate_00007 -# test_wait_for_image_validate_to_complete_timeout (former name) - - def test_image_mgmt_validate_00007(monkeypatch, module, mock_issu_details) -> None: """ + Function + - _wait_for_image_validate_to_complete + + Test + - serial_numbers_done is a set() + - serial_numbers_done has length 1 + - serial_numbers_done contains FDO21120U5D since "validated" == "Success" + - serial_numbers_done does not contain FDO2112189M since "validated" == "In-Progress" + - fail_json is called due to timeout + - fail_json error message is matched + + Description See test_wait_for_image_stage_to_complete for functional details. - - Since FDO2112189M validated == "In-Progress" the function should timeout - - Expectations: - 1. module.serial_numbers_done should be a set() - 2. module.serial_numbers_done should be length 1 - 3. module.serial_numbers_done should contain FDO21120U5D - 3. module.serial_numbers_done should not contain FDO2112189M - 4. The function should call fail_json due to timeout """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -301,12 +311,19 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" not in module.serial_numbers_done -# test_image_mgmt_validate_00008 -# test_wait_for_current_actions_to_complete (former name) - - def test_image_mgmt_validate_00008(monkeypatch, module, mock_issu_details) -> None: """ + Function + - _wait_for_current_actions_to_complete + + Test + - serial_numbers_done is a set() + - serial_numbers_done has length 2 + - serial_numbers_done contains all serial numbers in + serial_numbers + - fail_json is not called + + Description _wait_for_current_actions_to_complete waits until staging, validation, and upgrade actions are complete for all serial numbers. It calls SwitchIssuDetailsBySerialNumber.actions_in_progress() and expects @@ -314,13 +331,6 @@ def test_image_mgmt_validate_00008(monkeypatch, module, mock_issu_details) -> No the following keys has a value of "In-Progress": ["imageStaged", "upgrade", "validated"] - - Expectations: - 1. module.serial_numbers_done should be a set() - 2. module.serial_numbers_done should be length 2 - 3. module.serial_numbers_done should contain all serial numbers in - module.serial_numbers - 4. The function should return without calling fail_json. """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -342,22 +352,21 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "FDO2112189M" in module.serial_numbers_done -# test_image_mgmt_validate_00009 -# test_wait_for_current_actions_to_complete_timeout (former name) - - def test_image_mgmt_validate_00009(monkeypatch, module, mock_issu_details) -> None: """ + Function + - _wait_for_current_actions_to_complete + + Test + - serial_numbers_done is a set() + - serial_numbers_done has length 1 + - serial_numbers_done contains FDO21120U5D since "validated" == "Success" + - serial_numbers_done does not contain FDO2112189M since "validated" == "In-Progress" + - fail_json is called due to timeout + - fail_json error message is matched + + Description See test_wait_for_current_actions_to_complete for functional details. - - Since FDO2112189M validated == "In-Progress" the function should timeout - - Expectations: - 1. module.serial_numbers_done should be a set() - 2. module.serial_numbers_done should be length 1 - 3. module.serial_numbers_done should contain FDO21120U5D - 3. module.serial_numbers_done should not contain FDO2112189M - 4. The function should call fail_json due to timeout """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -374,11 +383,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: module.check_interval = 1 module.check_timeout = 1 - error_message = "ImageValidate._wait_for_current_actions_to_complete: " - error_message += "Timed out waiting for actions to complete. " - error_message += "serial_numbers_done: FDO21120U5D, " - error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" - with pytest.raises(AnsibleFailJson, match=error_message): + match = "ImageValidate._wait_for_current_actions_to_complete: " + match += "Timed out waiting for actions to complete. " + match += "serial_numbers_done: FDO21120U5D, " + match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + with pytest.raises(AnsibleFailJson, match=match): module._wait_for_current_actions_to_complete() assert isinstance(module.serial_numbers_done, set) assert len(module.serial_numbers_done) == 1 From 7ec981be27279ef825788008e810fc9697479c10 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 08:34:44 -1000 Subject: [PATCH 119/300] Update docstrings --- .../test_image_upgrade_ImageUpgrade.py | 568 +++++++++++------- 1 file changed, 347 insertions(+), 221 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index eed63c278..2f1971e79 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -97,7 +97,11 @@ def mock_issu_details() -> SwitchIssuDetailsByIpAddress: def test_image_mgmt_upgrade_00001(module) -> None: """ - ImageUpgrade.__init__ initializes class attributes to expected values + Function + - __init__ + + Test + - Class attributes are initialized to expected values """ module.__init__(MockAnsibleModule) assert isinstance(module, ImageUpgrade) @@ -106,7 +110,11 @@ def test_image_mgmt_upgrade_00001(module) -> None: def test_image_mgmt_upgrade_00002(module) -> None: """ - ImageUpgrade._init_defaults initializes attributes to expected values + Function + - _init_defaults + + Test + - defaults dictionary is initialized with expected keys, values """ module._init_defaults() assert isinstance(module.defaults, dict) @@ -127,7 +135,11 @@ def test_image_mgmt_upgrade_00002(module) -> None: def test_image_mgmt_upgrade_00003(module) -> None: """ - ImageUpgrade._init_properties initializes properties to expected values + Function + - _init_properties + + Test + - Class properties are initialized to expected values """ module._init_properties() assert isinstance(module.properties, dict) @@ -159,8 +171,14 @@ def test_image_mgmt_upgrade_00003(module) -> None: def test_image_mgmt_upgrade_00004(monkeypatch, module) -> None: """ - Function description: + Function + - validate_devices + + Test + - ip_addresses contains the ip addresses of the devices for which + validation succeeds + Description ImageUpgrade.validate_devices updates the set ImageUpgrade.ip_addresses with the ip addresses of the devices for which validation succeeds. Currently, validation succeeds for all devices. This function may be @@ -189,11 +207,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_upgrade_00005(module) -> None: """ - Function: ImageUpgrade.commit + Function + - commit - Expected results: - - 1. ImageUpgrade.commit calls fail_json if devices is None + Test + - fail_json is called because devices is None """ match = "ImageUpgrade.commit: call instance.devices before calling commit." with pytest.raises(AnsibleFailJson, match=match): @@ -202,16 +220,15 @@ def test_image_mgmt_upgrade_00005(module) -> None: def test_image_mgmt_upgrade_00006(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: merged_config contains all default values + Function + - _merge_defaults_to_switch_config - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all values missing that have defaults defined (see ImageUpgrade._init_defaults) - Expected results: - - 1. merged_config will contain the expected default values + Test + - merged_config contains expected default values """ config = {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} @@ -233,18 +250,20 @@ def test_image_mgmt_upgrade_00006(module) -> None: def test_image_mgmt_upgrade_00007(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the upgrade.epld is None path + Function + - _merge_defaults_to_switch_config - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except upgrade.nxos. This forces the code + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except upgrade.nxos. This forces the code to take the upgrade.epld is None path. - Expected results: + Test + - merged_config contains expected default values + - merged_config contains expected non-default values - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values + Description + Force code coverage of the upgrade.epld is None path """ config = { "policy": "KR5M", @@ -271,18 +290,20 @@ def test_image_mgmt_upgrade_00007(module) -> None: def test_image_mgmt_upgrade_00008(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the upgrade.nxos is None path + Function + - _merge_defaults_to_switch_config - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except upgrade.epld. This forces the code + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except upgrade.epld. This forces the code to take the upgrade.nxos is None path. - Expected results: + Test + - merged_config contains expected default values + - merged_config contains expected non-default values - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values + Description + Force code coverage of the upgrade.nxos is None path """ config = { "policy": "KR5M", @@ -309,18 +330,21 @@ def test_image_mgmt_upgrade_00008(module) -> None: def test_image_mgmt_upgrade_00009(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.nxos is None path + Function + - _merge_defaults_to_switch_config - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options, which is empty. This forces - the code to take the options.nxos is None path and provide default - values for options.nxos and options.epld. + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options, which is empty. + This forces the code to take the options is None path + and provide default values for options.nxos and options.epld. - Expected results: + Test + - merged_config contains expected default values + - merged_config contains expected non-default values - 1. merged_config will contain the expected default values + Description + Force code coverage of the options is None path """ config = { "policy": "KR5M", @@ -347,18 +371,21 @@ def test_image_mgmt_upgrade_00009(module) -> None: def test_image_mgmt_upgrade_00010(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.nxos.bios_force is None path + Function + - _merge_defaults_to_switch_config - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.nxos.mode. This forces the code - to take the options.nxos.bios_force is None path. + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.nxos.mode. + This forces the code to take the options.nxos.bios_force + is None path. - Expected results: + Test + - merged_config contains expected default values + - merged_config contains expected non-default values - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values + Description + Force code coverage of the options.nxos.bios_force is None path """ config = { "policy": "KR5M", @@ -385,8 +412,11 @@ def test_image_mgmt_upgrade_00010(module) -> None: def test_image_mgmt_upgrade_00011(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.nxos.mode is None path + Function + - _merge_defaults_to_switch_config + + Test + - Force code coverage of the options.nxos.mode is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -423,8 +453,11 @@ def test_image_mgmt_upgrade_00011(module) -> None: def test_image_mgmt_upgrade_00012(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.epld.golden is None path + Function + - _merge_defaults_to_switch_config + + Test + - Force code coverage of the options.epld.golden is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -461,8 +494,11 @@ def test_image_mgmt_upgrade_00012(module) -> None: def test_image_mgmt_upgrade_00013(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.epld.module is None path + Function + - _merge_defaults_to_switch_config + + Test + - Force code coverage of the options.epld.module is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -499,8 +535,11 @@ def test_image_mgmt_upgrade_00013(module) -> None: def test_image_mgmt_upgrade_00014(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.reboot.write_erase is None path + Function + - _merge_defaults_to_switch_config + + Test + - Force code coverage of the options.reboot.write_erase is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -537,8 +576,11 @@ def test_image_mgmt_upgrade_00014(module) -> None: def test_image_mgmt_upgrade_00015(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.reboot.config_reload is None path + Function + - _merge_defaults_to_switch_config + + Test + - Force code coverage of the options.reboot.config_reload is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -575,8 +617,11 @@ def test_image_mgmt_upgrade_00015(module) -> None: def test_image_mgmt_upgrade_00016(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.package.uninstall is None path + Function + - _merge_defaults_to_switch_config + + Test + - Force code coverage of the options.package.uninstall is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -613,8 +658,11 @@ def test_image_mgmt_upgrade_00016(module) -> None: def test_image_mgmt_upgrade_00017(module) -> None: """ - Function: ImageUpgrade._merge_defaults_to_switch_config - Test: Force code coverage of the options.package.install is None path + Function + - _merge_defaults_to_switch_config + + Test + - Force code coverage of the options.package.install is None path Setup: 1. _merge_defaults_to_switch_config is passed a dictionary with all @@ -651,16 +699,19 @@ def test_image_mgmt_upgrade_00017(module) -> None: def test_image_mgmt_upgrade_00018(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: upgrade.nxos set to invalid value + Function + - commit - Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + Test + - upgrade.nxos set to invalid value + + Setup + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded. - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the the image has already been staged and validated and the device has already been upgraded to the desired version. - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing. Expected results: @@ -716,18 +767,21 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00019(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: non-default values are set for several options - Test: policy_changed is set to False + Function + - commit + Test + - non-default values are set for several options + - policy_changed is set to False - Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + + Setup + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded. - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the the image has already been staged and validated and the device has already been upgraded to the desired version. - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing. @@ -787,16 +841,19 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00020(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: User explicitely sets default values for several options - Test: policy_changed is set to True + Function + - commit + + Test + - User explicitely sets default values for several options + - policy_changed is set to True Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing @@ -854,17 +911,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for nxos.mode + Function + - commit + + Test + - Invalid value for nxos.mode Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain an invalid nxos.mode value + - module.devices is set to contain an invalid nxos.mode value Expected results: @@ -921,17 +981,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Force code coverage of nxos.mode == "non_disruptive" path + Function + - commit + + Test + - Force code coverage of nxos.mode == "non_disruptive" path Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain nxos.mode non_disruptive + - module.devices is set to contain nxos.mode non_disruptive forcing the code to take nxos_mode == "non_disruptive" path Expected results: @@ -990,17 +1053,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Force code coverage of nxos.mode == "force_non_disruptive" path + Function + - commit + + Test + - Force code coverage of nxos.mode == "force_non_disruptive" path Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain nxos.mode force_non_disruptive + - module.devices is set to contain nxos.mode force_non_disruptive forcing the code to take nxos_mode == "force_non_disruptive" path Expected results: @@ -1059,17 +1125,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for options.nxos.bios_force + Function + - commit + + Test + - Invalid value for options.nxos.bios_force Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for + - module.devices is set to contain invalid value for options.nxos.bios_force Expected results: @@ -1126,17 +1195,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00025(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Incompatible values for options.epld.golden and upgrade.nxos + Function + - commit + + Test + - Incompatible values for options.epld.golden and upgrade.nxos Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain epld golden True and + - module.devices is set to contain epld golden True and upgrade.nxos True. Expected results: @@ -1195,18 +1267,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00026(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for epld.module + Function + - commit + + Test + - Invalid value for epld.module Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - 4. module.devices is set to contain invalid epld.module + - module.devices is set to contain invalid epld.module Expected results: @@ -1267,17 +1341,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00027(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for epld.golden + Function + - commit + + Test + - Invalid value for epld.golden Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid epld.golden + - module.devices is set to contain invalid epld.golden Expected results: @@ -1337,17 +1414,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00028(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for reboot + Function + - commit + + Test + - Invalid value for reboot Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for reboot + - module.devices is set to contain invalid value for reboot Expected results: @@ -1403,17 +1483,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00029(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for options.reboot.config_reload + Function + - commit + + Test + - Invalid value for options.reboot.config_reload Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for + - module.devices is set to contain invalid value for options.reboot.config_reload Expected results: @@ -1474,17 +1557,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00030(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for options.reboot.write_erase + Function + - commit + + Test + - Invalid value for options.reboot.write_erase Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for + - module.devices is set to contain invalid value for options.reboot.write_erase Expected results: @@ -1545,17 +1631,19 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for options.package.uninstall + Function + - commit + Test + - Invalid value for options.package.uninstall Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for + - module.devices is set to contain invalid value for options.package.uninstall Expected results: @@ -1622,17 +1710,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00032(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Bad result code in image upgrade response + Function + - commit + + Test + - Bad result code in image upgrade response Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. ImageUpgrade response (mock_dcnm_send_image_upgrade) is set + - ImageUpgrade response (mock_dcnm_send_image_upgrade) is set to return RESULT_CODE 500 with MESSAGE "Internal Server Error" Expected results: @@ -1695,17 +1786,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00033(monkeypatch, module) -> None: """ - Function: ImageUpgrade.commit - Test: Invalid value for upgrade.epld + Function + - commit + + Test + - Invalid value for upgrade.epld Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - 4. module.devices is set to contain invalid value for + - module.devices is set to contain invalid value for upgrade.epld Expected results: @@ -1769,29 +1863,32 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00043(module) -> None: """ - ImageUpgrade.check_interval + Function + - check_interval """ assert module.check_interval == 10 def test_image_mgmt_upgrade_00044(module) -> None: """ - ImageUpgrade.check_timeout + Function + - check_timeout """ assert module.check_timeout == 1800 def test_image_mgmt_upgrade_00045(monkeypatch, module) -> None: """ - Function: ImageUpgrade.response_data + Function + - response_data Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded. - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the the image has already been staged and validated and the device has already been upgraded to the desired version. - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing. @@ -1847,15 +1944,16 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00046(monkeypatch, module) -> None: """ - Function: ImageUpgrade.result + Function + - result Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded. - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the the image has already been staged and validated and the device has already been upgraded to the desired version. - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing. @@ -1911,15 +2009,16 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00047(monkeypatch, module) -> None: """ - Function: ImageUpgrade.response + Function + - response Setup: - 1. ImageUpgrade.devices is set to a list of one dict for a device + - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded. - 2. The methods called by commit are mocked to simulate that the + - The methods called by commit are mocked to simulate that the the image has already been staged and validated and the device has already been upgraded to the desired version. - 3. Methods called by commit that wait for current actions, and + - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing. @@ -1990,7 +2089,8 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): ) def test_image_mgmt_upgrade_00060(module, value, expected) -> None: """ - ImageUpgrade.bios_force setter + Function + - bios_force setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00060): @@ -2014,7 +2114,8 @@ def test_image_mgmt_upgrade_00060(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00061(module, value, expected) -> None: """ - ImageUpgrade.config_reload setter + Function + - config_reload setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00061): @@ -2051,7 +2152,8 @@ def test_image_mgmt_upgrade_00061(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00062(module, value, expected) -> None: """ - ImageUpgrade.devices setter + Function + - devices setter """ with expected: module.devices = value @@ -2071,7 +2173,8 @@ def test_image_mgmt_upgrade_00062(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00063(module, value, expected) -> None: """ - ImageUpgrade.disruptive setter + Function + - disruptive setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00063): @@ -2095,7 +2198,8 @@ def test_image_mgmt_upgrade_00063(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00064(module, value, expected) -> None: """ - ImageUpgrade.epld_golden setter + Function + - epld_golden setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00064): @@ -2119,7 +2223,8 @@ def test_image_mgmt_upgrade_00064(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00065(module, value, expected) -> None: """ - ImageUpgrade.epld_upgrade setter + Function + - epld_upgrade setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00065): @@ -2145,7 +2250,8 @@ def test_image_mgmt_upgrade_00065(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00066(module, value, expected) -> None: """ - ImageUpgrade.epld_module setter + Function + - epld_module setter """ with expected: module.epld_module = value @@ -2165,7 +2271,8 @@ def test_image_mgmt_upgrade_00066(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00067(module, value, expected) -> None: """ - ImageUpgrade.force_non_disruptive setter + Function + - force_non_disruptive setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00067): @@ -2189,7 +2296,8 @@ def test_image_mgmt_upgrade_00067(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00068(module, value, expected) -> None: """ - ImageUpgrade.non_disruptive setter + Function + - non_disruptive setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00068): @@ -2213,7 +2321,8 @@ def test_image_mgmt_upgrade_00068(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00069(module, value, expected) -> None: """ - ImageUpgrade.package_install setter + Function + - package_install setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00069): @@ -2237,7 +2346,8 @@ def test_image_mgmt_upgrade_00069(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00070(module, value, expected) -> None: """ - ImageUpgrade.package_uninstall setter + Function + - package_uninstall setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00070): @@ -2261,7 +2371,8 @@ def test_image_mgmt_upgrade_00070(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00071(module, value, expected) -> None: """ - ImageUpgrade.reboot setter + Function + - reboot setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00071): @@ -2285,7 +2396,8 @@ def test_image_mgmt_upgrade_00071(module, value, expected) -> None: ) def test_image_mgmt_upgrade_00072(module, value, expected) -> None: """ - ImageUpgrade.write_erase setter + Function + - write_erase setter """ if value == "FOO": with pytest.raises(AnsibleFailJson, match=match_00072): @@ -2297,9 +2409,13 @@ def test_image_mgmt_upgrade_00072(module, value, expected) -> None: def test_image_mgmt_upgrade_00080(monkeypatch, module, mock_issu_details) -> None: """ - Function: ImageUpgrade._wait_for_current_actions_to_complete - Test: Verify that two switches are added to ipv4_done + Function + - _wait_for_current_actions_to_complete + + Test + - Two switches are added to ipv4_done + Description _wait_for_current_actions_to_complete waits until staging, validation, and upgrade actions are complete for all ip addresses. It calls SwitchIssuDetailsByIpAddress.actions_in_progress() and expects @@ -2309,11 +2425,11 @@ def test_image_mgmt_upgrade_00080(monkeypatch, module, mock_issu_details) -> Non ["imageStaged", "upgrade", "validated"] Expectations: - 1. module.ipv4_done should be a set() - 2. module.ipv4_done should be length 2 - 3. module.ipv4_done should contain all ip addresses in + 1. module.ipv4_done is a set() + 2. module.ipv4_done is length 2 + 3. module.ipv4_done contains all ip addresses in module.ip_addresses - 4. The function should return without calling fail_json. + 4. fail_json is not called """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -2328,7 +2444,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: "172.22.150.108", ] module.check_interval = 0 - module._wait_for_current_actions_to_complete() + with does_not_raise(): + module._wait_for_current_actions_to_complete() assert isinstance(module.ipv4_done, set) assert len(module.ipv4_done) == 2 assert "172.22.150.102" in module.ipv4_done @@ -2337,18 +2454,22 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_upgrade_00081(monkeypatch, module, mock_issu_details) -> None: """ - Function: ImageUpgrade._wait_for_current_actions_to_complete - Test: Verify that one switch is added to ipv4_done - Test: Verify that fail_json is called due to timeout + Function + - _wait_for_current_actions_to_complete + + Test + - one switch is added to ipv4_done + - fail_json is called due to timeout See test_image_mgmt_upgrade_00080 for functional details. Expectations: - 1. module.ipv4_done should be a set() - 2. module.ipv4_done should be length 1 - 3. module.ipv4_done should contain 172.22.150.102 - 3. module.ipv4_done should not contain 172.22.150.108 - 4. The function should call fail_json due to timeout + - module.ipv4_done is a set() + - module.ipv4_done is length 1 + - module.ipv4_done contains 172.22.150.102 + - module.ipv4_done does not contain 172.22.150.108 + - fail_json is called due to timeout + - fail_json error message is matched """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -2381,22 +2502,24 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_upgrade_00090(monkeypatch, module, mock_issu_details) -> None: """ - Function: ImageUpgrade._wait_for_image_upgrade_to_complete - Test: One ip address is added to ipv4_done due to - issu_detail.upgrade == "Success" - Test: fail_json is called due one ip address with - issu_detail.upgrade == "Failed" + Function + - _wait_for_image_upgrade_to_complete + + Test + - One ip address is added to ipv4_done due to issu_detail.upgrade == "Success" + - fail_json is called due one ip address with issu_detail.upgrade == "Failed" + Description _wait_for_image_upgrade_to_complete looks at the upgrade status for each ip address and waits for it to be "Success" or "Failed". In the case where all ip addresses are "Success", the module returns. In the case where any ip address is "Failed", the module calls fail_json. Expectations: - 1. module.ipv4_done is a set() - 2. module.ipv4_done has length 1 - 3. module.ipv4_done contains 172.22.150.102, upgrade is "Success" - 4. Call fail_json on ip address 172.22.150.108, upgrade is "Failed" + - module.ipv4_done is a set() + - module.ipv4_done has length 1 + - module.ipv4_done contains 172.22.150.102, upgrade is "Success" + - Call fail_json on ip address 172.22.150.108, upgrade is "Failed" """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: @@ -2427,12 +2550,15 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_upgrade_00091(monkeypatch, module, mock_issu_details) -> None: """ - Function: ImageUpgrade._wait_for_image_upgrade_to_complete - Test: One ip address is added to ipv4_done due to - issu_detail.upgrade == "Success" - Test: fail_json is called due to timeout because one - ip address has issu_detail.upgrade == "In-Progress" + Function + - _wait_for_image_upgrade_to_complete + Test + - One ip address is added to ipv4_done as + issu_detail.upgrade == "Success" + - fail_json is called due to timeout since one + ip address has value issu_detail.upgrade == "In-Progress" + Description _wait_for_image_upgrade_to_complete looks at the upgrade status for each ip address and waits for it to be "Success" or "Failed". In the case where all ip addresses are "Success", the module returns. @@ -2441,10 +2567,10 @@ def test_image_mgmt_upgrade_00091(monkeypatch, module, mock_issu_details) -> Non timeout is exceeded Expectations: - 1. module.ipv4_done is a set() - 2. module.ipv4_done has length 1 - 3. module.ipv4_done contains 172.22.150.102, upgrade is "Success" - 4. Call fail_json due to timeout exceeded + - module.ipv4_done is a set() + - module.ipv4_done has length 1 + - module.ipv4_done contains 172.22.150.102, upgrade is "Success" + - fail_json is called due to timeout exceeded """ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: From 7ad3bcaeeb62595cca050a10c04e232b23fafc97 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 08:40:07 -1000 Subject: [PATCH 120/300] Run thru black, isort, pylint --- .../test_image_upgrade_ApiEndpoints.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py index 381c141d0..8fdea2fa7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Tests for ApiEndpoints class +""" from __future__ import absolute_import, division, print_function from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -26,7 +29,6 @@ def test_image_mgmt_api_00001() -> None: Endpoints.__init__ """ endpoints = ApiEndpoints() - endpoints.__init__() assert endpoints.endpoint_api_v1 == "/appcenter/cisco/ndfc/api/v1" assert endpoints.endpoint_feature_manager == "/appcenter/cisco/ndfc/api/v1/fm" assert ( @@ -200,12 +202,11 @@ def test_image_mgmt_api_00014() -> None: """ Endpoints.policy_info """ + path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/" + path += "image-policy/__POLICY_NAME__" endpoints = ApiEndpoints() assert endpoints.policy_info.get("verb") == "GET" - assert ( - endpoints.policy_info.get("path") - == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__" - ) + assert endpoints.policy_info.get("path") == path def test_image_mgmt_api_00015() -> None: From 118211b70db23ab2afde3b6d2e8b18ebaaa67f16 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 08:50:33 -1000 Subject: [PATCH 121/300] Add docstrings --- .../test_image_upgrade_ControllerVersion.py | 7 +++ .../test_image_upgrade_ImageInstallOptions.py | 7 +++ .../test_image_upgrade_ImagePolicies.py | 7 +++ .../test_image_upgrade_ImagePolicyAction.py | 7 +++ .../test_image_upgrade_ImageStage.py | 44 ++++++++++++++----- .../test_image_upgrade_ImageUpgrade.py | 7 +++ .../test_image_upgrade_ImageUpgradeCommon.py | 9 +++- .../test_image_upgrade_ImageValidate.py | 7 +++ .../test_image_upgrade_SwitchDetails.py | 7 +++ ...e_upgrade_SwitchIssuDetailsByDeviceName.py | 7 +++ ...ge_upgrade_SwitchIssuDetailsByIpAddress.py | 7 +++ ...upgrade_SwitchIssuDetailsBySerialNumber.py | 7 +++ 12 files changed, 111 insertions(+), 12 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py index 4a66b926a..dfa7eafdd 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py @@ -47,9 +47,16 @@ def responses_controller_version(key: str) -> Dict[str, str]: class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index 4b4dd458f..dab97ecfe 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -41,9 +41,16 @@ class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py index 67d451961..12b0e8b1a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py @@ -41,9 +41,16 @@ class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index 206908f6e..9138664c8 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -83,9 +83,16 @@ def responses_switch_issu_details(key: str) -> Dict[str, str]: class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py index 9439cb801..6173318e6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py @@ -11,6 +11,9 @@ # 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. +""" +Tests for ImageStage class +""" from __future__ import absolute_import, division, print_function @@ -40,6 +43,9 @@ @contextmanager def does_not_raise(): + """ + A context manager that does not raise an exception. + """ yield @@ -53,30 +59,46 @@ def does_not_raise(): def responses_controller_version(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ControllerVersion" + """ + mock responses from ControllerVersion + """ + response_file = "image_upgrade_responses_ControllerVersion" response = load_fixture(response_file).get(key) print(f"responses_controller_version: {key} : {response}") return response def responses_image_stage(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ImageStage" + """ + mock responses from ImageStage + """ + response_file = "image_upgrade_responses_ImageStage" response = load_fixture(response_file).get(key) print(f"responses_image_stage: {key} : {response}") return response def responses_issu_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchIssuDetails" + """ + mock responses from SwitchIssuDetails + """ + response_file = "image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) print(f"responses_issu_details: {key} : {response}") return response class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) @@ -103,10 +125,10 @@ def test_image_mgmt_stage_00001(module) -> None: assert module.class_name == "ImageStage" assert isinstance(module.properties, dict) assert isinstance(module.serial_numbers_done, set) - assert module.controller_version == None - assert module.path == None - assert module.verb == None - assert module.payload == None + assert module.controller_version is None + assert module.path is None + assert module.verb is None + assert module.payload is None assert isinstance(module.issu_detail, SwitchIssuDetailsBySerialNumber) assert isinstance(module.endpoints, ApiEndpoints) @@ -121,10 +143,10 @@ def test_image_mgmt_stage_00002(module) -> None: """ module._init_properties() assert isinstance(module.properties, dict) - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None - assert module.properties.get("serial_numbers") == None + assert module.properties.get("response_data") is None + assert module.properties.get("response") is None + assert module.properties.get("result") is None + assert module.properties.get("serial_numbers") is None assert module.properties.get("check_interval") == 10 assert module.properties.get("check_timeout") == 1800 diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 2f1971e79..929dec8d1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -79,9 +79,16 @@ def responses_issu_details(key: str) -> Dict[str, str]: class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py index df19210bf..15697f90f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py @@ -40,9 +40,16 @@ def does_not_raise(): class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} - def fail_json(msg) -> dict: + def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index 36443b79d..e88419c0a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -50,9 +50,16 @@ def response_data_issu_details(key: str) -> Dict[str, str]: class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py index 149c1e339..8c85bd77c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py @@ -53,9 +53,16 @@ def responses_switch_details(key: str) -> Dict[str, str]: class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py index 561c87e2a..a0bc2edd9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py @@ -46,9 +46,16 @@ def does_not_raise(): class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py index f33122c87..49b077558 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py @@ -46,9 +46,16 @@ def does_not_raise(): class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py index 2c6ad1206..e0b758d4f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py @@ -46,9 +46,16 @@ def does_not_raise(): class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + params = {} def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ raise AnsibleFailJson(msg) From 63bd11de4f8076f94f0a05854366a104c7ad0e22 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 08:52:29 -1000 Subject: [PATCH 122/300] Add docstrings --- .../dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py | 3 +++ .../dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py | 3 +++ .../test_image_upgrade_ImageUpgradeCommon.py | 3 +++ .../dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py | 3 +++ .../test_image_upgrade_SwitchIssuDetailsByDeviceName.py | 3 +++ .../test_image_upgrade_SwitchIssuDetailsByIpAddress.py | 3 +++ .../test_image_upgrade_SwitchIssuDetailsBySerialNumber.py | 3 +++ 7 files changed, 21 insertions(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index 9138664c8..56a260a1a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -42,6 +42,9 @@ @contextmanager def does_not_raise(): + """ + A context manager that does not raise an exception. + """ yield diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index 929dec8d1..a9e668dca 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -38,6 +38,9 @@ @contextmanager def does_not_raise(): + """ + A context manager that does not raise an exception. + """ yield diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py index 15697f90f..cd766cc1f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py @@ -36,6 +36,9 @@ @contextmanager def does_not_raise(): + """ + A context manager that does not raise an exception. + """ yield diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py index 8c85bd77c..ab4dae080 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py @@ -36,6 +36,9 @@ @contextmanager def does_not_raise(): + """ + A context manager that does not raise an exception. + """ yield diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py index a0bc2edd9..6a5abae0d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py @@ -36,6 +36,9 @@ @contextmanager def does_not_raise(): + """ + A context manager that does not raise an exception. + """ yield diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py index 49b077558..554ca8f33 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py @@ -36,6 +36,9 @@ @contextmanager def does_not_raise(): + """ + A context manager that does not raise an exception. + """ yield diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py index e0b758d4f..cea6e0628 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py @@ -36,6 +36,9 @@ @contextmanager def does_not_raise(): + """ + A context manager that does not raise an exception. + """ yield From 22c8c51ff2ccdd9ae99f83d3a4bbef0e85788a7f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 08:58:00 -1000 Subject: [PATCH 123/300] Modify boolean asserts to satisfy pylint --- .../test_image_upgrade_ControllerVersion.py | 16 +- .../test_image_upgrade_ImageInstallOptions.py | 50 +-- .../test_image_upgrade_ImagePolicies.py | 6 +- .../test_image_upgrade_ImagePolicyAction.py | 4 +- .../test_image_upgrade_ImageUpgrade.py | 334 +++++++++--------- .../test_image_upgrade_ImageUpgradeCommon.py | 2 +- .../test_image_upgrade_ImageValidate.py | 2 +- ...e_upgrade_SwitchIssuDetailsByDeviceName.py | 8 +- ...ge_upgrade_SwitchIssuDetailsByIpAddress.py | 8 +- ...upgrade_SwitchIssuDetailsBySerialNumber.py | 8 +- 10 files changed, 219 insertions(+), 219 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py index dfa7eafdd..cda4681d2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py @@ -99,8 +99,8 @@ def test_common_version_00002(monkeypatch, controller_version, key, expected) -> Expected results: - 1. test_common_version_00002a == False - 2. test_common_version_00002b == True + 1. test_common_version_00002a is False + 2. test_common_version_00002b is True 3. test_common_version_00002c == None """ @@ -171,8 +171,8 @@ def test_common_version_00004(monkeypatch, controller_version, key, expected) -> Expected results: - 1. test_common_version_00004a == True - 2. test_common_version_00004b == False + 1. test_common_version_00004a is True + 2. test_common_version_00004b is False 3. test_common_version_00004c == None """ @@ -209,8 +209,8 @@ def test_common_version_00005(monkeypatch, controller_version, key, expected) -> Expected results: - 1. test_common_version_00005a == True - 2. test_common_version_00005b == False + 1. test_common_version_00005a is True + 2. test_common_version_00005b is False 3. test_common_version_00005c == None """ @@ -247,8 +247,8 @@ def test_common_version_00006(monkeypatch, controller_version, key, expected) -> Expected results: - 1. test_common_version_00006a == True - 2. test_common_version_00006b == False + 1. test_common_version_00006a is True + 2. test_common_version_00006b is False 3. test_common_version_00006c == None """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py index dab97ecfe..6436b4bf6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py @@ -82,10 +82,10 @@ def test_image_mgmt_install_options_00002(module) -> None: """ module._init_properties() assert isinstance(module.properties, dict) - assert module.properties.get("epld") == False + assert module.properties.get("epld") is False assert module.properties.get("epld_modules") == None - assert module.properties.get("issu") == True - assert module.properties.get("package_install") == False + assert module.properties.get("issu") is True + assert module.properties.get("package_install") is False assert module.properties.get("policy_name") == None assert module.properties.get("response") == None assert module.properties.get("response_data") == None @@ -165,7 +165,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert module.version == "10.2.5" comp_disp = "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin" assert module.comp_disp == comp_disp - assert module.result.get("success") == True + assert module.result.get("success") is True def test_image_mgmt_install_options_00006(monkeypatch, module) -> None: @@ -200,9 +200,9 @@ def test_image_mgmt_install_options_00007(monkeypatch, module) -> None: Setup - Device has no policy attached - POST REQUEST - - issu == True - - epld == False - - package_install == False + - issu is True + - epld is False + - package_install is False Test - 200 response from endpoint @@ -241,7 +241,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert module.version == "10.2.5" assert module.version_check == "Compatibility status skipped." assert module.comp_disp == "Compatibility status skipped." - assert module.result.get("success") == True + assert module.result.get("success") is True def test_image_mgmt_install_options_00008(monkeypatch, module) -> None: @@ -252,9 +252,9 @@ def test_image_mgmt_install_options_00008(monkeypatch, module) -> None: Setup - Device has no policy attached - POST REQUEST - - issu == True - - epld == True - - package_install == False + - issu is True + - epld is True + - package_install is False Test - 200 response from endpoint @@ -297,7 +297,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert module.version == "10.2.5" assert module.version_check == "Compatibility status skipped." assert module.comp_disp == "Compatibility status skipped." - assert module.result.get("success") == True + assert module.result.get("success") is True def test_image_mgmt_install_options_00009(monkeypatch, module) -> None: @@ -308,9 +308,9 @@ def test_image_mgmt_install_options_00009(monkeypatch, module) -> None: Setup - Device has no policy attached - POST REQUEST - - issu == False - - epld == True - - package_install == False + - issu is False + - epld is True + - package_install is False Test - 200 response from endpoint @@ -353,7 +353,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert module.version == None assert module.version_check == None assert module.comp_disp == None - assert module.result.get("success") == True + assert module.result.get("success") is True def test_image_mgmt_install_options_00010(monkeypatch, module) -> None: @@ -364,9 +364,9 @@ def test_image_mgmt_install_options_00010(monkeypatch, module) -> None: Setup - Device has no policy attached - POST REQUEST - - issu == False - - epld == True - - package_install == True (causes expected error) + - issu is False + - epld is True + - package_install is True (causes expected error) Test - 500 response from endpoint due to @@ -410,9 +410,9 @@ def test_image_mgmt_install_options_00020(module) -> None: module._build_payload() assert module.payload.get("devices")[0].get("policyName") == "KRM5" assert module.payload.get("devices")[0].get("serialNumber") == "BAR" - assert module.payload.get("issu") == True - assert module.payload.get("epld") == False - assert module.payload.get("packageInstall") == False + assert module.payload.get("issu") is True + assert module.payload.get("epld") is False + assert module.payload.get("packageInstall") is False def test_image_mgmt_install_options_00021(module) -> None: @@ -435,9 +435,9 @@ def test_image_mgmt_install_options_00021(module) -> None: module._build_payload() assert module.payload.get("devices")[0].get("policyName") == "KRM5" assert module.payload.get("devices")[0].get("serialNumber") == "BAR" - assert module.payload.get("issu") == False - assert module.payload.get("epld") == True - assert module.payload.get("packageInstall") == True + assert module.payload.get("issu") is False + assert module.payload.get("epld") is True + assert module.payload.get("packageInstall") is True def test_image_mgmt_install_options_00022(module) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py index 12b0e8b1a..d6cb7b0a3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py @@ -118,7 +118,7 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: image_policies.refresh() image_policies.policy_name = "KR5M" assert isinstance(image_policies.response, dict) - assert image_policies.agnostic == False + assert image_policies.agnostic is False assert image_policies.description == "10.2.(5) with EPLD" assert image_policies.epld_image_name == "n9000-epld.10.2.5.M.img" assert image_policies.image_name == "nxos64-cs.10.2.5.M.bin" @@ -153,8 +153,8 @@ def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: image_policies.refresh() assert isinstance(image_policies.result, dict) - assert image_policies.result.get("found") == True - assert image_policies.result.get("success") == True + assert image_policies.result.get("found") is True + assert image_policies.result.get("success") is True def test_image_mgmt_image_policies_00021(monkeypatch, image_policies) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py index 56a260a1a..9915f2d8e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py @@ -498,8 +498,8 @@ def mock_dcnm_send_image_policy_action(*args, **kwargs) -> Dict[str, Any]: image_policy_action.response.get("DATA") == "Successfully detach the policy from device." ) - assert image_policy_action.result.get("success") == True - assert image_policy_action.result.get("changed") == True + assert image_policy_action.result.get("success") is True + assert image_policy_action.result.get("changed") is True match_00060 = "ImagePolicyAction.action: instance.action must be " diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py index a9e668dca..f2ed8ed2f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py @@ -128,19 +128,19 @@ def test_image_mgmt_upgrade_00002(module) -> None: """ module._init_defaults() assert isinstance(module.defaults, dict) - assert module.defaults["reboot"] == False - assert module.defaults["stage"] == True - assert module.defaults["validate"] == True - assert module.defaults["upgrade"]["nxos"] == True - assert module.defaults["upgrade"]["epld"] == False + assert module.defaults["reboot"] is False + assert module.defaults["stage"] is True + assert module.defaults["validate"] is True + assert module.defaults["upgrade"]["nxos"] is True + assert module.defaults["upgrade"]["epld"] is False assert module.defaults["options"]["nxos"]["mode"] == "disruptive" - assert module.defaults["options"]["nxos"]["bios_force"] == False + assert module.defaults["options"]["nxos"]["bios_force"] is False assert module.defaults["options"]["epld"]["module"] == "ALL" - assert module.defaults["options"]["epld"]["golden"] == False - assert module.defaults["options"]["reboot"]["config_reload"] == False - assert module.defaults["options"]["reboot"]["write_erase"] == False - assert module.defaults["options"]["package"]["install"] == False - assert module.defaults["options"]["package"]["uninstall"] == False + assert module.defaults["options"]["epld"]["golden"] is False + assert module.defaults["options"]["reboot"]["config_reload"] is False + assert module.defaults["options"]["reboot"]["write_erase"] is False + assert module.defaults["options"]["package"]["install"] is False + assert module.defaults["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00003(module) -> None: @@ -153,25 +153,25 @@ def test_image_mgmt_upgrade_00003(module) -> None: """ module._init_properties() assert isinstance(module.properties, dict) - assert module.properties.get("bios_force") == False + assert module.properties.get("bios_force") is False assert module.properties.get("check_interval") == 10 assert module.properties.get("check_timeout") == 1800 - assert module.properties.get("config_reload") == False + assert module.properties.get("config_reload") is False assert module.properties.get("devices") == None - assert module.properties.get("disruptive") == True - assert module.properties.get("epld_golden") == False + assert module.properties.get("disruptive") is True + assert module.properties.get("epld_golden") is False assert module.properties.get("epld_module") == "ALL" - assert module.properties.get("epld_upgrade") == False - assert module.properties.get("force_non_disruptive") == False + assert module.properties.get("epld_upgrade") is False + assert module.properties.get("force_non_disruptive") is False assert module.properties.get("response_data") == None assert module.properties.get("response") == None assert module.properties.get("result") == None - assert module.properties.get("non_disruptive") == False - assert module.properties.get("force_non_disruptive") == False - assert module.properties.get("package_install") == False - assert module.properties.get("package_uninstall") == False - assert module.properties.get("reboot") == False - assert module.properties.get("write_erase") == False + assert module.properties.get("non_disruptive") is False + assert module.properties.get("force_non_disruptive") is False + assert module.properties.get("package_install") is False + assert module.properties.get("package_uninstall") is False + assert module.properties.get("reboot") is False + assert module.properties.get("write_erase") is False assert module.valid_nxos_mode == { "disruptive", "non_disruptive", @@ -243,19 +243,19 @@ def test_image_mgmt_upgrade_00006(module) -> None: config = {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00007(module) -> None: @@ -283,19 +283,19 @@ def test_image_mgmt_upgrade_00007(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == False - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is False + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00008(module) -> None: @@ -323,19 +323,19 @@ def test_image_mgmt_upgrade_00008(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == True + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is True assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00009(module) -> None: @@ -364,19 +364,19 @@ def test_image_mgmt_upgrade_00009(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00010(module) -> None: @@ -405,19 +405,19 @@ def test_image_mgmt_upgrade_00010(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "non_disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00011(module) -> None: @@ -446,19 +446,19 @@ def test_image_mgmt_upgrade_00011(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == True + assert merged_config["options"]["nxos"]["bios_force"] is True assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00012(module) -> None: @@ -487,19 +487,19 @@ def test_image_mgmt_upgrade_00012(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == 27 - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00013(module) -> None: @@ -528,19 +528,19 @@ def test_image_mgmt_upgrade_00013(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == True - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is True + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00014(module) -> None: @@ -569,19 +569,19 @@ def test_image_mgmt_upgrade_00014(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == True - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is True + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00015(module) -> None: @@ -610,19 +610,19 @@ def test_image_mgmt_upgrade_00015(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == True - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is True + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00016(module) -> None: @@ -651,19 +651,19 @@ def test_image_mgmt_upgrade_00016(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == True - assert merged_config["options"]["package"]["uninstall"] == False + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is True + assert merged_config["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00017(module) -> None: @@ -692,19 +692,19 @@ def test_image_mgmt_upgrade_00017(module) -> None: } merged_config = module._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] == False - assert merged_config["stage"] == True - assert merged_config["validate"] == True - assert merged_config["upgrade"]["nxos"] == True - assert merged_config["upgrade"]["epld"] == False + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] == False + assert merged_config["options"]["nxos"]["bios_force"] is False assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] == False - assert merged_config["options"]["reboot"]["config_reload"] == False - assert merged_config["options"]["reboot"]["write_erase"] == False - assert merged_config["options"]["package"]["install"] == False - assert merged_config["options"]["package"]["uninstall"] == True + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is True def test_image_mgmt_upgrade_00018(monkeypatch, module) -> None: @@ -1009,9 +1009,9 @@ def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: Expected results: - 1. self.payload["issuUpgradeOptions1"]["disruptive"] == False - 2. self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] == False - 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] == True + 1. self.payload["issuUpgradeOptions1"]["disruptive"] is False + 2. self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is False + 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] is True """ key = "test_image_mgmt_upgrade_00022a" @@ -1056,9 +1056,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] module.commit() - assert module.payload["issuUpgradeOptions1"]["disruptive"] == False - assert module.payload["issuUpgradeOptions1"]["forceNonDisruptive"] == False - assert module.payload["issuUpgradeOptions1"]["nonDisruptive"] == True + assert module.payload["issuUpgradeOptions1"]["disruptive"] is False + assert module.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is False + assert module.payload["issuUpgradeOptions1"]["nonDisruptive"] is True def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: @@ -1081,9 +1081,9 @@ def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: Expected results: - 1. self.payload["issuUpgradeOptions1"]["disruptive"] == False - 2. self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] == True - 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] == False + 1. self.payload["issuUpgradeOptions1"]["disruptive"] is False + 2. self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is True + 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] is False """ key = "test_image_mgmt_upgrade_00023a" @@ -1128,9 +1128,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] module.commit() - assert module.payload["issuUpgradeOptions1"]["disruptive"] == False - assert module.payload["issuUpgradeOptions1"]["forceNonDisruptive"] == True - assert module.payload["issuUpgradeOptions1"]["nonDisruptive"] == False + assert module.payload["issuUpgradeOptions1"]["disruptive"] is False + assert module.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is True + assert module.payload["issuUpgradeOptions1"]["nonDisruptive"] is False def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py index cd766cc1f..89a03605b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py @@ -84,7 +84,7 @@ def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: with does_not_raise(): image_upgrade_common.__init__(MockAnsibleModule) assert image_upgrade_common.params == {} - assert image_upgrade_common.debug == False + assert image_upgrade_common.debug is False assert image_upgrade_common.fd == None assert image_upgrade_common.logfile == "/tmp/ansible_dcnm.log" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py index e88419c0a..91e151acf 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py @@ -102,7 +102,7 @@ def test_image_mgmt_validate_00002(module) -> None: assert module.properties.get("response_data") == {} assert module.properties.get("response") == {} assert module.properties.get("result") == {} - assert module.properties.get("non_disruptive") == False + assert module.properties.get("non_disruptive") is False assert module.properties.get("serial_numbers") == [] diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py index 6a5abae0d..d9e50b34d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py @@ -165,7 +165,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: # verify remaining properties using current device_name assert issu_details.eth_switch_id == 39890 assert issu_details.fabric == "hard" - assert issu_details.fcoe_enabled == False + assert issu_details.fcoe_enabled is False assert issu_details.group == "hard" # NOTE: For "id" see switch_id below assert issu_details.image_staged == "Success" @@ -173,7 +173,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert issu_details.ip_address == "172.22.150.108" assert issu_details.issu_allowed == None assert issu_details.last_upg_action == "2023-Oct-06 03:43" - assert issu_details.mds == False + assert issu_details.mds is False assert issu_details.mode == "Normal" assert issu_details.model == "N9K-C93180YC-EX" assert issu_details.model_type == 0 @@ -234,8 +234,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: issu_details.refresh() assert isinstance(issu_details.result, dict) - assert issu_details.result.get("found") == True - assert issu_details.result.get("success") == True + assert issu_details.result.get("found") is True + assert issu_details.result.get("success") is True def test_image_mgmt_switch_issu_details_by_device_name_00023( diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py index 554ca8f33..26e4c887b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py @@ -165,7 +165,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: # verify remaining properties using current ip_address assert issu_details.eth_switch_id == 39890 assert issu_details.fabric == "hard" - assert issu_details.fcoe_enabled == False + assert issu_details.fcoe_enabled is False assert issu_details.group == "hard" # NOTE: For "id" see switch_id below assert issu_details.image_staged == "Success" @@ -173,7 +173,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert issu_details.ip_address == "172.22.150.108" assert issu_details.issu_allowed == None assert issu_details.last_upg_action == "2023-Oct-06 03:43" - assert issu_details.mds == False + assert issu_details.mds is False assert issu_details.mode == "Normal" assert issu_details.model == "N9K-C93180YC-EX" assert issu_details.model_type == 0 @@ -234,8 +234,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: issu_details.refresh() assert isinstance(issu_details.result, dict) - assert issu_details.result.get("found") == True - assert issu_details.result.get("success") == True + assert issu_details.result.get("found") is True + assert issu_details.result.get("success") is True def test_image_mgmt_switch_issu_details_by_ip_address_00023( diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py index cea6e0628..e51f4da96 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py @@ -165,7 +165,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: # verify remaining properties using current serial_number assert issu_details.eth_switch_id == 39890 assert issu_details.fabric == "hard" - assert issu_details.fcoe_enabled == False + assert issu_details.fcoe_enabled is False assert issu_details.group == "hard" # NOTE: For "id" see switch_id below assert issu_details.image_staged == "Success" @@ -173,7 +173,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert issu_details.ip_address == "172.22.150.108" assert issu_details.issu_allowed == None assert issu_details.last_upg_action == "2023-Oct-06 03:43" - assert issu_details.mds == False + assert issu_details.mds is False assert issu_details.mode == "Normal" assert issu_details.model == "N9K-C93180YC-EX" assert issu_details.model_type == 0 @@ -234,8 +234,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: issu_details.refresh() assert isinstance(issu_details.result, dict) - assert issu_details.result.get("found") == True - assert issu_details.result.get("success") == True + assert issu_details.result.get("found") is True + assert issu_details.result.get("success") is True def test_image_mgmt_switch_issu_details_by_serial_number_00023( From d09bea6f7bea25aa5bf67f4e67e21c4547829298 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 18:37:32 -1000 Subject: [PATCH 124/300] Appease linters and ansible sanity (part 1) --- .../module_utils/common/controller_version.py | 4 +- .../module_utils/image_mgmt/api_endpoints.py | 3 + .../image_mgmt/image_upgrade_common.py | 6 + .../image_mgmt/switch_issu_details.py | 2 +- .../dcnm_image_upgrade/image_upgrade_utils.py | 107 ++ .../test_image_upgrade_ControllerVersion.py | 614 ---------- .../test_image_upgrade_ImageInstallOptions.py | 482 -------- ...py => test_image_upgrade_api_endpoints.py} | 2 +- .../test_image_upgrade_controller_version.py | 627 ++++++++++ ...est_image_upgrade_image_install_options.py | 501 ++++++++ ...y => test_image_upgrade_image_policies.py} | 165 +-- ...test_image_upgrade_image_policy_action.py} | 329 +++--- ...e.py => test_image_upgrade_image_stage.py} | 377 +++--- ...py => test_image_upgrade_image_upgrade.py} | 1052 +++++++++-------- ...est_image_upgrade_image_upgrade_common.py} | 128 +- ...y => test_image_upgrade_image_validate.py} | 15 +- ...y => test_image_upgrade_switch_details.py} | 15 +- ...ade_switch_issu_details_by_device_name.py} | 200 ++-- ...rade_switch_issu_details_by_ip_address.py} | 200 ++-- ...e_switch_issu_details_by_serial_number.py} | 14 +- 20 files changed, 2494 insertions(+), 2349 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py delete mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py delete mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_ApiEndpoints.py => test_image_upgrade_api_endpoints.py} (99%) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_ImagePolicies.py => test_image_upgrade_image_policies.py} (64%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_ImagePolicyAction.py => test_image_upgrade_image_policy_action.py} (56%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_ImageStage.py => test_image_upgrade_image_stage.py} (56%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_ImageUpgrade.py => test_image_upgrade_image_upgrade.py} (72%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_ImageUpgradeCommon.py => test_image_upgrade_image_upgrade_common.py} (80%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_ImageValidate.py => test_image_upgrade_image_validate.py} (98%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_SwitchDetails.py => test_image_upgrade_switch_details.py} (97%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_SwitchIssuDetailsByDeviceName.py => test_image_upgrade_switch_issu_details_by_device_name.py} (62%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_SwitchIssuDetailsByIpAddress.py => test_image_upgrade_switch_issu_details_by_ip_address.py} (63%) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_SwitchIssuDetailsBySerialNumber.py => test_image_upgrade_switch_issu_details_by_serial_number.py} (98%) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 2fb78b257..a1b39e86d 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -1,3 +1,5 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) @@ -59,7 +61,7 @@ def refresh(self): self.properties["response"] = dcnm_send(self.module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) - if self.result["success"] == False or self.result["found"] == False: + if self.result["success"] is False or self.result["found"] is False: msg = f"{self.class_name}.refresh() failed: {self.result}" self.module.fail_json(msg) diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py index 70c1fc0ac..37f45438f 100644 --- a/plugins/module_utils/image_mgmt/api_endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -1,6 +1,9 @@ """ Endpoints for image management API calls """ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + class ApiEndpoints: """ Endpoints for image management API calls diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index b5d95c01a..75c4f8e6a 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -1,3 +1,9 @@ +""" +Base class for the other image upgrade classes +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + import inspect diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index a6c2a88a2..38b265350 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -99,7 +99,7 @@ def refresh(self) -> None: self.properties["response"] = dcnm_send(self.module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) - if self.result["success"] == False or self.result["found"] == False: + if self.result["success"] is False or self.result["found"] == False: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retriving switch " msg += "information from the controller" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py new file mode 100644 index 000000000..e3e9269bc --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -0,0 +1,107 @@ +# Copyright (c) 2020-2024 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. + +""" +Utilities for image_upgrade unit tests +""" +from __future__ import absolute_import, division, print_function +import pytest +from contextlib import contextmanager + +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ + ControllerVersion +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ + ImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ + ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ + ImagePolicyAction +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ + ImageStage +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ + ImageUpgrade +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsByDeviceName +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsByIpAddress +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsBySerialNumber + +class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + + params = {} + + @staticmethod + def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ + raise AnsibleFailJson(msg) + +#See the following for explanation of why fixtures are explicitely named +#https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html + +@pytest.fixture(name="controller_version") +def controller_version_fixture(): + return ControllerVersion(MockAnsibleModule) + +@pytest.fixture(name="image_install_options") +def image_install_options_fixture(): + return ImageInstallOptions(MockAnsibleModule) + +@pytest.fixture(name="image_policies") +def image_policies_fixture(): + return ImagePolicies(MockAnsibleModule) + +@pytest.fixture(name="image_policy_action") +def image_policy_action_fixture(): + return ImagePolicyAction(MockAnsibleModule) + +@pytest.fixture(name="image_stage") +def image_stage_fixture(): + return ImageStage(MockAnsibleModule) + +@pytest.fixture(name="image_upgrade_common") +def image_upgrade_common_fixture(): + return ImageUpgradeCommon(MockAnsibleModule) + +@pytest.fixture(name="image_upgrade") +def image_upgrade_fixture(): + return ImageUpgrade(MockAnsibleModule) + +@pytest.fixture(name="issu_details_by_ip_address") +def issu_details_by_ip_address_fixture(): + return SwitchIssuDetailsByIpAddress(MockAnsibleModule) + +@pytest.fixture(name="issu_details_by_device_name") +def issu_details_by_device_name_fixture(): + return SwitchIssuDetailsByDeviceName(MockAnsibleModule) + +@pytest.fixture(name="issu_details_by_serial_number") +def issu_details_by_serial_number_fixture() -> SwitchIssuDetailsBySerialNumber: + return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) + +@contextmanager +def does_not_raise(): + """ + A context manager that does not raise an exception. + """ + yield diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py deleted file mode 100644 index cda4681d2..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ControllerVersion.py +++ /dev/null @@ -1,614 +0,0 @@ -# Copyright (c) 2020-2024 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 - -from typing import Any, Dict - -import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ - ControllerVersion - -from .fixture import load_fixture - -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - -""" -controller_version: 12 -description: Verify functionality of ControllerVersion -""" - - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_common = patch_module_utils + "common." - -dcnm_send_version = patch_common + "controller_version.dcnm_send" - - -def responses_controller_version(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ControllerVersion" - response = load_fixture(response_file).get(key) - print(f"responses_controller_version: {key} : {response}") - return response - - -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - -@pytest.fixture -def controller_version(): - return ControllerVersion(MockAnsibleModule) - - -def test_common_version_00001(controller_version) -> None: - """ - Properties are initialized to expected values - """ - controller_version._init_properties() - assert isinstance(controller_version.properties, dict) - assert controller_version.properties.get("response_data") == None - assert controller_version.properties.get("response") == None - assert controller_version.properties.get("result") == None - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00002a", False), - ("test_common_version_00002b", True), - ("test_common_version_00002c", None), - ], -) -def test_common_version_00002(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - ControllerVersion.dev returns: - - True if the controller is a development version - - False if the controller is not a development version - - None otherwise - - Expectations: - - 1. ControllerVersion.dev returns above values given corresponding responses - - Expected results: - - 1. test_common_version_00002a is False - 2. test_common_version_00002b is True - 3. test_common_version_00002c == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.dev == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00003a", "EASYFABRIC"), - ("test_common_version_00003b", None), - ], -) -def test_common_version_00003(monkeypatch, controller_version, key, expected) -> None: - """ - Description: - - ControllerVersion.install returns: - - Value of controller response "install" key, if present - - None, if controller response "install" key is missing - - Expectations: - - 1. ControllerVersion.install returns above values given - corresponding responses - - Expected results: - - 1. test_common_version_00003a == "EASYFABRIC" - 2. test_common_version_00003b == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.install == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00004a", True), - ("test_common_version_00004b", False), - ("test_common_version_00004c", None), - ], -) -def test_common_version_00004(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - ControllerVersion.is_ha_enabled returns: - - True, if controller response "isHaEnabled" key == "true" - - False, if controller response "isHaEnabled" key == "false" - - None, if controller response "isHaEnabled" key is missing - - Expectations: - - 1. install returns above values given corresponding responses - - Expected results: - - 1. test_common_version_00004a is True - 2. test_common_version_00004b is False - 3. test_common_version_00004c == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.is_ha_enabled == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00005a", True), - ("test_common_version_00005b", False), - ("test_common_version_00005c", None), - ], -) -def test_common_version_00005(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - ControllerVersion.is_media_controller returns: - - True, if controller response "isMediaController" key == "true" - - False, if controller response "isMediaController" key == "false" - - None, if controller response "isMediaController" key is missing - - Expectations: - - 1. ControllerVersion.is_media_controller returns above values - given corresponding responses - - Expected results: - - 1. test_common_version_00005a is True - 2. test_common_version_00005b is False - 3. test_common_version_00005c == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.is_media_controller == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00006a", True), - ("test_common_version_00006b", False), - ("test_common_version_00006c", None), - ], -) -def test_common_version_00006(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - ControllerVersion.is_ha_enabled returns: - - True, if NDFC response "is_upgrade_inprogress" key == "true" - - False, if NDFC response "is_upgrade_inprogress" key == "false" - - None, if NDFC response "is_upgrade_inprogress" key is missing - - Expectations: - - 1. ControllerVersion.is_upgrade_inprogress returns above values - given corresponding responses - - Expected results: - - 1. test_common_version_00006a is True - 2. test_common_version_00006b is False - 3. test_common_version_00006c == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.is_upgrade_inprogress == expected - - -def test_common_version_00007(monkeypatch, controller_version) -> None: - """ - Function description: - - ControllerVersion.response_data returns the "DATA" key in the - NDFC response, which is a dictionary of key-value pairs. - If the "DATA" key is absent, fail_json is called. - - Expectations: - - 1. ControllerVersion.response_data will return a dictionary of key-value - pairs - - Expected results: - - 1. test_common_version_00007a, ControllerVersion.response_data == type(dict) - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "test_common_version_00007a" - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert isinstance(controller_version.response_data, dict) - - -def test_common_version_00008(monkeypatch, controller_version) -> None: - """ - Function description: - - See: test_response_data_present - - Expectations: - - 1. fail_json is called if the "DATA" key is absent - - Expected results: - - 1. fail_json is called - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "test_common_version_00008a" - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - with pytest.raises(AnsibleFailJson): - controller_version.refresh() - - -def test_common_version_00009(monkeypatch, controller_version) -> None: - """ - Function description: - - ControllerVersion.result returns the result of its superclass - method ImageUpgradeCommon._handle_response() - - Expectations: - - 1. For a 200 response with "message" key == "OK", - ControllerVersion.result == {'found': True, 'success': True} - - Expected results: - - 1. ControllerVersion_result_200 == {'found': True, 'success': True} - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "test_common_version_00009a" - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.result == {"found": True, "success": True} - - -def test_common_version_00010(monkeypatch, controller_version) -> None: - """ - Function description: - - See: test_result_200 - - Expectations: - - 1. For a 404 response with "message" key == "Not Found", - ControllerVersion.result == {'found': False, 'success': True} - and ControllerVersion.refresh() calls fail_json - - Expected results: - - 1. fail_json is called - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "test_common_version_00010a" - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - with pytest.raises(AnsibleFailJson): - controller_version.refresh() - - -def test_common_version_00011(monkeypatch, controller_version) -> None: - """ - Function description: - - See: test_result_200 - - Expectations: - - 1. For a 500 response with any "message" key value, - ControllerVersion.result == {'found': False, 'success': False} - and ControllerVersion.refresh() calls fail_json - - Expected results: - - 1. fail_json is called - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - key = "test_common_version_00011a" - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - with pytest.raises(AnsibleFailJson): - controller_version.refresh() - - -@pytest.mark.parametrize( - "key, expected", - [("test_common_version_00012a", "LAN"), ("test_common_version_00012b", None)], -) -def test_common_version_00012(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - ControllerVersion.mode returns: - - If controller response "mode" key is present, its value - - If controller response "mode" key is not present, None - - Expectations: - - 1. ControllerVersion.mode returns above values - given corresponding responses - - Expected results: - - 1. test_common_version_00012a == "LAN" - 2. test_common_version_00012b == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.mode == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00013a", "foo-uuid"), - ("test_common_version_00013b", None), - ], -) -def test_common_version_00013(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - ControllerVersion.uuid returns: - - If controller response "uuid" key is present, its value - - If controller response "uuid" key is not present, None - - Expectations: - - 1. ControllerVersion.uuid returns above values - given corresponding responses - - Expected results: - - 1. test_common_version_00013a == "foo-uuid" - 2. test_common_version_00013b == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.uuid == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00014a", "12.1.3b"), - ("test_common_version_00014b", None), - ], -) -def test_common_version_00014(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - ControllerVersion.version returns: - - If controller response "version" key is present, its value - - If controller response "version" key is not present, None - - Expectations: - - 1. ControllerVersion.version returns above values - given corresponding responses - - Expected results: - - 1. test_common_version_00014a == "12.1.3b" - 2. test_common_version_00014b == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.version == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00015a", "12"), - ("test_common_version_00015b", None), - ], -) -def test_common_version_00015(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - version_major returns the major version of NDFC - It derives this from the "version" key in the NDFC response - by splitting the string on "." and returning the first element - - ControllerVersion.version_major returns: - - If controller response "version" key is present, the major version - - If controller response "version" key is not present, None - - Expectations: - - 1. ControllerVersion.version_major returns above values - given corresponding responses - - Expected results: - - 1. test_common_version_00015a == "12" - 2. test_common_version_00015b == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.version_major == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00016a", "1"), - ("test_common_version_00016b", None), - ], -) -def test_common_version_00016(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - version_minor returns the minor version of NDFC - It derives this from the "version" key in the NDFC response - by splitting the string on "." and returning the second element - - ControllerVersion.version_minor returns: - - If controller response "version" key is present, the minor version - - If controller response "version" key is not present, None - - Expectations: - - 1. ControllerVersion.version_minor returns above values - given corresponding responses - - Expected results: - - 1. test_common_version_00016a == "1" - 2. test_common_version_00016b == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.version_minor == expected - - -@pytest.mark.parametrize( - "key, expected", - [ - ("test_common_version_00017a", "3b"), - ("test_common_version_00017b", None), - ], -) -def test_common_version_00017(monkeypatch, controller_version, key, expected) -> None: - """ - Function description: - - version_patch returns the patch version of NDFC - It derives this from the "version" key in the NDFC response - by splitting the string on "." and returning the third element - - ControllerVersion.version_patch returns: - - If controller response "version" key is present, the patch version - - If controller response "version" key is not present, None - - Expectations: - - 1. ControllerVersion.version_patch returns above values - given corresponding responses - - Expected results: - - 1. test_common_version_00017a == "3b" - 2. test_common_version_00017b == None - """ - - def mock_dcnm_send_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - monkeypatch.setattr(dcnm_send_version, mock_dcnm_send_version) - - controller_version.refresh() - assert controller_version.version_patch == expected diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py deleted file mode 100644 index 6436b4bf6..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageInstallOptions.py +++ /dev/null @@ -1,482 +0,0 @@ -# Copyright (c) 2020-2024 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 - -from typing import Any, Dict - -import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ - ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ - ImageInstallOptions - -from .fixture import load_fixture - -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - -""" -controller_version: 12 -description: Verify functionality of class ImageInstallOptions -""" - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." - -dcnm_send_install_options = patch_image_mgmt + "install_options.dcnm_send" - - -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - -def responses_image_install_options(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ImageInstallOptions" - response = load_fixture(response_file).get(key) - print(f"{key} : : {response}") - return response - - -@pytest.fixture -def module(): - return ImageInstallOptions(MockAnsibleModule) - - -def test_image_mgmt_install_options_00001(module) -> None: - """ - Verify attributes set in __init__ - """ - module.__init__(MockAnsibleModule) - assert module.module == MockAnsibleModule - assert module.class_name == "ImageInstallOptions" - assert isinstance(module.endpoints, ApiEndpoints) - - -def test_image_mgmt_install_options_00002(module) -> None: - """ - Properties are initialized to expected values - """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("epld") is False - assert module.properties.get("epld_modules") == None - assert module.properties.get("issu") is True - assert module.properties.get("package_install") is False - assert module.properties.get("policy_name") == None - assert module.properties.get("response") == None - assert module.properties.get("response_data") == None - assert module.properties.get("result") == None - assert module.properties.get("serial_number") == None - - -# test_image_mgmt_install_options_00003 -# test_policy_name_not_defined (former name) - - -def test_image_mgmt_install_options_00003(module) -> None: - """ - fail_json() is called if policy_name is not set when refresh() is called. - """ - module.serial_number = "FOO" - match = "ImageInstallOptions.refresh: " - match += "instance.policy_name must be set before " - match += r"calling refresh\(\)" - with pytest.raises(AnsibleFailJson, match=match): - module.refresh() - - -# test_image_mgmt_install_options_00004 -# test_serial_number_not_defined (former name) - - -def test_image_mgmt_install_options_00004(module) -> None: - """ - fail_json() is called if serial_number is not set when refresh() is called. - """ - module.policy_name = "FOO" - match = "ImageInstallOptions.refresh: " - match += "instance.serial_number must be set before " - match += r"calling refresh\(\)" - with pytest.raises(AnsibleFailJson, match=match): - module.refresh() - - -def test_image_mgmt_install_options_00005(monkeypatch, module) -> None: - """ - Function - - refresh - - Test - - 200 response from endpoint - - Properties are updated with expected values - - endpoint: install-options - """ - - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00005a" - return responses_image_install_options(key) - - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - - module.policy_name = "KRM5" - module.serial_number = "BAR" - module.refresh() - assert isinstance(module.response, dict) - assert module.device_name == "cvd-1314-leaf" - assert module.err_message is None - assert module.epld_modules is None - assert module.install_option == "disruptive" - assert module.install_packages is None - assert module.ip_address == "172.22.150.105" - assert module.os_type == "64bit" - assert module.platform == "N9K/N3K" - assert module.pre_issu_link == "Not Applicable" - assert isinstance(module.raw_data, dict) - assert isinstance(module.raw_response, dict) - assert "compatibilityStatusList" in module.raw_data - assert module.rep_status == "skipped" - assert module.serial_number == "BAR" - assert module.status == "Success" - assert module.timestamp == "NA" - assert module.version == "10.2.5" - comp_disp = "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin" - assert module.comp_disp == comp_disp - assert module.result.get("success") is True - - -def test_image_mgmt_install_options_00006(monkeypatch, module) -> None: - """ - Function - - refresh - - Test - - fail_json is called if the response RETURN_CODE != 200 - """ - - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00006a" - return responses_image_install_options(key) - - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - - module.policy_name = "KRM5" - module.serial_number = "BAR" - match = "ImageInstallOptions.refresh: " - match += "Bad result when retrieving install-options from " - match += "the controller. Controller response:" - with pytest.raises(AnsibleFailJson, match=rf"{match}"): - module.refresh() - - -def test_image_mgmt_install_options_00007(monkeypatch, module) -> None: - """ - Function - - refresh - - Setup - - Device has no policy attached - - POST REQUEST - - issu is True - - epld is False - - package_install is False - - Test - - 200 response from endpoint - - Response contains expected values - - Endpoint - - install-options - """ - - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00007a" - return responses_image_install_options(key) - - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - - module.policy_name = "KRM5" - module.serial_number = "FDO21120U5D" - module.refresh() - assert isinstance(module.response, dict) - assert module.device_name == "leaf1" - assert module.err_message is None - assert module.epld_modules is None - assert module.install_option == "NA" - assert module.install_packages is None - assert module.ip_address == "172.22.150.102" - assert module.os_type == "64bit" - assert module.platform == "N9K/N3K" - assert module.pre_issu_link == "Not Applicable" - assert isinstance(module.raw_data, dict) - assert isinstance(module.raw_response, dict) - assert "compatibilityStatusList" in module.raw_data - assert module.rep_status == "skipped" - assert module.serial_number == "FDO21120U5D" - assert module.status == "Skipped" - assert module.timestamp == "NA" - assert module.version == "10.2.5" - assert module.version_check == "Compatibility status skipped." - assert module.comp_disp == "Compatibility status skipped." - assert module.result.get("success") is True - - -def test_image_mgmt_install_options_00008(monkeypatch, module) -> None: - """ - Function - - refresh - - Setup - - Device has no policy attached - - POST REQUEST - - issu is True - - epld is True - - package_install is False - - Test - - 200 response from endpoint - - Response contains expected values - - Endpoint - - install-options - """ - - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00008a" - return responses_image_install_options(key) - - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - - module.policy_name = "KRM5" - module.serial_number = "FDO21120U5D" - module.epld = True - module.issu = True - module.package_install = False - module.refresh() - assert isinstance(module.response, dict) - assert module.device_name == "leaf1" - assert module.err_message is None - assert isinstance(module.epld_modules, dict) - assert len(module.epld_modules.get("moduleList")) == 2 - assert module.install_option == "NA" - assert module.install_packages is None - assert module.ip_address == "172.22.150.102" - assert module.os_type == "64bit" - assert module.platform == "N9K/N3K" - assert module.pre_issu_link == "Not Applicable" - assert isinstance(module.raw_data, dict) - assert isinstance(module.raw_response, dict) - assert "compatibilityStatusList" in module.raw_data - assert module.rep_status == "skipped" - assert module.serial_number == "FDO21120U5D" - assert module.status == "Skipped" - assert module.timestamp == "NA" - assert module.version == "10.2.5" - assert module.version_check == "Compatibility status skipped." - assert module.comp_disp == "Compatibility status skipped." - assert module.result.get("success") is True - - -def test_image_mgmt_install_options_00009(monkeypatch, module) -> None: - """ - Function - - refresh - - Setup - - Device has no policy attached - - POST REQUEST - - issu is False - - epld is True - - package_install is False - - Test - - 200 response from endpoint - - Response contains expected values - - Endpoint - - install-options - """ - - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00009a" - return responses_image_install_options(key) - - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - - module.policy_name = "KRM5" - module.serial_number = "FDO21120U5D" - module.epld = True - module.issu = False - module.package_install = False - module.refresh() - assert isinstance(module.response, dict) - assert module.device_name is None - assert module.err_message is None - assert isinstance(module.epld_modules, dict) - assert len(module.epld_modules.get("moduleList")) == 2 - assert module.install_option == None - assert module.install_packages is None - assert module.ip_address == None - assert module.os_type == None - assert module.platform == None - assert module.pre_issu_link == None - assert isinstance(module.raw_data, dict) - assert isinstance(module.raw_response, dict) - assert "compatibilityStatusList" in module.raw_data - assert module.rep_status == None - assert module.serial_number == "FDO21120U5D" - assert module.status == None - assert module.timestamp == None - assert module.version == None - assert module.version_check == None - assert module.comp_disp == None - assert module.result.get("success") is True - - -def test_image_mgmt_install_options_00010(monkeypatch, module) -> None: - """ - Function - - refresh - - Setup - - Device has no policy attached - - POST REQUEST - - issu is False - - epld is True - - package_install is True (causes expected error) - - Test - - 500 response from endpoint due to - - KR5M policy has no packages defined and - - package_install set to True - - Response contains expected values - - Endpoint - - install-options - """ - - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00010a" - return responses_image_install_options(key) - - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - - module.policy_name = "KRM5" - module.serial_number = "FDO21120U5D" - module.epld = True - module.issu = True - module.package_install = True - match = "Selected policy KR5M does not have package to continue." - with pytest.raises(AnsibleFailJson, match=match): - module.refresh() - - -def test_image_mgmt_install_options_00020(module) -> None: - """ - Function - - build_payload - - Setup - - Defaults are not specified by the user - - Test - - Default values for issu, epld, and package_install are applied - """ - module.policy_name = "KRM5" - module.serial_number = "BAR" - module._build_payload() - assert module.payload.get("devices")[0].get("policyName") == "KRM5" - assert module.payload.get("devices")[0].get("serialNumber") == "BAR" - assert module.payload.get("issu") is True - assert module.payload.get("epld") is False - assert module.payload.get("packageInstall") is False - - -def test_image_mgmt_install_options_00021(module) -> None: - """ - Function - - build_payload - - Setup - - Values are specified by the user - - Test - - Payload contains user-specified values if the user sets them - - Defaults for issu, epld, and package_install are overridden by user values. - """ - module.policy_name = "KRM5" - module.serial_number = "BAR" - module.issu = False - module.epld = True - module.package_install = True - module._build_payload() - assert module.payload.get("devices")[0].get("policyName") == "KRM5" - assert module.payload.get("devices")[0].get("serialNumber") == "BAR" - assert module.payload.get("issu") is False - assert module.payload.get("epld") is True - assert module.payload.get("packageInstall") is True - - -def test_image_mgmt_install_options_00022(module) -> None: - """ - Function - - issu setter - - Test - - fail_json is called if issu is not a boolean. - """ - match = "ImageInstallOptions.issu.setter: issu must be a " - match += "boolean value" - with pytest.raises(AnsibleFailJson, match=match): - module.issu = "FOO" - - -def test_image_mgmt_install_options_00023(module) -> None: - """ - Function - - epld setter - - Test - - fail_json is called if epld is not a boolean. - """ - match = "ImageInstallOptions.epld.setter: epld must be a " - match += "boolean value" - with pytest.raises(AnsibleFailJson, match=match): - module.epld = "FOO" - - -def test_image_mgmt_install_options_00024(module) -> None: - """ - Function - - package_install setter - - Test - - fail_json is called if package_install is not a boolean. - """ - match = "ImageInstallOptions.package_install.setter: " - match += "package_install must be a boolean value" - with pytest.raises(AnsibleFailJson, match=match): - module.package_install = "FOO" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py similarity index 99% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py index 8fdea2fa7..7e4031f1c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ApiEndpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py @@ -13,7 +13,7 @@ # limitations under the License. """ -Tests for ApiEndpoints class +ApiEndpoints - unit tests """ from __future__ import absolute_import, division, print_function diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py new file mode 100644 index 000000000..413875ef2 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py @@ -0,0 +1,627 @@ +# Copyright (c) 2020-2024 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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument + +""" +ControllerVersion - unit tests +""" +from __future__ import absolute_import, division, print_function + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson + +from .fixture import load_fixture +from .image_upgrade_utils import controller_version_fixture + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +""" +controller_version: 12 +description: Verify functionality of ControllerVersion +""" + + +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_COMMON = PATCH_MODULE_UTILS + "common." + +DCNM_SEND_VERSION = PATCH_COMMON + "controller_version.dcnm_send" + + +def responses_controller_version(key: str) -> Dict[str, str]: + """ + Return the response from ControllerVersion + """ + response_file = "image_upgrade_responses_ControllerVersion" + response = load_fixture(response_file).get(key) + print(f"responses_controller_version: {key} : {response}") + return response + + +def test_common_version_00001(controller_version) -> None: + """ + Function + - __init__ + + Test + - Class properties are initialized to expected values + """ + instance = controller_version + assert isinstance(instance.properties, dict) + assert instance.properties.get("response_data") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00002a", False), + ("test_common_version_00002b", True), + ("test_common_version_00002c", None), + ], +) +def test_common_version_00002(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - dev + + Test + - dev returns True when the controller is a development version + - dev returns False when the controller is not a development version + - dev returns None otherwise + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.dev == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00003a", "EASYFABRIC"), + ("test_common_version_00003b", None), + ], +) +def test_common_version_00003(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - install + + Test + - install returns expected values + + Description + install returns: + - Value of the "install" key in the controller response, if present + - None, if the "install" key is absent from the controller response + + Expected results: + + 1. test_common_version_00003a == "EASYFABRIC" + 2. test_common_version_00003b is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.install == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00004a", True), + ("test_common_version_00004b", False), + ("test_common_version_00004c", None), + ], +) +def test_common_version_00004(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - is_ha_enabled + + Test + - is_ha_enabled returns expected values + + Description + is_ha_enabled returns: + - True, if "isHaEnabled" key in the controller response == "true" + - False, if "isHaEnabled" key in the controller response == "false" + - None, if "isHaEnabled" key is absent from the controller response + + Expected results: + + 1. test_common_version_00004a is True + 2. test_common_version_00004b is False + 3. test_common_version_00004c is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.is_ha_enabled == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00005a", True), + ("test_common_version_00005b", False), + ("test_common_version_00005c", None), + ], +) +def test_common_version_00005(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - is_media_controller + + Test + - is_media_controller returns expected values + + Description + is_media_controller returns: + - True, if "isMediaController" key in the controller response == "true" + - False, if "isMediaController" key in the controller response == "false" + - None, if "isMediaController" key is absent from the controller response + + Expected results: + + 1. test_common_version_00005a is True + 2. test_common_version_00005b is False + 3. test_common_version_00005c is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.is_media_controller == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00006a", True), + ("test_common_version_00006b", False), + ("test_common_version_00006c", None), + ], +) +def test_common_version_00006(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - is_upgrade_inprogress + + Test + - is_upgrade_inprogress returns expected values + + Description + is_upgrade_inprogress returns: + - True, if "is_upgrade_inprogress" key in the controller response == "true" + - False, if "is_upgrade_inprogress" key in the controller response == "false" + - None, if "is_upgrade_inprogress" key is absent from the controller response + + Expected results: + + 1. test_common_version_00006a is True + 2. test_common_version_00006b is False + 3. test_common_version_00006c is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.is_upgrade_inprogress == expected + + +def test_common_version_00007(monkeypatch, controller_version) -> None: + """ + Function + - refresh + - response_data + + Test + - response_data returns the "DATA" key in the controller response + + Description + response_data returns the "DATA" key in the controller response, + which is a dictionary of key-value pairs. + fail_json is called if the "DATA" key is absent. + + Expected results: + + 1. test_common_version_00007a, ControllerVersion.response_data == type(dict) + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + key = "test_common_version_00007a" + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert isinstance(instance.response_data, dict) + + +def test_common_version_00008(monkeypatch, controller_version) -> None: + """ + Function + - refresh + + Test + - fail_json is called because the "DATA" key is absent + + Description + response_data returns the "DATA" key in the controller response, + which is a dictionary of key-value pairs. + fail_json is called if the "DATA" key is absent. + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + key = "test_common_version_00008a" + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + with pytest.raises(AnsibleFailJson): + instance.refresh() + + +def test_common_version_00009(monkeypatch, controller_version) -> None: + """ + Function + - refresh + - result + + Test + - result returns expected values + + Description + result returns the result of its superclass + method ImageUpgradeCommon._handle_response() + + Expected results: + + - Since a 200 response with "message" key == "OK" is received + we expect result to return {'found': True, 'success': True} + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + key = "test_common_version_00009a" + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.result == {"found": True, "success": True} + + +def test_common_version_00010(monkeypatch, controller_version) -> None: + """ + Function + - refresh + - result + + Test + - result returns expected values + + Description + result returns the result of its superclass + method ImageUpgradeCommon._handle_response() + + Expected results: + + - Since a 404 response with "message" key == "Not Found" is received + we expect result to return {'found': False, 'success': True} + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + key = "test_common_version_00010a" + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + with pytest.raises(AnsibleFailJson): + instance.refresh() + assert instance.result == {"found": False, "success": True} + + +def test_common_version_00011(monkeypatch, controller_version) -> None: + """ + Function + - refresh + - result + + Test + - result returns expected values + - fail_json is called + + Description + result returns the result of its superclass + method ImageUpgradeCommon._handle_response() + + Expected results: + + - Since a 500 response is received (MESSAGE key ignored) + we expect result to return {'found': False, 'success': False} + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + key = "test_common_version_00011a" + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + with pytest.raises(AnsibleFailJson): + instance.refresh() + assert instance.result == {"found": False, "success": False} + + +@pytest.mark.parametrize( + "key, expected", + [("test_common_version_00012a", "LAN"), ("test_common_version_00012b", None)], +) +def test_common_version_00012(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - mode + + Test + - mode returns expected values + + Description + mode returns: + - its value, if the "mode" key is present in the controller response + - None, if the "mode" key is absent from the controller response + + Expected results: + + 1. test_common_version_00012a == "LAN" + 2. test_common_version_00012b is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.mode == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00013a", "foo-uuid"), + ("test_common_version_00013b", None), + ], +) +def test_common_version_00013(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - uuid + + Test + - uuid returns expected values + + Description + uuid returns: + - its value, if the "uuid" key is present in the controller response + - None, if the "uuid" key is absent from the controller response + + Expected results: + + 1. test_common_version_00013a == "foo-uuid" + 2. test_common_version_00013b is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.uuid == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00014a", "12.1.3b"), + ("test_common_version_00014b", None), + ], +) +def test_common_version_00014(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - version + + Test + - version returns expected values + + Description + mode returns: + - its value, if the "version" key is present in the controller response + - None, if the "version" key is absent from the controller response + + Expected results: + + 1. test_common_version_00014a == "12.1.3b" + 2. test_common_version_00014b is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.version == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00015a", "12"), + ("test_common_version_00015b", None), + ], +) +def test_common_version_00015(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - version_major + + Test + - version_major returns expected values + + Description + version_major returns the major version of the controller + It derives this from the "version" key in the controller response + by splitting the string on "." and returning the first element + + Expected results: + + 1. test_common_version_00015a == "12" + 2. test_common_version_00015b is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.version_major == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00016a", "1"), + ("test_common_version_00016b", None), + ], +) +def test_common_version_00016(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - version_minor + + Test + - version_minor returns expected values + + Description + version_minor returns the minor version of the controller + It derives this from the "version" key in the controller response + by splitting the string on "." and returning the second element + + Expected results: + + 1. test_common_version_00016a == "1" + 2. test_common_version_00016b is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.version_minor == expected + + +@pytest.mark.parametrize( + "key, expected", + [ + ("test_common_version_00017a", "3b"), + ("test_common_version_00017b", None), + ], +) +def test_common_version_00017(monkeypatch, controller_version, key, expected) -> None: + """ + Function + - refresh + - version_patch + + Test + - version_patch returns expected values + + Description + version_patch returns the patch version of the controller + It derives this from the "version" key in the controller response + by splitting the string on "." and returning the third element + + Expected results: + + 1. test_common_version_00017a == "3b" + 2. test_common_version_00017b is None + """ + + def mock_dcnm_send_version(*args) -> Dict[str, Any]: + return responses_controller_version(key) + + monkeypatch.setattr(DCNM_SEND_VERSION, mock_dcnm_send_version) + + instance = controller_version + instance.refresh() + assert instance.version_patch == expected diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py new file mode 100644 index 000000000..f0edddf83 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -0,0 +1,501 @@ +# Copyright (c) 2020-2024 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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +""" +ImageInstallOptions - unit tests +""" + +from __future__ import absolute_import, division, print_function + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints + +from .fixture import load_fixture +from .image_upgrade_utils import MockAnsibleModule, does_not_raise, image_install_options_fixture + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" + + +def responses_image_install_options(key: str) -> Dict[str, str]: + """ + Return the response from ImageInstallOptions + """ + response_file = "image_upgrade_responses_ImageInstallOptions" + response = load_fixture(response_file).get(key) + print(f"{key} : : {response}") + return response + + +def test_image_mgmt_install_options_00001(image_install_options) -> None: + """ + Function + - __init__ + + Test + - fail_json is not called + - Class attributes are initialized to expected values + """ + with does_not_raise(): + instance = image_install_options + assert instance.module == MockAnsibleModule + assert instance.class_name == "ImageInstallOptions" + assert isinstance(instance.endpoints, ApiEndpoints) + + +def test_image_mgmt_install_options_00002(image_install_options) -> None: + """ + Function + - _init_properties + + Test + - Class properties are initialized to expected values + """ + with does_not_raise(): + instance = image_install_options + assert isinstance(instance.properties, dict) + assert instance.properties.get("epld") is False + assert instance.properties.get("epld_modules") is None + assert instance.properties.get("issu") is True + assert instance.properties.get("package_install") is False + assert instance.properties.get("policy_name") is None + assert instance.properties.get("response") is None + assert instance.properties.get("response_data") is None + assert instance.properties.get("result") is None + assert instance.properties.get("serial_number") is None + + +def test_image_mgmt_install_options_00003(image_install_options) -> None: + """ + Function + - refresh + - serial_number setter + + Test + - fail_json is called because policy_name is not set when refresh is called + - fail_json error message is matched + """ + instance = image_install_options + instance.serial_number = "FOO" + match = "ImageInstallOptions.refresh: " + match += "instance.policy_name must be set before " + match += r"calling refresh\(\)" + with pytest.raises(AnsibleFailJson, match=match): + instance.refresh() + + +def test_image_mgmt_install_options_00004(image_install_options) -> None: + """ + Function + - refresh + + Test + - fail_json is called because serial_number is not set when refresh is called + - fail_json error message is matched + """ + match = "ImageInstallOptions.refresh: " + match += "instance.serial_number must be set before " + match += r"calling refresh\(\)" + + instance = image_install_options + instance.policy_name = "FOO" + with pytest.raises(AnsibleFailJson, match=match): + image_install_options.refresh() + + +def test_image_mgmt_install_options_00005(monkeypatch, image_install_options) -> None: + """ + Function + - refresh + + Test + - 200 response from endpoint + - Properties are updated with expected values + - endpoint: install-options + """ + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00005a" + return responses_image_install_options(key) + + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "BAR" + instance.refresh() + assert isinstance(instance.response, dict) + assert instance.device_name == "cvd-1314-leaf" + assert instance.err_message is None + assert instance.epld_modules is None + assert instance.install_option == "disruptive" + assert instance.install_packages is None + assert instance.ip_address == "172.22.150.105" + assert instance.os_type == "64bit" + assert instance.platform == "N9K/N3K" + assert instance.pre_issu_link == "Not Applicable" + assert isinstance(instance.raw_data, dict) + assert isinstance(instance.raw_response, dict) + assert "compatibilityStatusList" in instance.raw_data + assert instance.rep_status == "skipped" + assert instance.serial_number == "BAR" + assert instance.status == "Success" + assert instance.timestamp == "NA" + assert instance.version == "10.2.5" + comp_disp = "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin" + assert instance.comp_disp == comp_disp + assert instance.result.get("success") is True + + +def test_image_mgmt_install_options_00006(monkeypatch, image_install_options) -> None: + """ + Function + - refresh + + Test + - fail_json is called because RETURN_CODE != 200 in the response + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00006a" + return responses_image_install_options(key) + + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + + match = "ImageInstallOptions.refresh: " + match += "Bad result when retrieving install-options from " + match += "the controller. Controller response:" + + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "BAR" + with pytest.raises(AnsibleFailJson, match=rf"{match}"): + instance.refresh() + + +def test_image_mgmt_install_options_00007(monkeypatch, image_install_options) -> None: + """ + Function + - refresh + + Setup + - Device has no policy attached + - POST REQUEST + - issu is True + - epld is False + - package_install is False + + Test + - 200 response from endpoint + - Response contains expected values + + Endpoint + - install-options + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00007a" + return responses_image_install_options(key) + + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "FDO21120U5D" + instance.refresh() + assert isinstance(instance.response, dict) + assert instance.device_name == "leaf1" + assert instance.err_message is None + assert instance.epld_modules is None + assert instance.install_option == "NA" + assert instance.install_packages is None + assert instance.ip_address == "172.22.150.102" + assert instance.os_type == "64bit" + assert instance.platform == "N9K/N3K" + assert instance.pre_issu_link == "Not Applicable" + assert isinstance(instance.raw_data, dict) + assert isinstance(instance.raw_response, dict) + assert "compatibilityStatusList" in image_install_options.raw_data + assert instance.rep_status == "skipped" + assert instance.serial_number == "FDO21120U5D" + assert instance.status == "Skipped" + assert instance.timestamp == "NA" + assert instance.version == "10.2.5" + assert instance.version_check == "Compatibility status skipped." + assert instance.comp_disp == "Compatibility status skipped." + assert instance.result.get("success") is True + + +def test_image_mgmt_install_options_00008(monkeypatch, image_install_options) -> None: + """ + Function + - refresh + + Setup + - Device has no policy attached + - POST REQUEST + - issu is True + - epld is True + - package_install is False + + Test + - 200 response from endpoint + - Response contains expected values + + Endpoint + - install-options + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00008a" + return responses_image_install_options(key) + + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "FDO21120U5D" + instance.epld = True + instance.issu = True + instance.package_install = False + instance.refresh() + assert isinstance(instance.response, dict) + assert instance.device_name == "leaf1" + assert instance.err_message is None + assert isinstance(instance.epld_modules, dict) + assert len(instance.epld_modules.get("moduleList")) == 2 + assert instance.install_option == "NA" + assert instance.install_packages is None + assert instance.ip_address == "172.22.150.102" + assert instance.os_type == "64bit" + assert instance.platform == "N9K/N3K" + assert instance.pre_issu_link == "Not Applicable" + assert isinstance(instance.raw_data, dict) + assert isinstance(instance.raw_response, dict) + assert "compatibilityStatusList" in instance.raw_data + assert instance.rep_status == "skipped" + assert instance.serial_number == "FDO21120U5D" + assert instance.status == "Skipped" + assert instance.timestamp == "NA" + assert instance.version == "10.2.5" + assert instance.version_check == "Compatibility status skipped." + assert instance.comp_disp == "Compatibility status skipped." + assert instance.result.get("success") is True + + +def test_image_mgmt_install_options_00009(monkeypatch, image_install_options) -> None: + """ + Function + - refresh + + Setup + - Device has no policy attached + - POST REQUEST + - issu is False + - epld is True + - package_install is False + + Test + - 200 response from endpoint + - Response contains expected values + + Endpoint + - install-options + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00009a" + return responses_image_install_options(key) + + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "FDO21120U5D" + instance.epld = True + instance.issu = False + instance.package_install = False + instance.refresh() + assert isinstance(instance.response, dict) + assert instance.device_name is None + assert instance.err_message is None + assert isinstance(instance.epld_modules, dict) + assert len(instance.epld_modules.get("moduleList")) == 2 + assert instance.install_option is None + assert instance.install_packages is None + assert instance.ip_address is None + assert instance.os_type is None + assert instance.platform is None + assert instance.pre_issu_link is None + assert isinstance(instance.raw_data, dict) + assert isinstance(instance.raw_response, dict) + assert "compatibilityStatusList" in instance.raw_data + assert instance.rep_status is None + assert instance.serial_number == "FDO21120U5D" + assert instance.status is None + assert instance.timestamp is None + assert instance.version is None + assert instance.version_check is None + assert instance.comp_disp is None + assert instance.result.get("success") is True + + +def test_image_mgmt_install_options_00010(monkeypatch, image_install_options) -> None: + """ + Function + - refresh + + Setup + - Device has no policy attached + - POST REQUEST + - issu is False + - epld is True + - package_install is True (causes expected error) + + Test + - 500 response from endpoint due to + - KR5M policy has no packages defined and + - package_install set to True + - Response contains expected values + + Endpoint + - install-options + """ + + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_install_options_00010a" + return responses_image_install_options(key) + + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + + match = "Selected policy KR5M does not have package to continue." + + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "FDO21120U5D" + instance.epld = True + instance.issu = True + instance.package_install = True + with pytest.raises(AnsibleFailJson, match=match): + instance.refresh() + + +def test_image_mgmt_install_options_00020(image_install_options) -> None: + """ + Function + - build_payload + + Setup + - Defaults are not specified by the user + + Test + - Default values for issu, epld, and package_install are applied + """ + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "BAR" + instance._build_payload() # pylint: disable=protected-access + assert instance.payload.get("devices")[0].get("policyName") == "KRM5" + assert instance.payload.get("devices")[0].get("serialNumber") == "BAR" + assert instance.payload.get("issu") is True + assert instance.payload.get("epld") is False + assert instance.payload.get("packageInstall") is False + + +def test_image_mgmt_install_options_00021(image_install_options) -> None: + """ + Function + - build_payload + + Setup + - Values are specified by the user + + Test + - Payload contains user-specified values if the user sets them + - Defaults for issu, epld, and package_install are overridden by user values. + """ + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "BAR" + instance.issu = False + instance.epld = True + instance.package_install = True + instance._build_payload() # pylint: disable=protected-access + assert instance.payload.get("devices")[0].get("policyName") == "KRM5" + assert instance.payload.get("devices")[0].get("serialNumber") == "BAR" + assert instance.payload.get("issu") is False + assert instance.payload.get("epld") is True + assert instance.payload.get("packageInstall") is True + + +def test_image_mgmt_install_options_00022(image_install_options) -> None: + """ + Function + - issu setter + + Test + - fail_json is called if issu is not a boolean. + """ + match = "ImageInstallOptions.issu.setter: issu must be a " + match += "boolean value" + + instance = image_install_options + with pytest.raises(AnsibleFailJson, match=match): + instance.issu = "FOO" + + +def test_image_mgmt_install_options_00023(image_install_options) -> None: + """ + Function + - epld setter + + Test + - fail_json is called if epld is not a boolean. + """ + match = "ImageInstallOptions.epld.setter: epld must be a " + match += "boolean value" + + instance = image_install_options + with pytest.raises(AnsibleFailJson, match=match): + instance.epld = "FOO" + + +def test_image_mgmt_install_options_00024(image_install_options) -> None: + """ + Function + - package_install setter + + Test + - fail_json is called if package_install is not a boolean. + """ + match = "ImageInstallOptions.package_install.setter: " + match += "package_install must be a boolean value" + + instance = image_install_options + with pytest.raises(AnsibleFailJson, match=match): + instance.package_install = "FOO" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py similarity index 64% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index d6cb7b0a3..c8f11e90c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -12,6 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument + +""" +ImagePolicies - unit tests +""" + from __future__ import absolute_import, division, print_function from typing import Any, Dict @@ -21,10 +32,9 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ - ImagePolicies from .fixture import load_fixture +from .image_upgrade_utils import MockAnsibleModule, does_not_raise, image_policies_fixture __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -34,38 +44,22 @@ description: Verify functionality of class ImagePolicies """ -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -dcnm_send_image_policies = patch_image_mgmt + "image_policies.dcnm_send" +DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" -class MockAnsibleModule: +def responses_image_policies(key: str) -> Dict[str, str]: """ - Mock the AnsibleModule class + Return the response from ImagePolicies """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - -def responses_image_policies(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ImagePolicies" + response_file = "image_upgrade_responses_ImagePolicies" response = load_fixture(response_file).get(key) print(f"responses_image_policies: {key} : {response}") return response -@pytest.fixture -def image_policies(): - return ImagePolicies(MockAnsibleModule) - - def test_image_mgmt_image_policies_00001(image_policies) -> None: """ Function @@ -74,10 +68,11 @@ def test_image_mgmt_image_policies_00001(image_policies) -> None: Test - Class attributes are initialized to expected values """ - image_policies.__init__(MockAnsibleModule) - assert image_policies.module == MockAnsibleModule - assert image_policies.class_name == "ImagePolicies" - assert isinstance(image_policies.endpoints, ApiEndpoints) + with does_not_raise(): + instance = image_policies + assert instance.module == MockAnsibleModule + assert instance.class_name == "ImagePolicies" + assert isinstance(instance.endpoints, ApiEndpoints) def test_image_mgmt_image_policies_00002(image_policies) -> None: @@ -88,12 +83,13 @@ def test_image_mgmt_image_policies_00002(image_policies) -> None: Test - Class properties are initialized to expected values """ - image_policies._init_properties() + with does_not_raise(): + instance = image_policies assert isinstance(image_policies.properties, dict) - assert image_policies.properties.get("policy_name") == None - assert image_policies.properties.get("response_data") == None - assert image_policies.properties.get("response") == None - assert image_policies.properties.get("result") == None + assert instance.properties.get("policy_name") is None + assert instance.properties.get("response_data") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None def test_image_mgmt_image_policies_00010(monkeypatch, image_policies) -> None: @@ -104,32 +100,35 @@ def test_image_mgmt_image_policies_00010(monkeypatch, image_policies) -> None: Test - properties are initialized to expected values - 200 RETURN_CODE + - fail_json is not called Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ key = "test_image_mgmt_image_policies_00010a" - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) - - image_policies.refresh() - image_policies.policy_name = "KR5M" - assert isinstance(image_policies.response, dict) - assert image_policies.agnostic is False - assert image_policies.description == "10.2.(5) with EPLD" - assert image_policies.epld_image_name == "n9000-epld.10.2.5.M.img" - assert image_policies.image_name == "nxos64-cs.10.2.5.M.bin" - assert image_policies.nxos_version == "10.2.5_nxos64-cs_64bit" - assert image_policies.package_name == None - assert image_policies.platform == "N9K/N3K" - assert image_policies.platform_policies == None - assert image_policies.policy_name == "KR5M" - assert image_policies.policy_type == "PLATFORM" - assert image_policies.ref_count == 10 - assert image_policies.rpm_images == None + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + + instance = image_policies + with does_not_raise(): + instance.refresh() + instance.policy_name = "KR5M" + assert isinstance(instance.response, dict) + assert instance.agnostic is False + assert instance.description == "10.2.(5) with EPLD" + assert instance.epld_image_name == "n9000-epld.10.2.5.M.img" + assert instance.image_name == "nxos64-cs.10.2.5.M.bin" + assert instance.nxos_version == "10.2.5_nxos64-cs_64bit" + assert instance.package_name is None + assert instance.platform == "N9K/N3K" + assert instance.platform_policies is None + assert instance.policy_name == "KR5M" + assert instance.policy_type == "PLATFORM" + assert instance.ref_count == 10 + assert instance.rpm_images is None def test_image_mgmt_image_policies_00020(monkeypatch, image_policies) -> None: @@ -145,16 +144,18 @@ def test_image_mgmt_image_policies_00020(monkeypatch, image_policies) -> None: """ key = "test_image_mgmt_image_policies_00020a" - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") return responses_image_policies(key) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) - image_policies.refresh() - assert isinstance(image_policies.result, dict) - assert image_policies.result.get("found") is True - assert image_policies.result.get("success") is True + instance = image_policies + with does_not_raise(): + instance.refresh() + assert isinstance(instance.result, dict) + assert instance.result.get("found") is True + assert instance.result.get("success") is True def test_image_mgmt_image_policies_00021(monkeypatch, image_policies) -> None: @@ -170,16 +171,18 @@ def test_image_mgmt_image_policies_00021(monkeypatch, image_policies) -> None: """ key = "test_image_mgmt_image_policies_00021a" - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") return responses_image_policies(key) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) match = "ImagePolicies.refresh: Bad response when retrieving " match += "image policy information from the controller." + + instance = image_policies with pytest.raises(AnsibleFailJson, match=match): - image_policies.refresh() + instance.refresh() def test_image_mgmt_image_policies_00022(monkeypatch, image_policies) -> None: @@ -195,16 +198,18 @@ def test_image_mgmt_image_policies_00022(monkeypatch, image_policies) -> None: """ key = "test_image_mgmt_image_policies_00022a" - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") return responses_image_policies(key) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) match = "ImagePolicies.refresh: Bad response when retrieving " match += "image policy information from the controller." + + instance = image_policies with pytest.raises(AnsibleFailJson, match=match): - image_policies.refresh() + instance.refresh() def test_image_mgmt_image_policies_00023(monkeypatch, image_policies) -> None: @@ -221,16 +226,18 @@ def test_image_mgmt_image_policies_00023(monkeypatch, image_policies) -> None: """ key = "test_image_mgmt_image_policies_00023a" - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") return responses_image_policies(key) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) match = "ImagePolicies.refresh: " match += "the controller has no defined image policies." + + instance = image_policies with pytest.raises(AnsibleFailJson, match=match): - image_policies.refresh() + instance.refresh() def test_image_mgmt_image_policies_00024(monkeypatch, image_policies) -> None: @@ -240,26 +247,29 @@ def test_image_mgmt_image_policies_00024(monkeypatch, image_policies) -> None: Test - fail_json() is called if response does not contain policy_name. - - i.e. image policy with name FOO has not yet been created on NDFC. + - i.e. image policy with name FOO has not yet been created on the controller. Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ key = "test_image_mgmt_image_policies_00024a" - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") return responses_image_policies(key) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) image_policies.refresh() image_policies.policy_name = "FOO" match = "ImagePolicies._get: " match += "policy_name FOO is not defined on the controller." + + instance = image_policies with pytest.raises(AnsibleFailJson, match=match): - image_policies.policy_type == "PLATFORM" + if instance.policy_type == "PLATFORM": + pass def test_image_mgmt_image_policies_00025(monkeypatch, image_policies) -> None: @@ -269,7 +279,6 @@ def test_image_mgmt_image_policies_00025(monkeypatch, image_policies) -> None: Test - fail_json is called on response with missing policyName key. - - 200 RETURN_CODE Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies @@ -277,22 +286,22 @@ def test_image_mgmt_image_policies_00025(monkeypatch, image_policies) -> None: NOTES - This is to cover a check in ImagePolicies.refresh() - This scenario should never happen. - - TODO - Consider removing this check, and this testcase. """ key = "test_image_mgmt_image_policies_00025a" - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") return responses_image_policies(key) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) match = "ImagePolicies.refresh: " match += "Cannot parse policy information from the controller." + + instance = image_policies with pytest.raises(AnsibleFailJson, match=match): - image_policies.refresh() + instance.refresh() def test_image_mgmt_image_policies_00040(image_policies) -> None: @@ -305,5 +314,7 @@ def test_image_mgmt_image_policies_00040(image_policies) -> None: """ match = "ImagePolicies._get: instance.policy_name must be " match += "set before accessing property imageName." + + instance = image_policies with pytest.raises(AnsibleFailJson, match=match): - image_policies._get("imageName") + instance._get("imageName") # pylint: disable=protected-access diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py similarity index 56% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 9915f2d8e..97b81305f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImagePolicyAction.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -11,10 +11,18 @@ # 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. +""" +ImagePolicyAction - unit tests +""" +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument from __future__ import absolute_import, division, print_function -from contextlib import contextmanager from typing import Any, Dict import pytest @@ -22,14 +30,15 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ - ImagePolicies from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ ImagePolicyAction from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture +from .image_upgrade_utils import ( + does_not_raise, image_policies_fixture, image_policy_action_fixture, + issu_details_by_serial_number_fixture) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -39,81 +48,55 @@ Description: Verify functionality of ImagePolicyAction """ +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -@contextmanager -def does_not_raise(): - """ - A context manager that does not raise an exception. - """ - yield - - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." - -dcnm_send_image_policies = patch_image_mgmt + "image_policies.dcnm_send" -dcnm_send_image_policy_action = patch_image_mgmt + "image_policy_action.dcnm_send" -dcnm_send_switch_details = patch_image_mgmt + "switch_details.dcnm_send" -dcnm_send_switch_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" +DCNM_SEND_IMAGE_POLICY_ACTION = PATCH_IMAGE_MGMT + "image_policy_action.dcnm_send" +DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.dcnm_send" +DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" def responses_image_policies(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ImagePolicies" + """ + Return ImagePolicies controller responses + """ + response_file = "image_upgrade_responses_ImagePolicies" response = load_fixture(response_file).get(key) print(f"responses_image_policies: {key} : {response}") return response def responses_image_policy_action(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ImagePolicyAction" + """ + Return ImagePolicyAction controller responses + """ + response_file = "image_upgrade_responses_ImagePolicyAction" response = load_fixture(response_file).get(key) print(f"responses_image_policy_action: {key} : {response}") return response def responses_switch_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchDetails" + """ + Return SwitchDetails controller responses + """ + response_file = "image_upgrade_responses_SwitchDetails" response = load_fixture(response_file).get(key) print(f"responses_switch_details: {key} : {response}") return response def responses_switch_issu_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchIssuDetails" + """ + Return SwitchIssuDetails controller responses + """ + response_file = "image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) print(f"responses_switch_issu_details: {key} : {response}") return response -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - -@pytest.fixture -def image_policy_action(): - return ImagePolicyAction(MockAnsibleModule) - - -@pytest.fixture -def issu_details() -> SwitchIssuDetailsBySerialNumber: - return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) - - -@pytest.fixture -def image_policies() -> ImagePolicies: - return ImagePolicies(MockAnsibleModule) - - def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: """ Function @@ -124,17 +107,15 @@ def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: - fail_json is not called """ with does_not_raise(): - image_policy_action.__init__(MockAnsibleModule) - assert image_policy_action.class_name == "ImagePolicyAction" - assert isinstance(image_policy_action.endpoints, ApiEndpoints) - assert isinstance(image_policy_action, ImagePolicyAction) - assert isinstance( - image_policy_action.switch_issu_details, SwitchIssuDetailsBySerialNumber - ) - assert image_policy_action.path == None - assert image_policy_action.payloads == [] - assert image_policy_action.valid_actions == {"attach", "detach", "query"} - assert image_policy_action.verb == None + instance = image_policy_action + assert instance.class_name == "ImagePolicyAction" + assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance, ImagePolicyAction) + assert isinstance(instance.switch_issu_details, SwitchIssuDetailsBySerialNumber) + assert instance.path is None + assert instance.payloads == [] + assert instance.valid_actions == {"attach", "detach", "query"} + assert instance.verb is None def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: @@ -145,18 +126,18 @@ def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: Test - Class properties are initialized to expected values """ - image_policy_action._init_properties() - assert isinstance(image_policy_action.properties, dict) - assert image_policy_action.properties.get("action") == None - assert image_policy_action.properties.get("response") == None - assert image_policy_action.properties.get("result") == None - assert image_policy_action.properties.get("policy_name") == None - assert image_policy_action.properties.get("query_result") == None - assert image_policy_action.properties.get("serial_numbers") == None + instance = image_policy_action + assert isinstance(instance.properties, dict) + assert instance.properties.get("action") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None + assert instance.properties.get("policy_name") is None + assert instance.properties.get("query_result") is None + assert instance.properties.get("serial_numbers") is None def test_image_mgmt_image_policy_action_00003( - monkeypatch, image_policy_action, issu_details + monkeypatch, image_policy_action, issu_details_by_serial_number ) -> None: """ Function @@ -172,17 +153,18 @@ def test_image_mgmt_image_policy_action_00003( to attach policies to devices """ - def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_image_policy_action_00003a" return responses_switch_issu_details(key) monkeypatch.setattr( - dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) - image_policy_action.switch_issu_details = issu_details - image_policy_action.policy_name = "KR5M" - image_policy_action.serial_numbers = [ + instance = image_policy_action + instance.switch_issu_details = issu_details_by_serial_number + instance.policy_name = "KR5M" + instance.serial_numbers = [ "FDO2112189M", "FDO211218AX", "FDO211218B5", @@ -190,20 +172,20 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: "FDO211218GC", ] with does_not_raise(): - image_policy_action.build_payload() - assert isinstance(image_policy_action.payloads, list) - assert len(image_policy_action.payloads) == 5 + instance.build_payload() + assert isinstance(instance.payloads, list) + assert len(instance.payloads) == 5 def test_image_mgmt_image_policy_action_00004( - monkeypatch, image_policy_action, issu_details + monkeypatch, image_policy_action, issu_details_by_serial_number ) -> None: """ Function - build_payload Test - - fail_json is called since deviceName is null in the issu_details response + - fail_json is called since deviceName is null in the issu_details_by_serial_number response - The error message is matched Description @@ -212,17 +194,18 @@ def test_image_mgmt_image_policy_action_00004( of None, the function calls fail_json. """ - def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_image_policy_action_00004a" return responses_switch_issu_details(key) monkeypatch.setattr( - dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) - image_policy_action.switch_issu_details = issu_details - image_policy_action.policy_name = "KR5M" - image_policy_action.serial_numbers = [ + instance = image_policy_action + instance.switch_issu_details = issu_details_by_serial_number + instance.policy_name = "KR5M" + instance.serial_numbers = [ "FDO2112189M", ] match = "Unable to determine hostName for switch " @@ -230,11 +213,11 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "Please verify that the switch is managed by " match += "the controller." with pytest.raises(AnsibleFailJson, match=match): - image_policy_action.build_payload() + instance.build_payload() def test_image_mgmt_image_policy_action_00010( - image_policy_action, issu_details + image_policy_action, issu_details_by_serial_number ) -> None: """ Function @@ -249,32 +232,32 @@ def test_image_mgmt_image_policy_action_00010( If any of these validations fail, the function calls fail_json with a validation-specific error message. """ - - image_policy_action.switch_issu_details = issu_details - image_policy_action.policy_name = "KR5M" - image_policy_action.serial_numbers = [ + instance = image_policy_action + instance.switch_issu_details = issu_details_by_serial_number + instance.policy_name = "KR5M" + instance.serial_numbers = [ "FDO2112189M", ] match = "ImagePolicyAction.validate_request: " match += "instance.action must be set before calling commit()" with pytest.raises(AnsibleFailJson, match=match): - image_policy_action.validate_request() + instance.validate_request() -match_00011 = "ImagePolicyAction.validate_request: " -match_00011 += "instance.policy_name must be set before calling commit()" +MATCH_00011 = "ImagePolicyAction.validate_request: " +MATCH_00011 += "instance.policy_name must be set before calling commit()" @pytest.mark.parametrize( "action,expected", [ - ("attach", pytest.raises(AnsibleFailJson, match=match_00011)), - ("detach", pytest.raises(AnsibleFailJson, match=match_00011)), - ("query", pytest.raises(AnsibleFailJson, match=match_00011)), + ("attach", pytest.raises(AnsibleFailJson, match=MATCH_00011)), + ("detach", pytest.raises(AnsibleFailJson, match=MATCH_00011)), + ("query", pytest.raises(AnsibleFailJson, match=MATCH_00011)), ], ) def test_image_mgmt_image_policy_action_00011( - action, expected, image_policy_action, issu_details + action, expected, image_policy_action, issu_details_by_serial_number ) -> None: """ Function @@ -289,31 +272,31 @@ def test_image_mgmt_image_policy_action_00011( If any of these validations fail, the function calls fail_json with a validation-specific error message. """ - - image_policy_action.switch_issu_details = issu_details - image_policy_action.action = action - image_policy_action.serial_numbers = [ + instance = image_policy_action + instance.switch_issu_details = issu_details_by_serial_number + instance.action = action + instance.serial_numbers = [ "FDO2112189M", ] with expected: - image_policy_action.validate_request() + instance.validate_request() -match_00012 = "ImagePolicyAction.validate_request: " -match_00012 += "instance.serial_numbers must be set before calling commit()" +MATCH_00012 = "ImagePolicyAction.validate_request: " +MATCH_00012 += "instance.serial_numbers must be set before calling commit()" @pytest.mark.parametrize( "action,expected", [ - ("attach", pytest.raises(AnsibleFailJson, match=match_00012)), - ("detach", pytest.raises(AnsibleFailJson, match=match_00012)), + ("attach", pytest.raises(AnsibleFailJson, match=MATCH_00012)), + ("detach", pytest.raises(AnsibleFailJson, match=MATCH_00012)), ("query", does_not_raise()), ], ) def test_image_mgmt_image_policy_action_00012( - action, expected, image_policy_action, issu_details + action, expected, image_policy_action, issu_details_by_serial_number ) -> None: """ Function @@ -333,17 +316,17 @@ def test_image_mgmt_image_policy_action_00012( If any of these validations fail, the function calls fail_json with a validation-specific error message. """ - - image_policy_action.switch_issu_details = issu_details - image_policy_action.action = action - image_policy_action.policy_name = "KR5M" + instance = image_policy_action + instance.switch_issu_details = issu_details_by_serial_number + instance.action = action + instance.policy_name = "KR5M" with expected: - image_policy_action.validate_request() + instance.validate_request() def test_image_mgmt_image_policy_action_00013( - monkeypatch, image_policy_action, issu_details, image_policies + monkeypatch, image_policy_action, issu_details_by_serial_number, image_policies ) -> None: """ Function @@ -363,29 +346,30 @@ def test_image_mgmt_image_policy_action_00013( """ key = "test_image_mgmt_image_policy_action_00013a" - def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) monkeypatch.setattr( - dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) - image_policy_action.switch_issu_details = issu_details - image_policy_action.image_policies = image_policies - image_policy_action.action = "attach" - image_policy_action.policy_name = "KR5M" - image_policy_action.serial_numbers = ["FDO2112189M"] + instance = image_policy_action + instance.switch_issu_details = issu_details_by_serial_number + instance.image_policies = image_policies + instance.action = "attach" + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] match = "ImagePolicyAction.validate_request: " match += "policy KR5M does not support platform TEST_UNKNOWN_PLATFORM. " match += r"KR5M supports the following platform\(s\): N9K/N3K" with pytest.raises(AnsibleFailJson, match=match): - image_policy_action.validate_request() + instance.validate_request() def test_image_mgmt_image_policy_action_00020(monkeypatch, image_policy_action) -> None: @@ -415,33 +399,32 @@ def test_image_mgmt_image_policy_action_00020(monkeypatch, image_policy_action) """ key = "test_image_mgmt_image_policy_action_00020a" - def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) - def mock_validate_request(*args, **kwargs) -> None: + def mock_validate_request(*args) -> None: pass monkeypatch.setattr( - dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) - monkeypatch.setattr(image_policy_action, "validate_request", mock_validate_request) - monkeypatch.setattr( - image_policy_action, "valid_actions", {"attach", "detach", "query", "FOO"} - ) + instance = image_policy_action + monkeypatch.setattr(instance, "validate_request", mock_validate_request) + monkeypatch.setattr(instance, "valid_actions", {"attach", "detach", "query", "FOO"}) - image_policy_action.policy_name = "KR5M" - image_policy_action.serial_numbers = ["FDO2112189M"] - image_policy_action.action = "FOO" + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] + instance.action = "FOO" match = "ImagePolicyAction.commit: Unknown action FOO." with pytest.raises(AnsibleFailJson, match=match): - image_policy_action.commit() + instance.commit() def test_image_mgmt_image_policy_action_00021(monkeypatch, image_policy_action) -> None: @@ -464,46 +447,46 @@ def test_image_mgmt_image_policy_action_00021(monkeypatch, image_policy_action) """ key = "test_image_mgmt_image_policy_action_00021a" - def mock_dcnm_send_image_policies(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) - def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_switch_details(*args) -> Dict[str, Any]: return responses_switch_details(key) - def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_policy_action(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_policy_action(*args) -> Dict[str, Any]: return responses_image_policy_action(key) - monkeypatch.setattr(dcnm_send_image_policies, mock_dcnm_send_image_policies) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) monkeypatch.setattr( - dcnm_send_image_policy_action, mock_dcnm_send_image_policy_action + DCNM_SEND_IMAGE_POLICY_ACTION, mock_dcnm_send_image_policy_action ) - monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) monkeypatch.setattr( - dcnm_send_switch_issu_details, mock_dcnm_send_switch_issu_details + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) - image_policy_action.policy_name = "KR5M" - image_policy_action.serial_numbers = ["FDO2112189M"] - image_policy_action.action = "detach" + instance = image_policy_action + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] + instance.action = "detach" - image_policy_action.commit() - assert isinstance(image_policy_action.response, dict) - assert image_policy_action.response.get("RETURN_CODE") == 200 - assert image_policy_action.response.get("METHOD") == "DELETE" - assert image_policy_action.response.get("MESSAGE") == "OK" + instance.commit() + assert isinstance(instance.response, dict) + assert instance.response.get("RETURN_CODE") == 200 + assert instance.response.get("METHOD") == "DELETE" + assert instance.response.get("MESSAGE") == "OK" assert ( - image_policy_action.response.get("DATA") - == "Successfully detach the policy from device." + instance.response.get("DATA") == "Successfully detach the policy from device." ) - assert image_policy_action.result.get("success") is True - assert image_policy_action.result.get("changed") is True + assert instance.result.get("success") is True + assert instance.result.get("changed") is True -match_00060 = "ImagePolicyAction.action: instance.action must be " -match_00060 += "one of attach,detach,query. Got FOO." +MATCH_00060 = "ImagePolicyAction.action: instance.action must be " +MATCH_00060 += "one of attach,detach,query. Got FOO." @pytest.mark.parametrize( @@ -512,7 +495,7 @@ def mock_dcnm_send_image_policy_action(*args, **kwargs) -> Dict[str, Any]: ("attach", "attach"), ("detach", "detach"), ("query", "query"), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00060)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060)), ], ) def test_image_mgmt_image_policy_action_00060( @@ -527,23 +510,24 @@ def test_image_mgmt_image_policy_action_00060( - fail_json is called when value is not a valid action - fail_json error message is matched """ + instance = image_policy_action if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00060): - image_policy_action.action = value + with expected: + instance.action = value else: - image_policy_action.action = value - assert image_policy_action.action == expected + instance.action = value + assert instance.action == expected -match_00061 = "ImagePolicyAction.serial_numbers: instance.serial_numbers " -match_00061 += "must be a python list of switch serial numbers. Got FOO." +MATCH_00061 = "ImagePolicyAction.serial_numbers: instance.serial_numbers " +MATCH_00061 += "must be a python list of switch serial numbers. Got FOO." @pytest.mark.parametrize( "value, expected", [ (["FDO2112189M", "FDO21120U5D"], ["FDO2112189M", "FDO21120U5D"]), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00061)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00061)), ], ) def test_image_mgmt_image_policy_action_00061( @@ -558,9 +542,10 @@ def test_image_mgmt_image_policy_action_00061( - fail_json is called when value is not a list - fail_json error message is matched """ + instance = image_policy_action if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00061): - image_policy_action.serial_numbers = value + with expected: + instance.serial_numbers = value else: - image_policy_action.serial_numbers = value - assert image_policy_action.serial_numbers == expected + instance.serial_numbers = value + assert instance.serial_numbers == expected diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py similarity index 56% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 6173318e6..7f028615b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageStage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -11,13 +11,19 @@ # 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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument """ -Tests for ImageStage class +ImageStage - unit tests """ from __future__ import absolute_import, division, print_function -from contextlib import contextmanager from typing import Any, Dict import pytest @@ -25,37 +31,25 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ - ImageStage from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture +from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, + image_stage_fixture, + issu_details_by_serial_number_fixture) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of ImageStage -""" - - -@contextmanager -def does_not_raise(): - """ - A context manager that does not raise an exception. - """ - yield - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." -patch_common = patch_module_utils + "common." +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +PATCH_COMMON = PATCH_MODULE_UTILS + "common." -dcnm_send_controller_version = patch_common + "controller_version.dcnm_send" -dcnm_send_image_stage = patch_image_mgmt + "image_stage.dcnm_send" -dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +DCNM_SEND_CONTROLLER_VERSION = PATCH_COMMON + "controller_version.dcnm_send" +DCNM_SEND_IMAGE_STAGE = PATCH_IMAGE_MGMT + "image_stage.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" def responses_controller_version(key: str) -> Dict[str, str]: @@ -88,31 +82,7 @@ def responses_issu_details(key: str) -> Dict[str, str]: return response -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - -@pytest.fixture -def module(): - return ImageStage(MockAnsibleModule) - - -@pytest.fixture -def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: - return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) - - -def test_image_mgmt_stage_00001(module) -> None: +def test_image_mgmt_stage_00001(image_stage) -> None: """ Function - __init__ @@ -120,20 +90,20 @@ def test_image_mgmt_stage_00001(module) -> None: Test - Class attributes are initialized to expected values """ - module.__init__(MockAnsibleModule) - assert module.module == MockAnsibleModule - assert module.class_name == "ImageStage" - assert isinstance(module.properties, dict) - assert isinstance(module.serial_numbers_done, set) - assert module.controller_version is None - assert module.path is None - assert module.verb is None - assert module.payload is None - assert isinstance(module.issu_detail, SwitchIssuDetailsBySerialNumber) - assert isinstance(module.endpoints, ApiEndpoints) + instance = image_stage + assert instance.module == MockAnsibleModule + assert instance.class_name == "ImageStage" + assert isinstance(instance.properties, dict) + assert isinstance(instance.serial_numbers_done, set) + assert instance.controller_version is None + assert instance.path is None + assert instance.verb is None + assert instance.payload is None + assert isinstance(instance.issu_detail, SwitchIssuDetailsBySerialNumber) + assert isinstance(instance.endpoints, ApiEndpoints) -def test_image_mgmt_stage_00002(module) -> None: +def test_image_mgmt_stage_00002(image_stage) -> None: """ Function - _init_properties @@ -141,14 +111,14 @@ def test_image_mgmt_stage_00002(module) -> None: Test - Class properties are initialized to expected values """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("response_data") is None - assert module.properties.get("response") is None - assert module.properties.get("result") is None - assert module.properties.get("serial_numbers") is None - assert module.properties.get("check_interval") == 10 - assert module.properties.get("check_timeout") == 1800 + instance = image_stage + assert isinstance(instance.properties, dict) + assert instance.properties.get("response_data") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None + assert instance.properties.get("serial_numbers") is None + assert instance.properties.get("check_interval") == 10 + assert instance.properties.get("check_timeout") == 1800 @pytest.mark.parametrize( @@ -158,14 +128,14 @@ def test_image_mgmt_stage_00002(module) -> None: ("test_image_mgmt_stage_00003b", "12.1.3b"), ], ) -def test_image_mgmt_stage_00003(monkeypatch, module, key, expected) -> None: +def test_image_mgmt_stage_00003(monkeypatch, image_stage, key, expected) -> None: """ Function - _populate_controller_version Test - - test_image_mgmt_stage_00003a -> module.controller_version == "12.1.2e" - - test_image_mgmt_stage_00003b -> module.controller_version == "12.1.3b" + - test_image_mgmt_stage_00003a -> instance.controller_version == "12.1.2e" + - test_image_mgmt_stage_00003b -> instance.controller_version == "12.1.3b" Description _populate_controller_version retrieves the controller version from @@ -174,16 +144,19 @@ def test_image_mgmt_stage_00003(monkeypatch, module, key, expected) -> None: correctly-spelled "serialNumbers" key/value (12.1.3b). """ - def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_controller_version(*args) -> Dict[str, Any]: return responses_controller_version(key) - monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) + monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) - module._populate_controller_version() - assert module.controller_version == expected + instance = image_stage + instance._populate_controller_version() # pylint: disable=protected-access + assert instance.controller_version == expected -def test_image_mgmt_stage_00004(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_stage_00004( + monkeypatch, image_stage, issu_details_by_serial_number +) -> None: """ Function - prune_serial_numbers @@ -199,31 +172,34 @@ def test_image_mgmt_stage_00004(monkeypatch, module, mock_issu_details) -> None: imageStaged == "Success" (TODO: AND policy == ) """ - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00004a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance = image_stage + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO2112189M", "FDO211218AX", "FDO211218B5", "FDO211218FV", "FDO211218GC", ] - module.prune_serial_numbers() - assert isinstance(module.serial_numbers, list) - assert len(module.serial_numbers) == 3 - assert "FDO2112189M" in module.serial_numbers - assert "FDO211218AX" in module.serial_numbers - assert "FDO211218B5" in module.serial_numbers - assert "FDO211218FV" not in module.serial_numbers - assert "FDO211218GC" not in module.serial_numbers - - -def test_image_mgmt_stage_00005(monkeypatch, module, mock_issu_details) -> None: + instance.prune_serial_numbers() + assert isinstance(instance.serial_numbers, list) + assert len(instance.serial_numbers) == 3 + assert "FDO2112189M" in instance.serial_numbers + assert "FDO211218AX" in instance.serial_numbers + assert "FDO211218B5" in instance.serial_numbers + assert "FDO211218FV" not in instance.serial_numbers + assert "FDO211218GC" not in instance.serial_numbers + + +def test_image_mgmt_stage_00005( + monkeypatch, image_stage, issu_details_by_serial_number +) -> None: """ Function - validate_serial_numbers @@ -238,36 +214,37 @@ def test_image_mgmt_stage_00005(monkeypatch, module, mock_issu_details) -> None: number. """ - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00005a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - - module.issu_detail = mock_issu_details - module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "Image staging is failing for the following switch: " match += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. " match += "Check the switch connectivity to the controller " match += "and try again." + + instance = image_stage + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = ["FDO21120U5D", "FDO2112189M"] with pytest.raises(AnsibleFailJson, match=match): - module.validate_serial_numbers() + instance.validate_serial_numbers() -match_00006 = "ImageStage.commit: call instance.serial_numbers " -match_00006 += "before calling commit." +MATCH_00006 = "ImageStage.commit: call instance.serial_numbers " +MATCH_00006 += "before calling commit." @pytest.mark.parametrize( "serial_numbers_is_set, expected", [ (True, does_not_raise()), - (False, pytest.raises(AnsibleFailJson, match=match_00006)), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00006)), ], ) def test_image_mgmt_stage_00006( - monkeypatch, module, serial_numbers_is_set, expected + monkeypatch, image_stage, serial_numbers_is_set, expected ) -> None: """ Function @@ -290,17 +267,18 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00006a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) - monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) + monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + instance = image_stage if serial_numbers_is_set: - module.serial_numbers = ["FDO21120U5D"] + instance.serial_numbers = ["FDO21120U5D"] with expected: - module.commit() + instance.commit() -def test_image_mgmt_stage_00007(monkeypatch, module) -> None: +def test_image_mgmt_stage_00007(monkeypatch, image_stage) -> None: """ Function - commit @@ -324,21 +302,18 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00007a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_controller_version, mock_dcnm_send_controller_version) - monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) + monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" module_path += "stagingmanagement/stage-image" - module.serial_numbers = ["FDO21120U5D"] - module.commit() - assert module.path == module_path - assert module.verb == "POST" - - -# test_image_mgmt_stage_00008 -# test_commit_payload_serial_number_key_name (former name) + instance = image_stage + instance.serial_numbers = ["FDO21120U5D"] + instance.commit() + assert instance.path == module_path + assert instance.verb == "POST" @pytest.mark.parametrize( @@ -349,7 +324,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: ], ) def test_image_mgmt_stage_00008( - monkeypatch, module, controller_version, expected_serial_number_key + monkeypatch, image_stage, controller_version, expected_serial_number_key ) -> None: """ Function @@ -363,9 +338,10 @@ def test_image_mgmt_stage_00008( commit() will set the payload key name for the serial number based on the controller version, per Expected Results below """ + instance = image_stage - def mock_controller_version(*args, **kwargs) -> None: - module.controller_version = controller_version + def mock_controller_version(*args) -> None: + instance.controller_version = controller_version controller_version_patch = "ansible_collections.cisco.dcnm.plugins." controller_version_patch += "modules.dcnm_image_upgrade." @@ -376,19 +352,22 @@ def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00008a" return responses_image_stage(key) - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00008a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_image_stage, mock_dcnm_send_image_stage) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.serial_numbers = ["FDO21120U5D"] - module.commit() - assert expected_serial_number_key in module.payload.keys() + instance.serial_numbers = ["FDO21120U5D"] + instance.commit() + print(f"instance.payload: {instance.payload.keys()}") + assert expected_serial_number_key in instance.payload.keys() -def test_image_mgmt_stage_00009(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_stage_00009( + monkeypatch, image_stage, issu_details_by_serial_number +) -> None: """ Function - _wait_for_image_stage_to_complete @@ -406,27 +385,30 @@ def test_image_mgmt_stage_00009(monkeypatch, module, mock_issu_details) -> None: In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ + instance = image_stage - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00009a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 0 - module._wait_for_image_stage_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 2 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" in module.serial_numbers_done + instance.check_interval = 0 + instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 2 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_stage_00010(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_stage_00010( + monkeypatch, image_stage, issu_details_by_serial_number +) -> None: """ Function - _wait_for_image_stage_to_complete @@ -446,31 +428,34 @@ def test_image_mgmt_stage_00010(monkeypatch, module, mock_issu_details) -> None: In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ + instance = image_stage - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00010a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 0 + instance.check_interval = 0 match = "Seconds remaining 1800: stage image failed for " match += "cvd-2313-leaf, FDO2112189M, 172.22.150.108. image " match += "staged percent: 90" with pytest.raises(AnsibleFailJson, match=match): - module._wait_for_image_stage_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 1 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" not in module.serial_numbers_done + instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 1 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_stage_00011(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_stage_00011( + monkeypatch, image_stage, issu_details_by_serial_number +) -> None: """ Function - _wait_for_image_stage_to_complete @@ -488,20 +473,21 @@ def test_image_mgmt_stage_00011(monkeypatch, module, mock_issu_details) -> None: Description See test_wait_for_image_stage_to_complete for functional details. """ + instance = image_stage - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00011a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 1 - module.check_timeout = 1 + instance.check_interval = 1 + instance.check_timeout = 1 match = "ImageStage._wait_for_image_stage_to_complete: " match += "Timed out waiting for image stage to complete. " @@ -509,18 +495,16 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" with pytest.raises(AnsibleFailJson, match=match): - module._wait_for_image_stage_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 1 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" not in module.serial_numbers_done + instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 1 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" not in instance.serial_numbers_done -# test_image_mgmt_stage_00012 -# test_wait_for_current_actions_to_complete (former name) - - -def test_image_mgmt_stage_00012(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_stage_00012( + monkeypatch, image_stage, issu_details_by_serial_number +) -> None: """ Function - _wait_for_current_actions_to_complete @@ -542,27 +526,30 @@ def test_image_mgmt_stage_00012(monkeypatch, module, mock_issu_details) -> None: - upgrade - validated """ + instance = image_stage - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00012a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 0 - module._wait_for_current_actions_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 2 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" in module.serial_numbers_done + instance.check_interval = 0 + instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 2 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_stage_00013(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_stage_00013( + monkeypatch, image_stage, issu_details_by_serial_number +) -> None: """ Function - _wait_for_current_actions_to_complete @@ -579,28 +566,30 @@ def test_image_mgmt_stage_00013(monkeypatch, module, mock_issu_details) -> None: Description See test_image_mgmt_stage_00012 """ + instance = image_stage - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00013a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + match = "ImageStage._wait_for_current_actions_to_complete: " + match += "Timed out waiting for actions to complete. " + match += "serial_numbers_done: FDO21120U5D, " + match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 1 - module.check_timeout = 1 - - error_message = "ImageStage._wait_for_current_actions_to_complete: " - error_message += "Timed out waiting for actions to complete. " - error_message += "serial_numbers_done: FDO21120U5D, " - error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" - with pytest.raises(AnsibleFailJson, match=error_message): - module._wait_for_current_actions_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 1 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" not in module.serial_numbers_done + instance.check_interval = 1 + instance.check_timeout = 1 + + with pytest.raises(AnsibleFailJson, match=match): + instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 1 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" not in instance.serial_numbers_done diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py similarity index 72% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index f2ed8ed2f..fdff94050 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -12,9 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + + +""" +ImageUpgrade - unit tests +""" + from __future__ import absolute_import, division, print_function -from contextlib import contextmanager from typing import Any, Dict import pytest @@ -26,86 +39,63 @@ SwitchIssuDetailsByIpAddress from .fixture import load_fixture +from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, + image_upgrade_fixture, + issu_details_by_ip_address_fixture) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of NdfcSwitchUpgrade -""" - +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -@contextmanager -def does_not_raise(): - """ - A context manager that does not raise an exception. - """ - yield - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." - - -dcnm_send_image_upgrade = patch_image_mgmt + "image_upgrade.dcnm_send" -dcnm_send_install_options = patch_image_mgmt + "install_options.dcnm_send" -dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +DCNM_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.dcnm_send" +DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" def payloads_image_upgrade(key: str) -> Dict[str, str]: - payload_file = f"image_upgrade_payloads_ImageUpgrade" + """ + Return payloads for ImageUpgrade + """ + payload_file = "image_upgrade_payloads_ImageUpgrade" payload = load_fixture(payload_file).get(key) print(f"payload_data_image_upgrade: {key} : {payload}") return payload def responses_image_upgrade(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ImageUpgrade" + """ + Return responses for ImageUpgrade + """ + response_file = "image_upgrade_responses_ImageUpgrade" response = load_fixture(response_file).get(key) print(f"response_data_image_upgrade: {key} : {response}") return response def responses_install_options(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ImageInstallOptions" + """ + Return responses for ImageInstallOptions + """ + response_file = "image_upgrade_responses_ImageInstallOptions" response = load_fixture(response_file).get(key) print(f"response_data_install_options: {key} : {response}") return response def responses_issu_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchIssuDetails" + """ + Return responses for SwitchIssuDetails + """ + response_file = "image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) print(f"response_data_issu_details: {key} : {response}") return response -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - -@pytest.fixture -def module(): - return ImageUpgrade(MockAnsibleModule) - - -@pytest.fixture -def mock_issu_details() -> SwitchIssuDetailsByIpAddress: - return SwitchIssuDetailsByIpAddress(MockAnsibleModule) - - -def test_image_mgmt_upgrade_00001(module) -> None: +def test_image_mgmt_upgrade_00001(image_upgrade) -> None: """ Function - __init__ @@ -113,12 +103,12 @@ def test_image_mgmt_upgrade_00001(module) -> None: Test - Class attributes are initialized to expected values """ - module.__init__(MockAnsibleModule) - assert isinstance(module, ImageUpgrade) - assert module.class_name == "ImageUpgrade" + instance = image_upgrade + assert isinstance(instance, ImageUpgrade) + assert instance.class_name == "ImageUpgrade" -def test_image_mgmt_upgrade_00002(module) -> None: +def test_image_mgmt_upgrade_00002(image_upgrade) -> None: """ Function - _init_defaults @@ -126,24 +116,25 @@ def test_image_mgmt_upgrade_00002(module) -> None: Test - defaults dictionary is initialized with expected keys, values """ - module._init_defaults() - assert isinstance(module.defaults, dict) - assert module.defaults["reboot"] is False - assert module.defaults["stage"] is True - assert module.defaults["validate"] is True - assert module.defaults["upgrade"]["nxos"] is True - assert module.defaults["upgrade"]["epld"] is False - assert module.defaults["options"]["nxos"]["mode"] == "disruptive" - assert module.defaults["options"]["nxos"]["bios_force"] is False - assert module.defaults["options"]["epld"]["module"] == "ALL" - assert module.defaults["options"]["epld"]["golden"] is False - assert module.defaults["options"]["reboot"]["config_reload"] is False - assert module.defaults["options"]["reboot"]["write_erase"] is False - assert module.defaults["options"]["package"]["install"] is False - assert module.defaults["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00003(module) -> None: + instance = image_upgrade + instance._init_defaults() + assert isinstance(instance.defaults, dict) + assert instance.defaults["reboot"] is False + assert instance.defaults["stage"] is True + assert instance.defaults["validate"] is True + assert instance.defaults["upgrade"]["nxos"] is True + assert instance.defaults["upgrade"]["epld"] is False + assert instance.defaults["options"]["nxos"]["mode"] == "disruptive" + assert instance.defaults["options"]["nxos"]["bios_force"] is False + assert instance.defaults["options"]["epld"]["module"] == "ALL" + assert instance.defaults["options"]["epld"]["golden"] is False + assert instance.defaults["options"]["reboot"]["config_reload"] is False + assert instance.defaults["options"]["reboot"]["write_erase"] is False + assert instance.defaults["options"]["package"]["install"] is False + assert instance.defaults["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_00003(image_upgrade) -> None: """ Function - _init_properties @@ -151,35 +142,36 @@ def test_image_mgmt_upgrade_00003(module) -> None: Test - Class properties are initialized to expected values """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("bios_force") is False - assert module.properties.get("check_interval") == 10 - assert module.properties.get("check_timeout") == 1800 - assert module.properties.get("config_reload") is False - assert module.properties.get("devices") == None - assert module.properties.get("disruptive") is True - assert module.properties.get("epld_golden") is False - assert module.properties.get("epld_module") == "ALL" - assert module.properties.get("epld_upgrade") is False - assert module.properties.get("force_non_disruptive") is False - assert module.properties.get("response_data") == None - assert module.properties.get("response") == None - assert module.properties.get("result") == None - assert module.properties.get("non_disruptive") is False - assert module.properties.get("force_non_disruptive") is False - assert module.properties.get("package_install") is False - assert module.properties.get("package_uninstall") is False - assert module.properties.get("reboot") is False - assert module.properties.get("write_erase") is False - assert module.valid_nxos_mode == { + instance = image_upgrade + instance._init_properties() + assert isinstance(instance.properties, dict) + assert instance.properties.get("bios_force") is False + assert instance.properties.get("check_interval") == 10 + assert instance.properties.get("check_timeout") == 1800 + assert instance.properties.get("config_reload") is False + assert instance.properties.get("devices") is None + assert instance.properties.get("disruptive") is True + assert instance.properties.get("epld_golden") is False + assert instance.properties.get("epld_module") == "ALL" + assert instance.properties.get("epld_upgrade") is False + assert instance.properties.get("force_non_disruptive") is False + assert instance.properties.get("response_data") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None + assert instance.properties.get("non_disruptive") is False + assert instance.properties.get("force_non_disruptive") is False + assert instance.properties.get("package_install") is False + assert instance.properties.get("package_uninstall") is False + assert instance.properties.get("reboot") is False + assert instance.properties.get("write_erase") is False + assert instance.valid_nxos_mode == { "disruptive", "non_disruptive", "force_non_disruptive", } -def test_image_mgmt_upgrade_00004(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00004(monkeypatch, image_upgrade) -> None: """ Function - validate_devices @@ -198,24 +190,25 @@ def test_image_mgmt_upgrade_00004(monkeypatch, module) -> None: 1. instance.ip_addresses will contain {"172.22.150.102", "172.22.150.108"} """ + instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00005a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] - module.devices = devices - module.validate_devices() - assert isinstance(module.ip_addresses, set) - assert len(module.ip_addresses) == 2 - assert "172.22.150.102" in module.ip_addresses - assert "172.22.150.108" in module.ip_addresses + instance.devices = devices + instance.validate_devices() + assert isinstance(instance.ip_addresses, set) + assert len(instance.ip_addresses) == 2 + assert "172.22.150.102" in instance.ip_addresses + assert "172.22.150.108" in instance.ip_addresses -def test_image_mgmt_upgrade_00005(module) -> None: +def test_image_mgmt_upgrade_00005(image_upgrade) -> None: """ Function - commit @@ -223,12 +216,14 @@ def test_image_mgmt_upgrade_00005(module) -> None: Test - fail_json is called because devices is None """ + instance = image_upgrade + match = "ImageUpgrade.commit: call instance.devices before calling commit." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00006(module) -> None: +def test_image_mgmt_upgrade_00006(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -240,9 +235,11 @@ def test_image_mgmt_upgrade_00006(module) -> None: Test - merged_config contains expected default values """ + instance = image_upgrade + config = {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -258,7 +255,7 @@ def test_image_mgmt_upgrade_00006(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00007(module) -> None: +def test_image_mgmt_upgrade_00007(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -275,6 +272,8 @@ def test_image_mgmt_upgrade_00007(module) -> None: Description Force code coverage of the upgrade.epld is None path """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -282,7 +281,7 @@ def test_image_mgmt_upgrade_00007(module) -> None: "upgrade": {"nxos": False}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -298,7 +297,7 @@ def test_image_mgmt_upgrade_00007(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00008(module) -> None: +def test_image_mgmt_upgrade_00008(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -315,6 +314,8 @@ def test_image_mgmt_upgrade_00008(module) -> None: Description Force code coverage of the upgrade.nxos is None path """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -322,7 +323,7 @@ def test_image_mgmt_upgrade_00008(module) -> None: "upgrade": {"epld": True}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -338,7 +339,7 @@ def test_image_mgmt_upgrade_00008(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00009(module) -> None: +def test_image_mgmt_upgrade_00009(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -356,6 +357,8 @@ def test_image_mgmt_upgrade_00009(module) -> None: Description Force code coverage of the options is None path """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -363,7 +366,7 @@ def test_image_mgmt_upgrade_00009(module) -> None: "options": {}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -379,7 +382,7 @@ def test_image_mgmt_upgrade_00009(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00010(module) -> None: +def test_image_mgmt_upgrade_00010(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -397,6 +400,8 @@ def test_image_mgmt_upgrade_00010(module) -> None: Description Force code coverage of the options.nxos.bios_force is None path """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -404,7 +409,7 @@ def test_image_mgmt_upgrade_00010(module) -> None: "options": {"nxos": {"mode": "non_disruptive"}}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -420,7 +425,7 @@ def test_image_mgmt_upgrade_00010(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00011(module) -> None: +def test_image_mgmt_upgrade_00011(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -438,6 +443,8 @@ def test_image_mgmt_upgrade_00011(module) -> None: 1. merged_config will contain the expected default values 2. merged_config will contain the expected non-default values """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -445,7 +452,7 @@ def test_image_mgmt_upgrade_00011(module) -> None: "options": {"nxos": {"bios_force": True}}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -461,7 +468,7 @@ def test_image_mgmt_upgrade_00011(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00012(module) -> None: +def test_image_mgmt_upgrade_00012(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -479,6 +486,8 @@ def test_image_mgmt_upgrade_00012(module) -> None: 1. merged_config will contain the expected default values 2. merged_config will contain the expected non-default values """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -486,7 +495,7 @@ def test_image_mgmt_upgrade_00012(module) -> None: "options": {"epld": {"module": 27}}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -502,7 +511,7 @@ def test_image_mgmt_upgrade_00012(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00013(module) -> None: +def test_image_mgmt_upgrade_00013(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -520,6 +529,8 @@ def test_image_mgmt_upgrade_00013(module) -> None: 1. merged_config will contain the expected default values 2. merged_config will contain the expected non-default values """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -527,7 +538,7 @@ def test_image_mgmt_upgrade_00013(module) -> None: "options": {"epld": {"golden": True}}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -543,7 +554,7 @@ def test_image_mgmt_upgrade_00013(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00014(module) -> None: +def test_image_mgmt_upgrade_00014(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -561,6 +572,8 @@ def test_image_mgmt_upgrade_00014(module) -> None: 1. merged_config will contain the expected default values 2. merged_config will contain the expected non-default values """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -568,7 +581,7 @@ def test_image_mgmt_upgrade_00014(module) -> None: "options": {"reboot": {"config_reload": True}}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -584,7 +597,7 @@ def test_image_mgmt_upgrade_00014(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00015(module) -> None: +def test_image_mgmt_upgrade_00015(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -602,6 +615,8 @@ def test_image_mgmt_upgrade_00015(module) -> None: 1. merged_config will contain the expected default values 2. merged_config will contain the expected non-default values """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -609,7 +624,7 @@ def test_image_mgmt_upgrade_00015(module) -> None: "options": {"reboot": {"write_erase": True}}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -625,7 +640,7 @@ def test_image_mgmt_upgrade_00015(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00016(module) -> None: +def test_image_mgmt_upgrade_00016(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -643,6 +658,8 @@ def test_image_mgmt_upgrade_00016(module) -> None: 1. merged_config will contain the expected default values 2. merged_config will contain the expected non-default values """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -650,7 +667,7 @@ def test_image_mgmt_upgrade_00016(module) -> None: "options": {"package": {"install": True}}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -666,7 +683,7 @@ def test_image_mgmt_upgrade_00016(module) -> None: assert merged_config["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_00017(module) -> None: +def test_image_mgmt_upgrade_00017(image_upgrade) -> None: """ Function - _merge_defaults_to_switch_config @@ -684,6 +701,8 @@ def test_image_mgmt_upgrade_00017(module) -> None: 1. merged_config will contain the expected default values 2. merged_config will contain the expected non-default values """ + instance = image_upgrade + config = { "policy": "KR5M", "ip_address": "172.22.150.102", @@ -691,7 +710,7 @@ def test_image_mgmt_upgrade_00017(module) -> None: "options": {"package": {"uninstall": True}}, } - merged_config = module._merge_defaults_to_switch_config(config) + merged_config = instance._merge_defaults_to_switch_config(config) assert merged_config["reboot"] is False assert merged_config["stage"] is True assert merged_config["validate"] is True @@ -707,7 +726,7 @@ def test_image_mgmt_upgrade_00017(module) -> None: assert merged_config["options"]["package"]["uninstall"] is True -def test_image_mgmt_upgrade_00018(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00018(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -728,6 +747,8 @@ def test_image_mgmt_upgrade_00018(monkeypatch, module) -> None: 1. commit will call build_payload which will call fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00019a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -745,21 +766,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "KR5M", "stage": True, @@ -772,10 +793,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): ] match = r"ImageUpgrade.build_payload: upgrade.nxos must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00019(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00019(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -797,11 +818,13 @@ def test_image_mgmt_upgrade_00019(monkeypatch, module) -> None: Expected results: - 1. module.payload will equal a payload previously obtained by + 1. instance.payload will equal a payload previously obtained by running ansible-playbook against the controller for this scenario which verifies that the non-default values are included in the payload. """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00019a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -819,21 +842,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "KR5M", "stage": True, @@ -844,12 +867,12 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] - module.commit() + instance.commit() - assert module.payload == payloads_image_upgrade(key) + assert instance.payload == payloads_image_upgrade(key) -def test_image_mgmt_upgrade_00020(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -869,9 +892,11 @@ def test_image_mgmt_upgrade_00020(monkeypatch, module) -> None: Expected results: - 1. module.payload will equal a payload previously obtained by + 1. instance.payload will equal a payload previously obtained by running ansible-playbook against the controller for this scenario """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00020a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -889,21 +914,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -914,12 +939,12 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - module.commit() + instance.commit() - assert module.payload == payloads_image_upgrade(key) + assert instance.payload == payloads_image_upgrade(key) -def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00021(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -934,12 +959,14 @@ def test_image_mgmt_upgrade_00021(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain an invalid nxos.mode value + - instance.devices is set to contain an invalid nxos.mode value Expected results: 1. commit calls build_payload, which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00021a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -957,21 +984,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -986,10 +1013,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " match += "Got FOO." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00022(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1004,7 +1031,7 @@ def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain nxos.mode non_disruptive + - instance.devices is set to contain nxos.mode non_disruptive forcing the code to take nxos_mode == "non_disruptive" path Expected results: @@ -1013,6 +1040,8 @@ def test_image_mgmt_upgrade_00022(monkeypatch, module) -> None: 2. self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is False 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] is True """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00022a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1030,21 +1059,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1055,13 +1084,13 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - module.commit() - assert module.payload["issuUpgradeOptions1"]["disruptive"] is False - assert module.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is False - assert module.payload["issuUpgradeOptions1"]["nonDisruptive"] is True + instance.commit() + assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False + assert instance.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is False + assert instance.payload["issuUpgradeOptions1"]["nonDisruptive"] is True -def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00023(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1076,7 +1105,7 @@ def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain nxos.mode force_non_disruptive + - instance.devices is set to contain nxos.mode force_non_disruptive forcing the code to take nxos_mode == "force_non_disruptive" path Expected results: @@ -1085,6 +1114,8 @@ def test_image_mgmt_upgrade_00023(monkeypatch, module) -> None: 2. self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is True 3. self.payload["issuUpgradeOptions1"]["nonDisruptive"] is False """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00023a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1102,21 +1133,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1127,13 +1158,13 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - module.commit() - assert module.payload["issuUpgradeOptions1"]["disruptive"] is False - assert module.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is True - assert module.payload["issuUpgradeOptions1"]["nonDisruptive"] is False + instance.commit() + assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False + assert instance.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is True + assert instance.payload["issuUpgradeOptions1"]["nonDisruptive"] is False -def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00024(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1148,13 +1179,15 @@ def test_image_mgmt_upgrade_00024(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain invalid value for + - instance.devices is set to contain invalid value for options.nxos.bios_force Expected results: 1. commit calls build_payload which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00024a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1172,21 +1205,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1200,10 +1233,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade.build_payload: " match += r"options.nxos.bios_force must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00025(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00025(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1218,13 +1251,15 @@ def test_image_mgmt_upgrade_00025(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain epld golden True and + - instance.devices is set to contain epld golden True and upgrade.nxos True. Expected results: 1. commit calls build_payload which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00025a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1242,21 +1277,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1272,10 +1307,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += "all other upgrade options, e.g. upgrade.nxos, " match += "must be False." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00026(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00026(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1290,12 +1325,14 @@ def test_image_mgmt_upgrade_00026(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain invalid epld.module + - instance.devices is set to contain invalid epld.module Expected results: 1. commit calls build_payload which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00026a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1313,21 +1350,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1346,10 +1383,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += "options.epld.module must either be 'ALL' " match += r"or an integer. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00027(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00027(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1364,12 +1401,14 @@ def test_image_mgmt_upgrade_00027(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain invalid epld.golden + - instance.devices is set to contain invalid epld.golden Expected results: 1. commit calls build_payload which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00027a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1387,21 +1426,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1419,10 +1458,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade.build_payload: " match += r"options.epld.golden must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00028(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00028(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1437,12 +1476,14 @@ def test_image_mgmt_upgrade_00028(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain invalid value for reboot + - instance.devices is set to contain invalid value for reboot Expected results: 1. commit calls build_payload which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00028a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1460,21 +1501,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1488,10 +1529,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade.build_payload: " match += r"reboot must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00029(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00029(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1506,13 +1547,15 @@ def test_image_mgmt_upgrade_00029(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain invalid value for + - instance.devices is set to contain invalid value for options.reboot.config_reload Expected results: 1. commit calls build_payload which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00029a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1530,21 +1573,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1562,10 +1605,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade.build_payload: " match += r"options.reboot.config_reload must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00030(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00030(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1580,13 +1623,15 @@ def test_image_mgmt_upgrade_00030(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain invalid value for + - instance.devices is set to contain invalid value for options.reboot.write_erase Expected results: 1. commit calls build_payload which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00030a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1604,21 +1649,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1636,10 +1681,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade.build_payload: " match += r"options.reboot.write_erase must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00031(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1653,7 +1698,7 @@ def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain invalid value for + - instance.devices is set to contain invalid value for options.package.uninstall Expected results: @@ -1666,6 +1711,8 @@ def test_image_mgmt_upgrade_00031(monkeypatch, module) -> None: on invalid values before ImageUpgrade has a chance to verify the value. """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00031a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1683,21 +1730,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1715,10 +1762,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade.build_payload: " match += r"options.package.uninstall must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00032(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00032(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1741,6 +1788,8 @@ def test_image_mgmt_upgrade_00032(monkeypatch, module) -> None: 1. commit calls fail_json because self.result will not equal "success" """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00032a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1758,21 +1807,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1791,10 +1840,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += "imagemanagement/rest/imageupgrade/upgrade-image', " match += r"'RETURN_CODE': 500\}" with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() -def test_image_mgmt_upgrade_00033(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00033(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1809,13 +1858,15 @@ def test_image_mgmt_upgrade_00033(monkeypatch, module) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - module.devices is set to contain invalid value for + - instance.devices is set to contain invalid value for upgrade.epld Expected results: 1. commit calls build_payload which calls fail_json """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00033a" def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: @@ -1833,21 +1884,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "NR3F", "stage": True, @@ -1865,29 +1916,31 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade.build_payload: " match += r"upgrade.epld must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): - module.commit() + instance.commit() # getters -def test_image_mgmt_upgrade_00043(module) -> None: +def test_image_mgmt_upgrade_00043(image_upgrade) -> None: """ Function - check_interval """ - assert module.check_interval == 10 + instance = image_upgrade + assert instance.check_interval == 10 -def test_image_mgmt_upgrade_00044(module) -> None: +def test_image_mgmt_upgrade_00044(image_upgrade) -> None: """ Function - check_timeout """ - assert module.check_timeout == 1800 + instance = image_upgrade + assert instance.check_timeout == 1800 -def test_image_mgmt_upgrade_00045(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00045(monkeypatch, image_upgrade) -> None: """ Function - response_data @@ -1904,8 +1957,10 @@ def test_image_mgmt_upgrade_00045(monkeypatch, module) -> None: Expected results: - 1. module.response_data == 121 + 1. instance.response_data == 121 """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00045a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -1923,21 +1978,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "KR5M", "stage": True, @@ -1948,11 +2003,11 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] - module.commit() - assert module.response_data == 121 + instance.commit() + assert instance.response_data == 121 -def test_image_mgmt_upgrade_00046(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00046(monkeypatch, image_upgrade) -> None: """ Function - result @@ -1969,8 +2024,10 @@ def test_image_mgmt_upgrade_00046(monkeypatch, module) -> None: Expected results: - 1. module.result == {'success': True, 'changed': True} + 1. instance.result == {'success': True, 'changed': True} """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00046a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -1988,21 +2045,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "KR5M", "stage": True, @@ -2013,11 +2070,11 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] - module.commit() - assert module.result == {"success": True, "changed": True} + instance.commit() + assert instance.result == {"success": True, "changed": True} -def test_image_mgmt_upgrade_00047(monkeypatch, module) -> None: +def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: """ Function - response @@ -2034,8 +2091,10 @@ def test_image_mgmt_upgrade_00047(monkeypatch, module) -> None: Expected results: - 1. module.response is a dict + 1. instance.response is a dict """ + instance = image_upgrade + key = "test_image_mgmt_upgrade_00047a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -2053,21 +2112,21 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass - monkeypatch.setattr(dcnm_send_install_options, mock_dcnm_send_install_options) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - monkeypatch.setattr(dcnm_send_image_upgrade, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) monkeypatch.setattr( - module, + instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) monkeypatch.setattr( - module, + instance, "_wait_for_image_upgrade_to_complete", mock_wait_for_image_upgrade_to_complete, ) - module.devices = [ + instance.devices = [ { "policy": "KR5M", "stage": True, @@ -2078,15 +2137,15 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] - module.commit() - print(f"module.response: {module.response}") - assert isinstance(module.response, dict) - assert module.response["DATA"] == 121 + instance.commit() + print(f"instance.response: {instance.response}") + assert isinstance(instance.response, dict) + assert instance.response["DATA"] == 121 # setters -match_00060 = "ImageUpgrade.bios_force: instance.bios_force must be a boolean." +MATCH_00060 = "ImageUpgrade.bios_force: instance.bios_force must be a boolean." @pytest.mark.parametrize( @@ -2094,24 +2153,26 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00060)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060)), ], ) -def test_image_mgmt_upgrade_00060(module, value, expected) -> None: +def test_image_mgmt_upgrade_00060(image_upgrade, value, expected) -> None: """ Function - bios_force setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00060): - module.bios_force = value + with pytest.raises(AnsibleFailJson, match=MATCH_00060): + instance.bios_force = value else: - module.bios_force = value - assert module.bios_force == expected + instance.bios_force = value + assert instance.bios_force == expected -match_00061 = "ImageUpgrade.config_reload: " -match_00061 += "instance.config_reload must be a boolean." +MATCH_00061 = "ImageUpgrade.config_reload: " +MATCH_00061 += "instance.config_reload must be a boolean." @pytest.mark.parametrize( @@ -2119,58 +2180,62 @@ def test_image_mgmt_upgrade_00060(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00061)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00061)), ], ) -def test_image_mgmt_upgrade_00061(module, value, expected) -> None: +def test_image_mgmt_upgrade_00061(image_upgrade, value, expected) -> None: """ Function - config_reload setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00061): - module.config_reload = value + with pytest.raises(AnsibleFailJson, match=MATCH_00061): + instance.config_reload = value else: - module.config_reload = value - assert module.config_reload == expected + instance.config_reload = value + assert instance.config_reload == expected -match_00062_common = "ImageUpgrade.devices: " -match_00062_common += "instance.devices must be a python list of dict" +MATCH_00062_COMMON = "ImageUpgrade.devices: " +MATCH_00062_COMMON += "instance.devices must be a python list of dict" -match_00062_fail_1 = f"{match_00062_common}. Got not a list." -match_00062_fail_2 = rf"{match_00062_common}. Got \['not a dict'\]." +MATCH_00062_FAIL_1 = f"{MATCH_00062_COMMON}. Got not a list." +MATCH_00062_FAIL_2 = rf"{MATCH_00062_COMMON}. Got \['not a dict'\]." -match_00062_fail_3 = f"{match_00062_common}, where each dict contains " -match_00062_fail_3 += "the following keys: ip_address. " -match_00062_fail_3 += r"Got \[\{'bad_key_ip_address': '192.168.1.1'\}\]." +MATCH_00062_FAIL_3 = f"{MATCH_00062_COMMON}, where each dict contains " +MATCH_00062_FAIL_3 += "the following keys: ip_address. " +MATCH_00062_FAIL_3 += r"Got \[\{'bad_key_ip_address': '192.168.1.1'\}\]." -data_00062_pass = [{"ip_address": "192.168.1.1"}] -data_00062_fail_1 = "not a list" -data_00062_fail_2 = ["not a dict"] -data_00062_fail_3 = [{"bad_key_ip_address": "192.168.1.1"}] +DATA_00062_PASS = [{"ip_address": "192.168.1.1"}] +DATA_00062_FAIL_1 = "not a list" +DATA_00062_FAIL_2 = ["not a dict"] +DATA_00062_FAIL_3 = [{"bad_key_ip_address": "192.168.1.1"}] @pytest.mark.parametrize( "value, expected", [ - (data_00062_pass, does_not_raise()), - (data_00062_fail_1, pytest.raises(AnsibleFailJson, match=match_00062_fail_1)), - (data_00062_fail_2, pytest.raises(AnsibleFailJson, match=match_00062_fail_2)), - (data_00062_fail_3, pytest.raises(AnsibleFailJson, match=match_00062_fail_3)), + (DATA_00062_PASS, does_not_raise()), + (DATA_00062_FAIL_1, pytest.raises(AnsibleFailJson, match=MATCH_00062_FAIL_1)), + (DATA_00062_FAIL_2, pytest.raises(AnsibleFailJson, match=MATCH_00062_FAIL_2)), + (DATA_00062_FAIL_3, pytest.raises(AnsibleFailJson, match=MATCH_00062_FAIL_3)), ], ) -def test_image_mgmt_upgrade_00062(module, value, expected) -> None: +def test_image_mgmt_upgrade_00062(image_upgrade, value, expected) -> None: """ Function - devices setter """ + instance = image_upgrade + with expected: - module.devices = value + instance.devices = value -match_00063 = "ImageUpgrade.disruptive: " -match_00063 += "instance.disruptive must be a boolean." +MATCH_00063 = "ImageUpgrade.disruptive: " +MATCH_00063 += "instance.disruptive must be a boolean." @pytest.mark.parametrize( @@ -2178,24 +2243,26 @@ def test_image_mgmt_upgrade_00062(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00063)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00063)), ], ) -def test_image_mgmt_upgrade_00063(module, value, expected) -> None: +def test_image_mgmt_upgrade_00063(image_upgrade, value, expected) -> None: """ Function - disruptive setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00063): - module.disruptive = value + with pytest.raises(AnsibleFailJson, match=MATCH_00063): + instance.disruptive = value else: - module.disruptive = value - assert module.disruptive == expected + instance.disruptive = value + assert instance.disruptive == expected -match_00064 = "ImageUpgrade.epld_golden: " -match_00064 += "instance.epld_golden must be a boolean." +MATCH_00064 = "ImageUpgrade.epld_golden: " +MATCH_00064 += "instance.epld_golden must be a boolean." @pytest.mark.parametrize( @@ -2203,24 +2270,26 @@ def test_image_mgmt_upgrade_00063(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00064)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00064)), ], ) -def test_image_mgmt_upgrade_00064(module, value, expected) -> None: +def test_image_mgmt_upgrade_00064(image_upgrade, value, expected) -> None: """ Function - epld_golden setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00064): - module.epld_golden = value + with pytest.raises(AnsibleFailJson, match=MATCH_00064): + instance.epld_golden = value else: - module.epld_golden = value - assert module.epld_golden == expected + instance.epld_golden = value + assert instance.epld_golden == expected -match_00065 = "ImageUpgrade.epld_upgrade: " -match_00065 += "instance.epld_upgrade must be a boolean." +MATCH_00065 = "ImageUpgrade.epld_upgrade: " +MATCH_00065 += "instance.epld_upgrade must be a boolean." @pytest.mark.parametrize( @@ -2228,24 +2297,26 @@ def test_image_mgmt_upgrade_00064(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00065)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00065)), ], ) -def test_image_mgmt_upgrade_00065(module, value, expected) -> None: +def test_image_mgmt_upgrade_00065(image_upgrade, value, expected) -> None: """ Function - epld_upgrade setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00065): - module.epld_upgrade = value + with pytest.raises(AnsibleFailJson, match=MATCH_00065): + instance.epld_upgrade = value else: - module.epld_upgrade = value - assert module.epld_upgrade == expected + instance.epld_upgrade = value + assert instance.epld_upgrade == expected -match_00066_fail_1 = "ImageUpgrade.epld_module: " -match_00066_fail_1 += "instance.epld_module must be an integer or 'ALL'" +MATCH_00066_FAIL_1 = "ImageUpgrade.epld_module: " +MATCH_00066_FAIL_1 += "instance.epld_module must be an integer or 'ALL'" @pytest.mark.parametrize( @@ -2255,20 +2326,21 @@ def test_image_mgmt_upgrade_00065(module, value, expected) -> None: (1, does_not_raise()), (27, does_not_raise()), ("27", does_not_raise()), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00066_fail_1)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00066_FAIL_1)), ], ) -def test_image_mgmt_upgrade_00066(module, value, expected) -> None: +def test_image_mgmt_upgrade_00066(image_upgrade, value, expected) -> None: """ Function - epld_module setter """ + instance = image_upgrade with expected: - module.epld_module = value + instance.epld_module = value -match_00067 = "ImageUpgrade.force_non_disruptive: " -match_00067 += "instance.force_non_disruptive must be a boolean." +MATCH_00067 = "ImageUpgrade.force_non_disruptive: " +MATCH_00067 += "instance.force_non_disruptive must be a boolean." @pytest.mark.parametrize( @@ -2276,24 +2348,26 @@ def test_image_mgmt_upgrade_00066(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00067)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00067)), ], ) -def test_image_mgmt_upgrade_00067(module, value, expected) -> None: +def test_image_mgmt_upgrade_00067(image_upgrade, value, expected) -> None: """ Function - force_non_disruptive setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00067): - module.force_non_disruptive = value + with pytest.raises(AnsibleFailJson, match=MATCH_00067): + instance.force_non_disruptive = value else: - module.force_non_disruptive = value - assert module.force_non_disruptive == expected + instance.force_non_disruptive = value + assert instance.force_non_disruptive == expected -match_00068 = "ImageUpgrade.non_disruptive: " -match_00068 += "instance.non_disruptive must be a boolean." +MATCH_00068 = "ImageUpgrade.non_disruptive: " +MATCH_00068 += "instance.non_disruptive must be a boolean." @pytest.mark.parametrize( @@ -2301,24 +2375,26 @@ def test_image_mgmt_upgrade_00067(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00068)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00068)), ], ) -def test_image_mgmt_upgrade_00068(module, value, expected) -> None: +def test_image_mgmt_upgrade_00068(image_upgrade, value, expected) -> None: """ Function - non_disruptive setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00068): - module.non_disruptive = value + with pytest.raises(AnsibleFailJson, match=MATCH_00068): + instance.non_disruptive = value else: - module.non_disruptive = value - assert module.non_disruptive == expected + instance.non_disruptive = value + assert instance.non_disruptive == expected -match_00069 = "ImageUpgrade.package_install: " -match_00069 += "instance.package_install must be a boolean." +MATCH_00069 = "ImageUpgrade.package_install: " +MATCH_00069 += "instance.package_install must be a boolean." @pytest.mark.parametrize( @@ -2326,24 +2402,26 @@ def test_image_mgmt_upgrade_00068(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00069)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00069)), ], ) -def test_image_mgmt_upgrade_00069(module, value, expected) -> None: +def test_image_mgmt_upgrade_00069(image_upgrade, value, expected) -> None: """ Function - package_install setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00069): - module.package_install = value + with pytest.raises(AnsibleFailJson, match=MATCH_00069): + instance.package_install = value else: - module.package_install = value - assert module.package_install == expected + instance.package_install = value + assert instance.package_install == expected -match_00070 = "ImageUpgrade.package_uninstall: " -match_00070 += "instance.package_uninstall must be a boolean." +MATCH_00070 = "ImageUpgrade.package_uninstall: " +MATCH_00070 += "instance.package_uninstall must be a boolean." @pytest.mark.parametrize( @@ -2351,24 +2429,26 @@ def test_image_mgmt_upgrade_00069(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00070)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00070)), ], ) -def test_image_mgmt_upgrade_00070(module, value, expected) -> None: +def test_image_mgmt_upgrade_00070(image_upgrade, value, expected) -> None: """ Function - package_uninstall setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00070): - module.package_uninstall = value + with pytest.raises(AnsibleFailJson, match=MATCH_00070): + instance.package_uninstall = value else: - module.package_uninstall = value - assert module.package_uninstall == expected + instance.package_uninstall = value + assert instance.package_uninstall == expected -match_00071 = "ImageUpgrade.reboot: " -match_00071 += "instance.reboot must be a boolean." +MATCH_00071 = "ImageUpgrade.reboot: " +MATCH_00071 += "instance.reboot must be a boolean." @pytest.mark.parametrize( @@ -2376,24 +2456,26 @@ def test_image_mgmt_upgrade_00070(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00071)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00071)), ], ) -def test_image_mgmt_upgrade_00071(module, value, expected) -> None: +def test_image_mgmt_upgrade_00071(image_upgrade, value, expected) -> None: """ Function - reboot setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00071): - module.reboot = value + with pytest.raises(AnsibleFailJson, match=MATCH_00071): + instance.reboot = value else: - module.reboot = value - assert module.reboot == expected + instance.reboot = value + assert instance.reboot == expected -match_00072 = "ImageUpgrade.write_erase: " -match_00072 += "instance.write_erase must be a boolean." +MATCH_00072 = "ImageUpgrade.write_erase: " +MATCH_00072 += "instance.write_erase must be a boolean." @pytest.mark.parametrize( @@ -2401,23 +2483,27 @@ def test_image_mgmt_upgrade_00071(module, value, expected) -> None: [ (True, True), (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=match_00072)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00072)), ], ) -def test_image_mgmt_upgrade_00072(module, value, expected) -> None: +def test_image_mgmt_upgrade_00072(image_upgrade, value, expected) -> None: """ Function - write_erase setter """ + instance = image_upgrade + if value == "FOO": - with pytest.raises(AnsibleFailJson, match=match_00072): - module.write_erase = value + with pytest.raises(AnsibleFailJson, match=MATCH_00072): + instance.write_erase = value else: - module.write_erase = value - assert module.write_erase == expected + instance.write_erase = value + assert instance.write_erase == expected -def test_image_mgmt_upgrade_00080(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_upgrade_00080( + monkeypatch, image_upgrade, issu_details_by_ip_address +) -> None: """ Function - _wait_for_current_actions_to_complete @@ -2435,34 +2521,37 @@ def test_image_mgmt_upgrade_00080(monkeypatch, module, mock_issu_details) -> Non ["imageStaged", "upgrade", "validated"] Expectations: - 1. module.ipv4_done is a set() - 2. module.ipv4_done is length 2 - 3. module.ipv4_done contains all ip addresses in - module.ip_addresses + 1. instance.ipv4_done is a set() + 2. instance.ipv4_done is length 2 + 3. instance.ipv4_done contains all ip addresses in + instance.ip_addresses 4. fail_json is not called """ + instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00080a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.ip_addresses = [ + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ "172.22.150.102", "172.22.150.108", ] - module.check_interval = 0 + instance.check_interval = 0 with does_not_raise(): - module._wait_for_current_actions_to_complete() - assert isinstance(module.ipv4_done, set) - assert len(module.ipv4_done) == 2 - assert "172.22.150.102" in module.ipv4_done - assert "172.22.150.108" in module.ipv4_done + instance._wait_for_current_actions_to_complete() + assert isinstance(instance.ipv4_done, set) + assert len(instance.ipv4_done) == 2 + assert "172.22.150.102" in instance.ipv4_done + assert "172.22.150.108" in instance.ipv4_done -def test_image_mgmt_upgrade_00081(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_upgrade_00081( + monkeypatch, image_upgrade, issu_details_by_ip_address +) -> None: """ Function - _wait_for_current_actions_to_complete @@ -2474,27 +2563,28 @@ def test_image_mgmt_upgrade_00081(monkeypatch, module, mock_issu_details) -> Non See test_image_mgmt_upgrade_00080 for functional details. Expectations: - - module.ipv4_done is a set() - - module.ipv4_done is length 1 - - module.ipv4_done contains 172.22.150.102 - - module.ipv4_done does not contain 172.22.150.108 + - instance.ipv4_done is a set() + - instance.ipv4_done is length 1 + - instance.ipv4_done contains 172.22.150.102 + - instance.ipv4_done does not contain 172.22.150.108 - fail_json is called due to timeout - fail_json error message is matched """ + instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00081a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.ip_addresses = [ + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ "172.22.150.102", "172.22.150.108", ] - module.check_interval = 1 - module.check_timeout = 1 + instance.check_interval = 1 + instance.check_timeout = 1 match = "ImageUpgrade._wait_for_current_actions_to_complete: " match += "Timed out waiting for actions to complete. " @@ -2503,14 +2593,16 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += r"check the device\(s\) to determine the cause " match += r"\(e\.g\. show install all status\)\." with pytest.raises(AnsibleFailJson, match=match): - module._wait_for_current_actions_to_complete() - assert isinstance(module.ipv4_done, set) - assert len(module.ipv4_done) == 1 - assert "172.22.150.102" in module.ipv4_done - assert "172.22.150.108" not in module.ipv4_done + instance._wait_for_current_actions_to_complete() + assert isinstance(instance.ipv4_done, set) + assert len(instance.ipv4_done) == 1 + assert "172.22.150.102" in instance.ipv4_done + assert "172.22.150.108" not in instance.ipv4_done -def test_image_mgmt_upgrade_00090(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_upgrade_00090( + monkeypatch, image_upgrade, issu_details_by_ip_address +) -> None: """ Function - _wait_for_image_upgrade_to_complete @@ -2526,24 +2618,25 @@ def test_image_mgmt_upgrade_00090(monkeypatch, module, mock_issu_details) -> Non In the case where any ip address is "Failed", the module calls fail_json. Expectations: - - module.ipv4_done is a set() - - module.ipv4_done has length 1 - - module.ipv4_done contains 172.22.150.102, upgrade is "Success" + - instance.ipv4_done is a set() + - instance.ipv4_done has length 1 + - instance.ipv4_done contains 172.22.150.102, upgrade is "Success" - Call fail_json on ip address 172.22.150.108, upgrade is "Failed" """ + instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00090a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.ip_addresses = [ + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ "172.22.150.102", "172.22.150.108", ] - module.check_interval = 0 + instance.check_interval = 0 match = "ImageUpgrade._wait_for_image_upgrade_to_complete: " match += "Seconds remaining 1800: " match += "upgrade image Failed for cvd-2313-leaf, FDO2112189M, " @@ -2551,14 +2644,16 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "Check the controller to determine the cause. " match += "Operations > Image Management > Devices > View Details." with pytest.raises(AnsibleFailJson, match=match): - module._wait_for_image_upgrade_to_complete() - assert isinstance(module.ipv4_done, set) - assert len(module.ipv4_done) == 1 - assert "172.22.150.102" in module.ipv4_done - assert "172.22.150.108" not in module.ipv4_done + instance._wait_for_image_upgrade_to_complete() + assert isinstance(instance.ipv4_done, set) + assert len(instance.ipv4_done) == 1 + assert "172.22.150.102" in instance.ipv4_done + assert "172.22.150.108" not in instance.ipv4_done -def test_image_mgmt_upgrade_00091(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_upgrade_00091( + monkeypatch, image_upgrade, issu_details_by_ip_address +) -> None: """ Function - _wait_for_image_upgrade_to_complete @@ -2577,25 +2672,26 @@ def test_image_mgmt_upgrade_00091(monkeypatch, module, mock_issu_details) -> Non timeout is exceeded Expectations: - - module.ipv4_done is a set() - - module.ipv4_done has length 1 - - module.ipv4_done contains 172.22.150.102, upgrade is "Success" + - instance.ipv4_done is a set() + - instance.ipv4_done has length 1 + - instance.ipv4_done contains 172.22.150.102, upgrade is "Success" - fail_json is called due to timeout exceeded """ + instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00091a" return responses_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.ip_addresses = [ + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ "172.22.150.102", "172.22.150.108", ] - module.check_interval = 1 - module.check_timeout = 1 + instance.check_interval = 1 + instance.check_timeout = 1 match = "ImageUpgrade._wait_for_image_upgrade_to_complete: " match += r"The following device\(s\) did not complete upgrade: " @@ -2603,8 +2699,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += r"Check the device\(s\) to determine the cause " match += r"\(e\.g\. show install all status\)\." with pytest.raises(AnsibleFailJson, match=match): - module._wait_for_image_upgrade_to_complete() - assert isinstance(module.ipv4_done, set) - assert len(module.ipv4_done) == 1 - assert "172.22.150.102" in module.ipv4_done - assert "172.22.150.108" not in module.ipv4_done + instance._wait_for_image_upgrade_to_complete() + assert isinstance(instance.ipv4_done, set) + assert len(instance.ipv4_done) == 1 + assert "172.22.150.102" in instance.ipv4_done + assert "172.22.150.108" not in instance.ipv4_done diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py similarity index 80% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 89a03605b..6e6089129 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageUpgradeCommon.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -12,57 +12,39 @@ # See the License for the specific language governing permissions and # limitations under the License. +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument + +""" +ImageUpgradeCommon - unit tests +""" + from __future__ import absolute_import, division, print_function -from contextlib import contextmanager from typing import Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ - ImageUpgradeCommon +# from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +# ImageUpgradeCommon from .fixture import load_fixture +from .image_upgrade_utils import MockAnsibleModule, does_not_raise, image_upgrade_common_fixture __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of class ImageUpgradeCommon -""" - -@contextmanager -def does_not_raise(): - """ - A context manager that does not raise an exception. - """ - yield - - -class MockAnsibleModule: +def responses_image_upgrade_common(key: str) -> Dict[str, str]: """ - Mock the AnsibleModule class + Return responses from ImageUpgradeCommon """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - -@pytest.fixture -def image_upgrade_common(): - return ImageUpgradeCommon(MockAnsibleModule) - - -def responses_image_upgrade_common(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_ImageUpgradeCommon" + response_file = "image_upgrade_responses_ImageUpgradeCommon" response = load_fixture(response_file).get(key) verb = response.get("METHOD") print(f"{key} : {verb} : {response}") @@ -82,11 +64,11 @@ def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: - image_upgrade_common.logfile is /tmp/ansible_dcnm.log """ with does_not_raise(): - image_upgrade_common.__init__(MockAnsibleModule) - assert image_upgrade_common.params == {} - assert image_upgrade_common.debug is False - assert image_upgrade_common.fd == None - assert image_upgrade_common.logfile == "/tmp/ansible_dcnm.log" + instance = image_upgrade_common + assert instance.params == {} + assert instance.debug is False + assert instance.fd is None + assert instance.logfile == "/tmp/ansible_dcnm.log" @pytest.mark.parametrize( @@ -121,9 +103,11 @@ def test_image_mgmt_image_upgrade_common_00020( _handle_reponse() calls either _handle_get_reponse if verb is "GET" or _handle_post_put_delete_response if verb is "DELETE", "POST", or "PUT" """ + instance = image_upgrade_common + data = responses_image_upgrade_common(key) with does_not_raise(): - result = image_upgrade_common._handle_response( + result = instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) assert result.get("success") == expected.get("success") @@ -162,9 +146,11 @@ def test_image_mgmt_image_upgrade_common_00030( _handle_reponse() calls either _handle_get_reponse if verb is "GET" or _handle_post_put_delete_response if verb is "DELETE", "POST", or "PUT" """ + instance = image_upgrade_common + data = responses_image_upgrade_common(key) with does_not_raise(): - result = image_upgrade_common._handle_response( + result = instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) assert result.get("success") == expected.get("success") @@ -203,9 +189,11 @@ def test_image_mgmt_image_upgrade_common_00040( _handle_reponse() calls either _handle_get_reponse if verb is "GET" or _handle_post_put_delete_response if verb is "DELETE", "POST", or "PUT" """ + instance = image_upgrade_common + data = responses_image_upgrade_common(key) with does_not_raise(): - result = image_upgrade_common._handle_response( + result = instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) assert result.get("success") == expected.get("success") @@ -243,8 +231,10 @@ def test_image_mgmt_image_upgrade_common_00050( Test - _handle_reponse returns expected values for GET requests """ + instance = image_upgrade_common + data = responses_image_upgrade_common(key) - result = image_upgrade_common._handle_response( + result = instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) assert result.get("success") == expected.get("success") @@ -259,9 +249,11 @@ def test_image_mgmt_image_upgrade_common_00060(image_upgrade_common) -> None: Test - fail_json is called because an unknown request verb is provided """ + instance = image_upgrade_common + data = responses_image_upgrade_common("test_image_mgmt_image_upgrade_common_00060a") with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): - image_upgrade_common._handle_response(data.get("response"), data.get("verb")) + instance._handle_response(data.get("response"), data.get("verb")) # pylint: disable=protected-access @pytest.mark.parametrize( @@ -296,9 +288,11 @@ def test_image_mgmt_image_upgrade_common_00070( - fail_json is not called - _handle_get_reponse() returns expected values for GET requests """ + instance = image_upgrade_common + data = responses_image_upgrade_common(key) with does_not_raise(): - result = image_upgrade_common._handle_get_response(data.get("response")) + result = instance._handle_get_response(data.get("response")) # pylint: disable=protected-access assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") @@ -332,9 +326,11 @@ def test_image_mgmt_image_upgrade_common_00080( - return expected values for POST requests - fail_json is not called """ + instance = image_upgrade_common + data = responses_image_upgrade_common(key) with does_not_raise(): - result = image_upgrade_common._handle_post_put_delete_response( + result = instance._handle_post_put_delete_response( # pylint: disable=protected-access data.get("response") ) assert result.get("success") == expected.get("success") @@ -371,7 +367,8 @@ def test_image_mgmt_image_upgrade_common_00090( Test - expected values are returned for all cases """ - assert image_upgrade_common.make_boolean(key) == expected + instance = image_upgrade_common + assert instance.make_boolean(key) == expected @pytest.mark.parametrize( @@ -404,7 +401,8 @@ def test_image_mgmt_image_upgrade_common_00100( Test - expected values are returned for all cases """ - assert image_upgrade_common.make_none(key) == expected + instance = image_upgrade_common + assert instance.make_none(key) == expected def test_image_mgmt_image_upgrade_common_00110(image_upgrade_common) -> None: @@ -415,9 +413,11 @@ def test_image_mgmt_image_upgrade_common_00110(image_upgrade_common) -> None: Test - log_msg returns None when debug is False """ - ERROR_MESSAGE = "This is an error message" - image_upgrade_common.debug = False - assert image_upgrade_common.log_msg(ERROR_MESSAGE) == None + instance = image_upgrade_common + + error_message = "This is an error message" + instance.debug = False + assert instance.log_msg(error_message) is None def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) -> None: @@ -428,16 +428,18 @@ def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) - Test - log_msg writes to the logfile when debug is True """ + instance = image_upgrade_common + directory = tmp_path / "test_log_msg" directory.mkdir() - filename = directory / f"test_log_msg.txt" + filename = directory / "test_log_msg.txt" - ERROR_MESSAGE = "This is an error message" - image_upgrade_common.debug = True - image_upgrade_common.logfile = filename - image_upgrade_common.log_msg(ERROR_MESSAGE) + error_message = "This is an error message" + instance.debug = True + instance.logfile = filename + instance.log_msg(error_message) - assert filename.read_text(encoding="UTF-8") == ERROR_MESSAGE + "\n" + assert filename.read_text(encoding="UTF-8") == error_message + "\n" assert len(list(tmp_path.iterdir())) == 1 @@ -453,12 +455,14 @@ def test_image_mgmt_image_upgrade_common_00112(tmp_path, image_upgrade_common) - To ensure an error is generated, we attempt a write to a filename that is too long for the target OS. """ + instance = image_upgrade_common + directory = tmp_path / "test_log_msg" directory.mkdir() filename = directory / f"test_{'a' * 2000}_log_msg.txt" - ERROR_MESSAGE = "This is an error message" - image_upgrade_common.debug = True - image_upgrade_common.logfile = filename + error_message = "This is an error message" + instance.debug = True + instance.logfile = filename with pytest.raises(AnsibleFailJson, match=r"error opening logfile"): - image_upgrade_common.log_msg(ERROR_MESSAGE) + instance.log_msg(error_message) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py similarity index 98% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 91e151acf..e32cf99ac 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_ImageValidate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -27,6 +27,7 @@ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture +from .image_upgrade_utils import MockAnsibleModule __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -49,20 +50,6 @@ def response_data_issu_details(key: str) -> Dict[str, str]: return response -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - @pytest.fixture def module(): return ImageValidate(MockAnsibleModule) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py similarity index 97% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index ab4dae080..3b12e1885 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchDetails.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -24,6 +24,7 @@ SwitchDetails from .fixture import load_fixture +from .image_upgrade_utils import MockAnsibleModule __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -55,20 +56,6 @@ def responses_switch_details(key: str) -> Dict[str, str]: return response -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - @pytest.fixture def switch_details(): return SwitchDetails(MockAnsibleModule) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py similarity index 62% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index d9e50b34d..b669bfb0c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByDeviceName.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -20,10 +20,9 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ - SwitchIssuDetailsByDeviceName from .fixture import load_fixture +from .image_upgrade_utils import MockAnsibleModule, does_not_raise, issu_details_by_device_name __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -33,35 +32,12 @@ description: Verify functionality of subclass SwitchIssuDetailsByDeviceName """ - -@contextmanager -def does_not_raise(): - """ - A context manager that does not raise an exception. - """ - yield - - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - def responses_switch_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -69,58 +45,58 @@ def responses_switch_issu_details(key: str) -> Dict[str, str]: return response -@pytest.fixture -def issu_details(): - return SwitchIssuDetailsByDeviceName(MockAnsibleModule) +# @pytest.fixture +# def issu_details(): +# return SwitchIssuDetailsByDeviceName(MockAnsibleModule) -def test_image_mgmt_switch_issu_details_by_device_name_00001(issu_details) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00001(issu_details_by_device_name) -> None: """ Function - __init__ Test - fail_json is not called - - issu_details.properties is a dict + - issu_details_by_device_name.properties is a dict """ with does_not_raise(): - issu_details.__init__(MockAnsibleModule) - assert isinstance(issu_details.properties, dict) + issu_details_by_device_name.__init__(MockAnsibleModule) + assert isinstance(issu_details_by_device_name.properties, dict) -def test_image_mgmt_switch_issu_details_by_device_name_00002(issu_details) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00002(issu_details_by_device_name) -> None: """ Function - _init_properties Test - Class properties initialized to expected values - - issu_details.properties is a dict - - issu_details.action_keys is a set + - issu_details_by_device_name.properties is a dict + - issu_details_by_device_name.action_keys is a set - action_keys contains expected values """ action_keys = {"imageStaged", "upgrade", "validated"} - issu_details._init_properties() - assert isinstance(issu_details.properties, dict) - assert isinstance(issu_details.properties.get("action_keys"), set) - assert issu_details.properties.get("action_keys") == action_keys - assert issu_details.properties.get("response_data") == None - assert issu_details.properties.get("response") == None - assert issu_details.properties.get("result") == None - assert issu_details.properties.get("device_name") == None + issu_details_by_device_name._init_properties() + assert isinstance(issu_details_by_device_name.properties, dict) + assert isinstance(issu_details_by_device_name.properties.get("action_keys"), set) + assert issu_details_by_device_name.properties.get("action_keys") == action_keys + assert issu_details_by_device_name.properties.get("response_data") == None + assert issu_details_by_device_name.properties.get("response") == None + assert issu_details_by_device_name.properties.get("result") == None + assert issu_details_by_device_name.properties.get("device_name") == None def test_image_mgmt_switch_issu_details_by_device_name_00020( - monkeypatch, issu_details + monkeypatch, issu_details_by_device_name ) -> None: """ Function - refresh Test - - issu_details.response is a dict - - issu_details.response_data is a list + - issu_details_by_device_name.response is a dict + - issu_details_by_device_name.response_data is a list """ key = "test_image_mgmt_switch_issu_details_by_device_name_00020a" @@ -130,13 +106,13 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - assert isinstance(issu_details.response, dict) - assert isinstance(issu_details.response_data, list) + issu_details_by_device_name.refresh() + assert isinstance(issu_details_by_device_name.response, dict) + assert isinstance(issu_details_by_device_name.response_data, list) def test_image_mgmt_switch_issu_details_by_device_name_00021( - monkeypatch, issu_details + monkeypatch, issu_details_by_device_name ) -> None: """ Function @@ -154,75 +130,75 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.device_name = "leaf1" - assert issu_details.device_name == "leaf1" - assert issu_details.serial_number == "FDO21120U5D" + issu_details_by_device_name.refresh() + issu_details_by_device_name.device_name = "leaf1" + assert issu_details_by_device_name.device_name == "leaf1" + assert issu_details_by_device_name.serial_number == "FDO21120U5D" # change device_name to a different switch, expect different information - issu_details.device_name = "cvd-2313-leaf" - assert issu_details.device_name == "cvd-2313-leaf" - assert issu_details.serial_number == "FDO2112189M" + issu_details_by_device_name.device_name = "cvd-2313-leaf" + assert issu_details_by_device_name.device_name == "cvd-2313-leaf" + assert issu_details_by_device_name.serial_number == "FDO2112189M" # verify remaining properties using current device_name - assert issu_details.eth_switch_id == 39890 - assert issu_details.fabric == "hard" - assert issu_details.fcoe_enabled is False - assert issu_details.group == "hard" + assert issu_details_by_device_name.eth_switch_id == 39890 + assert issu_details_by_device_name.fabric == "hard" + assert issu_details_by_device_name.fcoe_enabled is False + assert issu_details_by_device_name.group == "hard" # NOTE: For "id" see switch_id below - assert issu_details.image_staged == "Success" - assert issu_details.image_staged_percent == 100 - assert issu_details.ip_address == "172.22.150.108" - assert issu_details.issu_allowed == None - assert issu_details.last_upg_action == "2023-Oct-06 03:43" - assert issu_details.mds is False - assert issu_details.mode == "Normal" - assert issu_details.model == "N9K-C93180YC-EX" - assert issu_details.model_type == 0 - assert issu_details.peer == None - assert issu_details.platform == "N9K" - assert issu_details.policy == "KR5M" - assert issu_details.reason == "Upgrade" - assert issu_details.role == "leaf" - assert issu_details.status == "In-Sync" - assert issu_details.status_percent == 100 + assert issu_details_by_device_name.image_staged == "Success" + assert issu_details_by_device_name.image_staged_percent == 100 + assert issu_details_by_device_name.ip_address == "172.22.150.108" + assert issu_details_by_device_name.issu_allowed == None + assert issu_details_by_device_name.last_upg_action == "2023-Oct-06 03:43" + assert issu_details_by_device_name.mds is False + assert issu_details_by_device_name.mode == "Normal" + assert issu_details_by_device_name.model == "N9K-C93180YC-EX" + assert issu_details_by_device_name.model_type == 0 + assert issu_details_by_device_name.peer == None + assert issu_details_by_device_name.platform == "N9K" + assert issu_details_by_device_name.policy == "KR5M" + assert issu_details_by_device_name.reason == "Upgrade" + assert issu_details_by_device_name.role == "leaf" + assert issu_details_by_device_name.status == "In-Sync" + assert issu_details_by_device_name.status_percent == 100 # NOTE: switch_id appears in the response data as "id" # NOTE: "id" is a python reserved keyword, so we changed the property name - assert issu_details.switch_id == 2 - assert issu_details.sys_name == "cvd-2313-leaf" - assert issu_details.system_mode == "Normal" - assert issu_details.upg_groups == None - assert issu_details.upgrade == "Success" - assert issu_details.upgrade_percent == 100 - assert issu_details.validated == "Success" - assert issu_details.validated_percent == 100 - assert issu_details.version == "10.2(5)" + assert issu_details_by_device_name.switch_id == 2 + assert issu_details_by_device_name.sys_name == "cvd-2313-leaf" + assert issu_details_by_device_name.system_mode == "Normal" + assert issu_details_by_device_name.upg_groups == None + assert issu_details_by_device_name.upgrade == "Success" + assert issu_details_by_device_name.upgrade_percent == 100 + assert issu_details_by_device_name.validated == "Success" + assert issu_details_by_device_name.validated_percent == 100 + assert issu_details_by_device_name.version == "10.2(5)" # NOTE: Two vdc_id values exist in the response data for each switch. # NOTE: Namely, "vdcId" and "vdc_id" # NOTE: Properties are provided for both, as follows. # NOTE: vdc_id == vdcId # NOTE: vdc_id2 == vdc_id - assert issu_details.vdc_id == 0 - assert issu_details.vdc_id2 == -1 - assert issu_details.vpc_peer == None + assert issu_details_by_device_name.vdc_id == 0 + assert issu_details_by_device_name.vdc_id2 == -1 + assert issu_details_by_device_name.vpc_peer == None # NOTE: Two vpc role keys exist in the response data for each switch. # NOTE: Namely, "vpcRole" and "vpc_role" # NOTE: Properties are provided for both, as follows. # NOTE: vpc_role == vpcRole # NOTE: vpc_role2 == vpc_role # NOTE: Values are synthesized in the response for this test - assert issu_details.vpc_role == "FOO" - assert issu_details.vpc_role2 == "BAR" + assert issu_details_by_device_name.vpc_role == "FOO" + assert issu_details_by_device_name.vpc_role2 == "BAR" def test_image_mgmt_switch_issu_details_by_device_name_00022( - monkeypatch, issu_details + monkeypatch, issu_details_by_device_name ) -> None: """ Function - refresh Test - - issu_details.result is a dict - - issu_details.result contains expected key/values for 200 RESULT_CODE + - issu_details_by_device_name.result is a dict + - issu_details_by_device_name.result contains expected key/values for 200 RESULT_CODE """ key = "test_image_mgmt_switch_issu_details_by_device_name_00022a" @@ -232,14 +208,14 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - assert isinstance(issu_details.result, dict) - assert issu_details.result.get("found") is True - assert issu_details.result.get("success") is True + issu_details_by_device_name.refresh() + assert isinstance(issu_details_by_device_name.result, dict) + assert issu_details_by_device_name.result.get("found") is True + assert issu_details_by_device_name.result.get("success") is True def test_image_mgmt_switch_issu_details_by_device_name_00023( - monkeypatch, issu_details + monkeypatch, issu_details_by_device_name ) -> None: """ Function @@ -258,11 +234,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match = "Bad result when retriving switch information from the controller" with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + issu_details_by_device_name.refresh() def test_image_mgmt_switch_issu_details_by_device_name_00024( - monkeypatch, issu_details + monkeypatch, issu_details_by_device_name ) -> None: """ Function @@ -282,11 +258,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match = "SwitchIssuDetailsByDeviceName.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + issu_details_by_device_name.refresh() def test_image_mgmt_switch_issu_details_by_device_name_00025( - monkeypatch, issu_details + monkeypatch, issu_details_by_device_name ) -> None: """ Function @@ -307,11 +283,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match = "SwitchIssuDetailsByDeviceName.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + issu_details_by_device_name.refresh() def test_image_mgmt_switch_issu_details_by_device_name_00040( - monkeypatch, issu_details + monkeypatch, issu_details_by_device_name ) -> None: """ Function @@ -334,16 +310,16 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.device_name = "FOO" + issu_details_by_device_name.refresh() + issu_details_by_device_name.device_name = "FOO" match = "SwitchIssuDetailsByDeviceName._get: FOO does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): - issu_details._get("serialNumber") + issu_details_by_device_name._get("serialNumber") def test_image_mgmt_switch_issu_details_by_device_name_00041( - monkeypatch, issu_details + monkeypatch, issu_details_by_device_name ) -> None: """ Function @@ -367,9 +343,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.device_name = "leaf1" + issu_details_by_device_name.refresh() + issu_details_by_device_name.device_name = "leaf1" match = "SwitchIssuDetailsByDeviceName._get: leaf1 unknown " match += f"property name: FOO" with pytest.raises(AnsibleFailJson, match=match): - issu_details._get("FOO") + issu_details_by_device_name._get("FOO") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py similarity index 63% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 26e4c887b..6d5321fa1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsByIpAddress.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -14,16 +14,15 @@ from __future__ import absolute_import, division, print_function -from contextlib import contextmanager from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ - SwitchIssuDetailsByIpAddress from .fixture import load_fixture +from .image_upgrade_utils import MockAnsibleModule, does_not_raise, issu_details_by_ip_address + __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -34,34 +33,12 @@ """ -@contextmanager -def does_not_raise(): - """ - A context manager that does not raise an exception. - """ - yield - - patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." patch_image_mgmt = patch_module_utils + "image_mgmt." dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - - def responses_switch_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" response = load_fixture(response_file).get(key) @@ -69,58 +46,53 @@ def responses_switch_issu_details(key: str) -> Dict[str, str]: return response -@pytest.fixture -def issu_details(): - return SwitchIssuDetailsByIpAddress(MockAnsibleModule) - - -def test_image_mgmt_switch_issu_details_by_ip_address_00001(issu_details) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00001(issu_details_by_ip_address) -> None: """ Function - __init__ Test - fail_json is not called - - issu_details.properties is a dict + - issu_details_by_ip_address.properties is a dict """ with does_not_raise(): - issu_details.__init__(MockAnsibleModule) - assert isinstance(issu_details.properties, dict) + issu_details_by_ip_address.__init__(MockAnsibleModule) + assert isinstance(issu_details_by_ip_address.properties, dict) -def test_image_mgmt_switch_issu_details_by_ip_address_00002(issu_details) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00002(issu_details_by_ip_address) -> None: """ Function - _init_properties Test - Class properties initialized to expected values - - issu_details.properties is a dict - - issu_details.action_keys is a set + - issu_details_by_ip_address.properties is a dict + - issu_details_by_ip_address.action_keys is a set - action_keys contains expected values """ action_keys = {"imageStaged", "upgrade", "validated"} - issu_details._init_properties() - assert isinstance(issu_details.properties, dict) - assert isinstance(issu_details.properties.get("action_keys"), set) - assert issu_details.properties.get("action_keys") == action_keys - assert issu_details.properties.get("response_data") == None - assert issu_details.properties.get("response") == None - assert issu_details.properties.get("result") == None - assert issu_details.properties.get("ip_address") == None + issu_details_by_ip_address._init_properties() + assert isinstance(issu_details_by_ip_address.properties, dict) + assert isinstance(issu_details_by_ip_address.properties.get("action_keys"), set) + assert issu_details_by_ip_address.properties.get("action_keys") == action_keys + assert issu_details_by_ip_address.properties.get("response_data") is None + assert issu_details_by_ip_address.properties.get("response") is None + assert issu_details_by_ip_address.properties.get("result") is None + assert issu_details_by_ip_address.properties.get("ip_address") is None def test_image_mgmt_switch_issu_details_by_ip_address_00020( - monkeypatch, issu_details + monkeypatch, issu_details_by_ip_address ) -> None: """ Function - refresh Test - - issu_details.response is a dict - - issu_details.response_data is a list + - issu_details_by_ip_address.response is a dict + - issu_details_by_ip_address.response_data is a list """ key = "test_image_mgmt_switch_issu_details_by_ip_address_00020a" @@ -130,13 +102,13 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - assert isinstance(issu_details.response, dict) - assert isinstance(issu_details.response_data, list) + issu_details_by_ip_address.refresh() + assert isinstance(issu_details_by_ip_address.response, dict) + assert isinstance(issu_details_by_ip_address.response_data, list) def test_image_mgmt_switch_issu_details_by_ip_address_00021( - monkeypatch, issu_details + monkeypatch, issu_details_by_ip_address ) -> None: """ Function @@ -154,75 +126,75 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.ip_address = "172.22.150.102" - assert issu_details.device_name == "leaf1" - assert issu_details.serial_number == "FDO21120U5D" + issu_details_by_ip_address.refresh() + issu_details_by_ip_address.ip_address = "172.22.150.102" + assert issu_details_by_ip_address.device_name == "leaf1" + assert issu_details_by_ip_address.serial_number == "FDO21120U5D" # change ip_address to a different switch, expect different information - issu_details.ip_address = "172.22.150.108" - assert issu_details.device_name == "cvd-2313-leaf" - assert issu_details.serial_number == "FDO2112189M" + issu_details_by_ip_address.ip_address = "172.22.150.108" + assert issu_details_by_ip_address.device_name == "cvd-2313-leaf" + assert issu_details_by_ip_address.serial_number == "FDO2112189M" # verify remaining properties using current ip_address - assert issu_details.eth_switch_id == 39890 - assert issu_details.fabric == "hard" - assert issu_details.fcoe_enabled is False - assert issu_details.group == "hard" + assert issu_details_by_ip_address.eth_switch_id == 39890 + assert issu_details_by_ip_address.fabric == "hard" + assert issu_details_by_ip_address.fcoe_enabled is False + assert issu_details_by_ip_address.group == "hard" # NOTE: For "id" see switch_id below - assert issu_details.image_staged == "Success" - assert issu_details.image_staged_percent == 100 - assert issu_details.ip_address == "172.22.150.108" - assert issu_details.issu_allowed == None - assert issu_details.last_upg_action == "2023-Oct-06 03:43" - assert issu_details.mds is False - assert issu_details.mode == "Normal" - assert issu_details.model == "N9K-C93180YC-EX" - assert issu_details.model_type == 0 - assert issu_details.peer == None - assert issu_details.platform == "N9K" - assert issu_details.policy == "KR5M" - assert issu_details.reason == "Upgrade" - assert issu_details.role == "leaf" - assert issu_details.status == "In-Sync" - assert issu_details.status_percent == 100 + assert issu_details_by_ip_address.image_staged == "Success" + assert issu_details_by_ip_address.image_staged_percent == 100 + assert issu_details_by_ip_address.ip_address == "172.22.150.108" + assert issu_details_by_ip_address.issu_allowed is None + assert issu_details_by_ip_address.last_upg_action == "2023-Oct-06 03:43" + assert issu_details_by_ip_address.mds is False + assert issu_details_by_ip_address.mode == "Normal" + assert issu_details_by_ip_address.model == "N9K-C93180YC-EX" + assert issu_details_by_ip_address.model_type == 0 + assert issu_details_by_ip_address.peer is None + assert issu_details_by_ip_address.platform == "N9K" + assert issu_details_by_ip_address.policy == "KR5M" + assert issu_details_by_ip_address.reason == "Upgrade" + assert issu_details_by_ip_address.role == "leaf" + assert issu_details_by_ip_address.status == "In-Sync" + assert issu_details_by_ip_address.status_percent == 100 # NOTE: switch_id appears in the response data as "id" # NOTE: "id" is a python reserved keyword, so we changed the property name - assert issu_details.switch_id == 2 - assert issu_details.sys_name == "cvd-2313-leaf" - assert issu_details.system_mode == "Normal" - assert issu_details.upg_groups == None - assert issu_details.upgrade == "Success" - assert issu_details.upgrade_percent == 100 - assert issu_details.validated == "Success" - assert issu_details.validated_percent == 100 - assert issu_details.version == "10.2(5)" + assert issu_details_by_ip_address.switch_id == 2 + assert issu_details_by_ip_address.sys_name == "cvd-2313-leaf" + assert issu_details_by_ip_address.system_mode == "Normal" + assert issu_details_by_ip_address.upg_groups is None + assert issu_details_by_ip_address.upgrade == "Success" + assert issu_details_by_ip_address.upgrade_percent == 100 + assert issu_details_by_ip_address.validated == "Success" + assert issu_details_by_ip_address.validated_percent == 100 + assert issu_details_by_ip_address.version == "10.2(5)" # NOTE: Two vdc_id values exist in the response data for each switch. # NOTE: Namely, "vdcId" and "vdc_id" # NOTE: Properties are provided for both, as follows. # NOTE: vdc_id == vdcId # NOTE: vdc_id2 == vdc_id - assert issu_details.vdc_id == 0 - assert issu_details.vdc_id2 == -1 - assert issu_details.vpc_peer == None + assert issu_details_by_ip_address.vdc_id == 0 + assert issu_details_by_ip_address.vdc_id2 == -1 + assert issu_details_by_ip_address.vpc_peer is None # NOTE: Two vpc role keys exist in the response data for each switch. # NOTE: Namely, "vpcRole" and "vpc_role" # NOTE: Properties are provided for both, as follows. # NOTE: vpc_role == vpcRole # NOTE: vpc_role2 == vpc_role # NOTE: Values are synthesized in the response for this test - assert issu_details.vpc_role == "FOO" - assert issu_details.vpc_role2 == "BAR" + assert issu_details_by_ip_address.vpc_role == "FOO" + assert issu_details_by_ip_address.vpc_role2 == "BAR" def test_image_mgmt_switch_issu_details_by_ip_address_00022( - monkeypatch, issu_details + monkeypatch, issu_details_by_ip_address ) -> None: """ Function - refresh Test - - issu_details.result is a dict - - issu_details.result contains expected key/values for 200 RESULT_CODE + - issu_details_by_ip_address.result is a dict + - issu_details_by_ip_address.result contains expected key/values for 200 RESULT_CODE """ key = "test_image_mgmt_switch_issu_details_by_ip_address_00022a" @@ -232,14 +204,14 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - assert isinstance(issu_details.result, dict) - assert issu_details.result.get("found") is True - assert issu_details.result.get("success") is True + issu_details_by_ip_address.refresh() + assert isinstance(issu_details_by_ip_address.result, dict) + assert issu_details_by_ip_address.result.get("found") is True + assert issu_details_by_ip_address.result.get("success") is True def test_image_mgmt_switch_issu_details_by_ip_address_00023( - monkeypatch, issu_details + monkeypatch, issu_details_by_ip_address ) -> None: """ Function @@ -259,11 +231,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match = "Bad result when retriving switch information from the controller" with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + issu_details_by_ip_address.refresh() def test_image_mgmt_switch_issu_details_by_ip_address_00024( - monkeypatch, issu_details + monkeypatch, issu_details_by_ip_address ) -> None: """ Function @@ -283,11 +255,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match = "SwitchIssuDetailsByIpAddress.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + issu_details_by_ip_address.refresh() def test_image_mgmt_switch_issu_details_by_ip_address_00025( - monkeypatch, issu_details + monkeypatch, issu_details_by_ip_address ) -> None: """ Function @@ -308,11 +280,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match = "SwitchIssuDetailsByIpAddress.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + issu_details_by_ip_address.refresh() def test_image_mgmt_switch_issu_details_by_ip_address_00040( - monkeypatch, issu_details + monkeypatch, issu_details_by_ip_address ) -> None: """ Function description: @@ -335,16 +307,16 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.ip_address = "1.1.1.1" + issu_details_by_ip_address.refresh() + issu_details_by_ip_address.ip_address = "1.1.1.1" match = "SwitchIssuDetailsByIpAddress._get: 1.1.1.1 does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): - issu_details._get("serialNumber") + issu_details_by_ip_address._get("serialNumber") def test_image_mgmt_switch_issu_details_by_ip_address_00041( - monkeypatch, issu_details + monkeypatch, issu_details_by_ip_address ) -> None: """ Function description: @@ -367,9 +339,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.ip_address = "172.22.150.102" + issu_details_by_ip_address.refresh() + issu_details_by_ip_address.ip_address = "172.22.150.102" match = "SwitchIssuDetailsByIpAddress._get: 172.22.150.102 unknown " match += f"property name: FOO" with pytest.raises(AnsibleFailJson, match=match): - issu_details._get("FOO") + issu_details_by_ip_address._get("FOO") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py similarity index 98% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index e51f4da96..c19a327b1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_SwitchIssuDetailsBySerialNumber.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -24,6 +24,7 @@ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture +from .image_upgrade_utils import MockAnsibleModule __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -48,19 +49,6 @@ def does_not_raise(): dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" -class MockAnsibleModule: - """ - Mock the AnsibleModule class - """ - - params = {} - - def fail_json(msg) -> AnsibleFailJson: - """ - mock the fail_json method - """ - raise AnsibleFailJson(msg) - def responses_switch_issu_details(key: str) -> Dict[str, str]: response_file = f"image_upgrade_responses_SwitchIssuDetails" From f607842be3ca19af088ab6720205926eab11220a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 Nov 2023 18:46:20 -1000 Subject: [PATCH 125/300] Fix imports --- .../test_image_upgrade_switch_issu_details_by_device_name.py | 2 +- .../test_image_upgrade_switch_issu_details_by_ip_address.py | 2 +- .../test_image_upgrade_switch_issu_details_by_serial_number.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index b669bfb0c..f6b780d2b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -22,7 +22,7 @@ AnsibleFailJson from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule, does_not_raise, issu_details_by_device_name +from .image_upgrade_utils import MockAnsibleModule, does_not_raise, issu_details_by_device_name_fixture __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 6d5321fa1..65c6bb83d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -21,7 +21,7 @@ AnsibleFailJson from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule, does_not_raise, issu_details_by_ip_address +from .image_upgrade_utils import MockAnsibleModule, does_not_raise, issu_details_by_ip_address_fixture __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index c19a327b1..9ab020e55 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -24,7 +24,7 @@ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule +from .image_upgrade_utils import MockAnsibleModule, issu_details_by_serial_number_fixture __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" From 68ebfabb8f71985dc03f0a5b1f0d4f71a01c5ef1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 16 Nov 2023 16:02:12 -1000 Subject: [PATCH 126/300] Fixes, hardening, reduce unit test code duplication, more... - Consolidate fixtures and file loaders into image_upgrade_utils.py - Harden fixture.py - Fix EPLD idempotence issue - Fix merge of switch and global configs - Add initial unit tests for ImageUpgradeTask (to verify above fix for merging configs) - Modifications to appease pylint - More work toward passing Ansible sanity (more TODO though) - Temporarily add debugging statements throughout --- .../module_utils/common/controller_version.py | 5 +- .../module_utils/image_mgmt/api_endpoints.py | 7 +- .../module_utils/image_mgmt/image_policies.py | 9 +- .../image_mgmt/image_policy_action.py | 11 +- .../module_utils/image_mgmt/image_stage.py | 39 ++- .../module_utils/image_mgmt/image_upgrade.py | 196 +++++++---- .../image_mgmt/image_upgrade_common.py | 11 +- .../module_utils/image_mgmt/image_validate.py | 5 + .../image_mgmt/install_options.py | 14 +- .../module_utils/image_mgmt/switch_details.py | 5 + .../image_mgmt/switch_issu_details.py | 5 + plugins/modules/dcnm_image_upgrade.py | 274 ++++++++++++--- .../dcnm/dcnm_image_upgrade/fixture.py | 31 +- .../image_upgrade_playbook_configs.json | 124 +++++++ ...e_upgrade_responses_SwitchIssuDetails.json | 28 ++ .../dcnm_image_upgrade/image_upgrade_utils.py | 229 +++++++++++- .../test_image_upgrade_controller_version.py | 20 +- ...est_image_upgrade_image_install_options.py | 20 +- .../test_image_upgrade_image_policies.py | 23 +- .../test_image_upgrade_image_policy_action.py | 57 +-- .../test_image_upgrade_image_stage.py | 66 +--- .../test_image_upgrade_image_upgrade.py | 158 ++++----- ...test_image_upgrade_image_upgrade_common.py | 39 +-- ...pgrade_image_upgrade_image_upgrade_task.py | 328 ++++++++++++++++++ .../test_image_upgrade_image_validate.py | 307 ++++++++-------- .../test_image_upgrade_switch_details.py | 153 ++++---- ...rade_switch_issu_details_by_device_name.py | 229 ++++++------ ...grade_switch_issu_details_by_ip_address.py | 226 ++++++------ ...de_switch_issu_details_by_serial_number.py | 264 +++++++------- 29 files changed, 1865 insertions(+), 1018 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index a1b39e86d..c643e90c1 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -1,5 +1,8 @@ from __future__ import (absolute_import, division, print_function) -__metaclass__ = type + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, ) diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py index 37f45438f..322be540d 100644 --- a/plugins/module_utils/image_mgmt/api_endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -1,8 +1,11 @@ """ Endpoints for image management API calls """ -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + class ApiEndpoints: """ diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 7462922f7..61a200bfc 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -2,6 +2,11 @@ Retrieve image policy details from the controller and provide property accessors for the policy attributes. """ +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + import inspect from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -38,12 +43,12 @@ class ImagePolicies(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] + self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): - self.method_name = inspect.stack()[0][3] + self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties = {} self.properties["policy_name"] = None self.properties["response_data"] = None diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index e66184ead..d1ec6652d 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -1,6 +1,11 @@ """ Perform image policy actions on the controller for one or more switches. """ +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + import inspect import json @@ -47,7 +52,7 @@ class ImagePolicyAction(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() self._init_properties() self.image_policies = ImagePolicies(self.module) @@ -58,7 +63,7 @@ def __init__(self, module): self.verb = None def _init_properties(self): - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties = {} self.properties["action"] = None self.properties["response"] = None @@ -317,7 +322,7 @@ def policy_name(self): @policy_name.setter def policy_name(self, value): - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties["policy_name"] = value @property diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index fe0043cac..c56a7baed 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -1,3 +1,11 @@ +""" +ImageStage - Methods to stage images to NX-OS switches +""" +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + import copy import inspect import json @@ -47,11 +55,11 @@ class ImageStage(ImageUpgradeCommon): Response: Unfortunately, the response does not contain consistent data. Would be better if all responses contained serial numbers as keys so that - we could verify against a set() of serial numbers. Sigh. It is what it is. + we could verify against a set() of serial numbers. { 'RETURN_CODE': 200, 'METHOD': 'POST', - 'REQUEST_PATH': 'https: //172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image', + 'REQUEST_PATH': '.../api/v1/imagemanagement/rest/stagingmanagement/stage-image', 'MESSAGE': 'OK', 'DATA': [ { @@ -81,7 +89,7 @@ class ImageStage(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() self._init_properties() self.serial_numbers_done = set() @@ -90,9 +98,10 @@ def __init__(self, module): self.verb = None self.payload = None self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) + self.log_msg("DEBUG: ImageStage.__init__ DONE") def _init_properties(self): - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties = {} self.properties["serial_numbers"] = None self.properties["response_data"] = None @@ -109,7 +118,7 @@ def _populate_controller_version(self): 1. This cannot go into ImageUpgradeCommon() due to circular imports resulting in RecursionError """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable instance = ControllerVersion(self.module) instance.refresh() self.controller_version = instance.version @@ -119,7 +128,7 @@ def prune_serial_numbers(self): If the image is already staged on a switch, remove that switch's serial number from the list of serial numbers to stage. """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: self.issu_detail.serial_number = serial_number @@ -143,7 +152,7 @@ def validate_serial_numbers(self): msg += f"{self.issu_detail.device_name}, " msg += f"{self.issu_detail.ip_address}, " msg += f"{self.issu_detail.serial_number}. " - msg += f"Check the switch connectivity to the controller " + msg += "Check the switch connectivity to the controller " msg += "and try again." self.module.fail_json(msg) @@ -221,10 +230,10 @@ def _wait_for_current_actions_to_complete(self): if self.serial_numbers_done != serial_numbers_todo: msg = f"{self.class_name}.{method_name}: " - msg += f"Timed out waiting for actions to complete. " - msg += f"serial_numbers_done: " + msg += "Timed out waiting for actions to complete. " + msg += "serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " + msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" self.module.fail_json(msg) @@ -265,10 +274,10 @@ def _wait_for_image_stage_to_complete(self): if self.serial_numbers_done != serial_numbers_todo: msg = f"{self.class_name}.{method_name}: " - msg += f"Timed out waiting for image stage to complete. " - msg += f"serial_numbers_done: " + msg += "Timed out waiting for image stage to complete. " + msg += "serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " + msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" self.module.fail_json(msg) @@ -326,7 +335,7 @@ def check_interval(self): def check_interval(self, value): if not isinstance(value, int): msg = f"{self.__class__.__name__}: instance.check_interval must " - msg += f"be an integer." + msg += "be an integer." self.module.fail_json(msg) self.properties["check_interval"] = value @@ -341,6 +350,6 @@ def check_timeout(self): def check_timeout(self, value): if not isinstance(value, int): msg = f"{self.__class__.__name__}: instance.check_timeout must " - msg += f"be an integer." + msg += "be an integer." self.module.fail_json(msg) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index fbbe6faa1..47a281841 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -1,8 +1,16 @@ +""" +ImageUpgrade - Methods to upgrade images on NX-OS switches +""" +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + import copy import inspect import json from time import sleep -from typing import Any, Dict, List, Set, Union +from typing import Any, Dict, List, Set from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -104,7 +112,8 @@ class ImageUpgrade(ImageUpgradeCommon): to determine when the upgrade is complete. Basically, we ignore these responses in favor of the poll responses. - If an action is in progress, text is returned: - "Action in progress for some of selected device(s). Please try again after completing current action." + "Action in progress for some of selected device(s). + Please try again after completing current action." - If an action is not in progress, text is returned: "3" """ @@ -112,7 +121,7 @@ class ImageUpgrade(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() @@ -120,33 +129,34 @@ def __init__(self, module): self._init_properties() self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) self.install_options = ImageInstallOptions(self.module) + self.log_msg("DEBUG: ImageUpgrade.__init__ DONE") def _init_defaults(self) -> None: - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.defaults: Dict[str, Any] = {} - self.defaults["reboot"] = False - self.defaults["stage"] = True - self.defaults["validate"] = True - self.defaults["upgrade"] = {} - self.defaults["upgrade"]["nxos"] = True - self.defaults["upgrade"]["epld"] = False self.defaults["options"] = {} - self.defaults["options"]["nxos"] = {} - self.defaults["options"]["nxos"]["mode"] = "disruptive" - self.defaults["options"]["nxos"]["bios_force"] = False self.defaults["options"]["epld"] = {} self.defaults["options"]["epld"]["module"] = "ALL" self.defaults["options"]["epld"]["golden"] = False - self.defaults["options"]["reboot"] = {} - self.defaults["options"]["reboot"]["config_reload"] = False - self.defaults["options"]["reboot"]["write_erase"] = False + self.defaults["options"]["nxos"] = {} + self.defaults["options"]["nxos"]["mode"] = "disruptive" + self.defaults["options"]["nxos"]["bios_force"] = False self.defaults["options"]["package"] = {} self.defaults["options"]["package"]["install"] = False self.defaults["options"]["package"]["uninstall"] = False + self.defaults["options"]["reboot"] = {} + self.defaults["options"]["reboot"]["config_reload"] = False + self.defaults["options"]["reboot"]["write_erase"] = False + self.defaults["reboot"] = False + self.defaults["stage"] = True + self.defaults["upgrade"] = {} + self.defaults["upgrade"]["epld"] = False + self.defaults["upgrade"]["nxos"] = True + self.defaults["validate"] = True def _init_properties(self) -> None: - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable # self.ip_addresses is used in: # self._wait_for_current_actions_to_complete() @@ -219,7 +229,7 @@ def validate_devices(self) -> None: switches which can be upgraded. This is used in _wait_for_current_actions_to_complete """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable for device in self.devices: self.issu_detail.ip_address = device.get("ip_address") @@ -236,7 +246,7 @@ def validate_devices(self) -> None: self.ip_addresses.add(str(self.issu_detail.ip_address)) def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable if config.get("stage") is None: config["stage"] = self.defaults["stage"] @@ -298,54 +308,31 @@ def build_payload(self, device) -> None: """ method_name = inspect.stack()[0][3] - msg = f"REMOVE: {self.class_name}.{method_name}: " - msg += f"device PRE : {json.dumps(device, indent=4, sort_keys=True)}" + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"device PRE_DEFAULTS : {json.dumps(device, indent=4, sort_keys=True)}" self.log_msg(msg) device = self._merge_defaults_to_switch_config(device) - msg = f"REMOVE: {self.class_name}.{method_name}: " - msg += f"device POST: {json.dumps(device, indent=4, sort_keys=True)}" + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"device POST_DEFAULTS: {json.dumps(device, indent=4, sort_keys=True)}" self.log_msg(msg) - # device.upgrade - nxos_upgrade = device.get("upgrade").get("nxos") - nxos_upgrade = self.make_boolean(nxos_upgrade) - if not isinstance(nxos_upgrade, bool): - msg = f"{self.class_name}.{method_name}: " - msg += "upgrade.nxos must be a boolean. " - msg += f"Got {nxos_upgrade}." - self.module.fail_json(msg) - - epld_upgrade = device.get("upgrade").get("epld") - epld_upgrade = self.make_boolean(epld_upgrade) - if not isinstance(epld_upgrade, bool): - msg = f"{self.class_name}.{method_name}: " - msg += "upgrade.epld must be a boolean. " - msg += f"Got {epld_upgrade}." - self.module.fail_json(msg) - # TODO:2 Validate ip_address self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() - msg = f"REMOVE: {self.class_name}.{method_name}: " - msg += f"issu_detail.response: {json.dumps(self.issu_detail.response, indent=4, sort_keys=True)}" - self.log_msg(msg) - self.install_options.serial_number = self.issu_detail.serial_number - self.install_options.policy_name = device.get("policy") - self.install_options.epld = device.get("upgrade").get("epld") - self.install_options.nxos = device.get("upgrade").get("nxos") + # install_options will fail_json if any of these are invalid + # so no need to validate these here. + self.install_options.policy_name = device.get("policy", None) + self.install_options.epld = device.get("upgrade", {}).get("epld", None) + self.install_options.nxos = device.get("upgrade", {}).get("nxos", None) self.install_options.package_install = ( - device.get("options").get("package").get("install") + device.get("options", {}).get("package", {}).get("install", None) ) self.install_options.refresh() - msg = f"REMOVE: {self.class_name}.{method_name}: " - msg += f"install_options.response: {json.dumps(self.install_options.response, indent=4, sort_keys=True)}" - self.log_msg(msg) - # devices_to_upgrade must currently be a single device devices_to_upgrade: List[dict] = [] @@ -356,7 +343,36 @@ def build_payload(self, device) -> None: self.payload: Dict[str, Any] = {} self.payload["devices"] = devices_to_upgrade - self.payload["issuUpgrade"] = device.get("upgrade").get("nxos") + + self.build_payload_issu_upgrade(device) + self.build_payload_issu_options_1(device) + self.build_payload_issu_options_2(device) + self.build_payload_epld(device) + self.build_payload_reboot(device) + self.build_payload_reboot_options(device) + self.build_payload_package(device) + + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log_msg(msg) + + def build_payload_issu_upgrade(self, device) -> None: + """ + Build the issuUpgrade portion of the payload. + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + nxos_upgrade = device.get("upgrade").get("nxos") + nxos_upgrade = self.make_boolean(nxos_upgrade) + if not isinstance(nxos_upgrade, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "upgrade.nxos must be a boolean. " + msg += f"Got {nxos_upgrade}." + self.module.fail_json(msg) + self.payload["issuUpgrade"] = nxos_upgrade + + def build_payload_issu_options_1(self, device) -> None: + method_name = inspect.stack()[0][3] # nxos_mode: The choices for nxos_mode are mutually-exclusive. # If one is set to True, the others must be False. @@ -385,7 +401,9 @@ def build_payload(self, device) -> None: verify_nxos_mode_list.append(True) self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True - # biosForce corresponds to BIOS Force GUI option + def build_payload_issu_options_2(self, device) -> None: + method_name = inspect.stack()[0][3] + bios_force = device.get("options").get("nxos").get("bios_force") bios_force = self.make_boolean(bios_force) if not isinstance(bios_force, bool): @@ -397,7 +415,20 @@ def build_payload(self, device) -> None: self.payload["issuUpgradeOptions2"] = {} self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force - # EPLD + def build_payload_epld(self, device) -> None: + """ + Build the epldUpgrade and epldOptions portions of the payload. + """ + method_name = inspect.stack()[0][3] + + epld_upgrade = device.get("upgrade").get("epld") + epld_upgrade = self.make_boolean(epld_upgrade) + if not isinstance(epld_upgrade, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "upgrade.epld must be a boolean. " + msg += f"Got {epld_upgrade}." + self.module.fail_json(msg) + epld_module = device.get("options").get("epld").get("module") epld_golden = device.get("options").get("epld").get("golden") @@ -420,7 +451,7 @@ def build_payload(self, device) -> None: if epld_module != "ALL": try: epld_module = int(epld_module) - except: + except ValueError: msg = f"{self.class_name}.{method_name}: " msg += "options.epld.module must either be 'ALL' " msg += f"or an integer. Got {epld_module}." @@ -431,7 +462,11 @@ def build_payload(self, device) -> None: self.payload["epldOptions"]["moduleNumber"] = epld_module self.payload["epldOptions"]["golden"] = epld_golden - # Reboot + def build_payload_reboot(self, device) -> None: + """ + Build the reboot portion of the payload. + """ + method_name = inspect.stack()[0][3] reboot = device.get("reboot") reboot = self.make_boolean(reboot) @@ -442,7 +477,12 @@ def build_payload(self, device) -> None: self.module.fail_json(msg) self.payload["reboot"] = reboot - # Reboot options + def build_payload_reboot_options(self, device) -> None: + """ + Build the rebootOptions portion of the payload. + """ + method_name = inspect.stack()[0][3] + config_reload = device.get("options").get("reboot").get("config_reload") write_erase = device.get("options").get("reboot").get("write_erase") @@ -464,7 +504,13 @@ def build_payload(self, device) -> None: self.payload["rebootOptions"]["configReload"] = config_reload self.payload["rebootOptions"]["writeErase"] = write_erase - # Packages + + def build_payload_package(self, device) -> None: + """ + Build the packageInstall and packageUnInstall portions of the payload. + """ + method_name = inspect.stack()[0][3] + package_install = device.get("options").get("package").get("install") package_uninstall = device.get("options").get("package").get("uninstall") @@ -489,10 +535,6 @@ def build_payload(self, device) -> None: self.payload["pacakgeInstall"] = package_install self.payload["pacakgeUnInstall"] = package_uninstall - msg = f"REMOVE: {self.class_name}.{method_name}: " - msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" - self.log_msg(msg) - def commit(self) -> None: """ Commit the image upgrade request to the controller and wait @@ -500,8 +542,8 @@ def commit(self) -> None: """ method_name = inspect.stack()[0][3] - msg = f"REMOVE: {self.class_name}.{method_name}: " - msg += f"self.devices: {self.devices}" + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.devices: {json.dumps(self.devices, indent=4, sort_keys=True)}" self.log_msg(msg) if self.devices is None: @@ -515,18 +557,18 @@ def commit(self) -> None: self.path: str = self.endpoints.image_upgrade.get("path") self.verb: str = self.endpoints.image_upgrade.get("verb") - msg = f"REMOVE: {self.class_name}.{method_name}: " + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"self.verb {self.verb}, self.path: {self.path}" self.log_msg(msg) for device in self.devices: - msg = f"REMOVE: {self.class_name}.{method_name}: " + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"device: {json.dumps(device, indent=4, sort_keys=True)}" self.log_msg(msg) self.build_payload(device) - msg = f"REMOVE: {self.class_name}.{method_name}: " + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log_msg(msg) @@ -535,8 +577,8 @@ def commit(self) -> None: ) self.properties["result"] = self._handle_response(self.response, self.verb) - msg = f"REMOVE: {self.class_name}.{method_name}: " - msg += f"self.response: {self.response}" + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" self.log_msg(msg) if not self.result["success"]: @@ -577,10 +619,10 @@ def _wait_for_current_actions_to_complete(self): if self.ipv4_done != self.ipv4_todo: msg = f"{self.class_name}.{method_name}: " - msg += f"Timed out waiting for actions to complete. " - msg += f"ipv4_done: " + msg += "Timed out waiting for actions to complete. " + msg += "ipv4_done: " msg += f"{','.join(sorted(self.ipv4_done))}, " - msg += f"ipv4_todo: " + msg += "ipv4_todo: " msg += f"{','.join(sorted(self.ipv4_todo))}. " msg += "check the device(s) to determine the cause " msg += "(e.g. show install all status)." @@ -782,11 +824,11 @@ def epld_module(self, value): pass try: value = int(value) - except: + except ValueError: pass if not isinstance(value, int) and value != "ALL": msg = f"{self.class_name}.{method_name}: " - msg += f"instance.epld_module must be an integer or 'ALL'" + msg += "instance.epld_module must be an integer or 'ALL'" self.module.fail_json(msg) self.properties["epld_module"] = value @@ -909,7 +951,7 @@ def check_interval(self): def check_interval(self, value): if not isinstance(value, int): msg = f"{self.__class__.__name__}: instance.check_interval must " - msg += f"be an integer." + msg += "be an integer." self.module.fail_json(msg) self.properties["check_interval"] = value @@ -924,7 +966,7 @@ def check_timeout(self): def check_timeout(self, value): if not isinstance(value, int): msg = f"{self.__class__.__name__}: instance.check_timeout must " - msg += f"be an integer." + msg += "be an integer." self.module.fail_json(msg) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 75c4f8e6a..320798207 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -1,8 +1,10 @@ """ Base class for the other image upgrade classes """ -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name import inspect @@ -29,6 +31,7 @@ def __init__(self, module): self.fd = None self.logfile = "/tmp/ansible_dcnm.log" self.module = module + self.log_msg("ImageUpgradeCommon.__init__ DONE") def _handle_response(self, response, verb): # don't add self.method_name to this method since @@ -126,7 +129,9 @@ def log_msg(self, msg): return if self.fd is None: try: - self.fd = open(f"{self.logfile}", "a+", encoding="UTF-8") + # since we need self.fd open throughout several classes + # we are disabling pylint R1732 + self.fd = open(f"{self.logfile}", "a+", encoding="UTF-8") # pylint: disable=consider-using-with except IOError as err: msg = f"error opening logfile {self.logfile}. " msg += f"detail: {err}" diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 690b0e6e1..f19021317 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -1,3 +1,8 @@ +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + import copy import inspect import json diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 77b4313a7..f838b4444 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -1,3 +1,8 @@ +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + import inspect import json from time import sleep @@ -163,7 +168,6 @@ def refresh(self) -> None: self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) - self.properties["response_data"] = self.response.get("DATA", {}) self.properties["result"] = self._handle_response(self.response, self.verb) @@ -171,6 +175,8 @@ def refresh(self) -> None: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retrieving install-options from " msg += f"the controller. Controller response: {self.response}. " + if self.response_data.get("error", None) is None: + self.module.fail_json(msg) if "does not have package to continue" in self.response_data.get( "error", "" ): @@ -179,8 +185,6 @@ def refresh(self) -> None: msg += f"True in the playbook for device {self.serial_number}." self.module.fail_json(msg) - self.properties["response_data"] = self.response.get("DATA", {}) - if self.response_data.get("compatibilityStatusList") is None: self.compatibility_status = {} else: @@ -228,6 +232,10 @@ def policy_name(self): @policy_name.setter def policy_name(self, value): + if not isinstance(value, str): + msg = f"{self.class_name}.policy_name.setter: " + msg += f"policy_name must be a string. Got {value}." + self.module.fail_json(msg) self.properties["policy_name"] = value @property diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index 28c040b2a..2ade8a7d0 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -1,3 +1,8 @@ +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + import inspect from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 38b265350..c7857bdd6 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -1,3 +1,8 @@ +from __future__ import absolute_import, division, print_function + +# disabling pylint invalid-name for Ansible standard boilerplate +__metaclass__ = type # pylint: disable=invalid-name + import inspect from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 5c7ae304e..06d4177a3 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -27,6 +27,7 @@ import copy import inspect import json +from collections.abc import MutableMapping as Map from typing import Any, Dict, List from ansible.module_utils.basic import AnsibleModule @@ -53,7 +54,7 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, validate_list_of_dicts) -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -450,6 +451,10 @@ def __init__(self, module): method_name = inspect.stack()[0][3] self.params = self.module.params + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.params: {json.dumps(self.params, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.endpoints = ApiEndpoints() self.have = None self.idempotent_want = None @@ -464,6 +469,10 @@ def __init__(self, module): self.config = module.params.get("config", {}) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.config: {json.dumps(self.config, indent=4, sort_keys=True)}" + self.log_msg(msg) + if not isinstance(self.config, dict): msg = f"{self.class_name}.{method_name}: " msg += "expected dict type for self.config. " @@ -488,7 +497,13 @@ def __init__(self, module): msg += f"got {self.config.keys()}" self.module.fail_json(msg) - if self.config["switches"] is None: + if not isinstance(self.config["switches"], list): + msg = f"{self.class_name}.{method_name}: " + msg += "expected list type for self.config['switches']. " + msg += f"got {type(self.config['switches']).__name__}" + self.module.fail_json(msg) + + if len(self.config["switches"]) == 0: msg = f"{self.class_name}.{method_name}: " msg += "missing list of switches in playbook config." self.module.fail_json(msg) @@ -510,7 +525,7 @@ def get_have(self) -> None: Determine current switch ISSU state on NDFC """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.have = SwitchIssuDetailsByIpAddress(self.module) self.have.refresh() @@ -523,13 +538,28 @@ def get_want(self) -> None: """ method_name = inspect.stack()[0][3] + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += "Calling _merge_global_and_switch_configs with " + msg += f"self.config: {json.dumps(self.config, indent=4, sort_keys=True)}" + self.log_msg(msg) + self._merge_global_and_switch_configs(self.config) + + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += "Calling _validate_switch_configs with self.switch_configs: " + msg += f"{json.dumps(self.switch_configs, indent=4, sort_keys=True)}" + self.log_msg(msg) + self._validate_switch_configs() if not self.switch_configs: return self.want = self.switch_configs + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.want: {json.dumps(self.want, indent=4, sort_keys=True)}" + self.log_msg(msg) + def _build_idempotent_want(self, want) -> None: """ Build an itempotent want item based on the have item contents. @@ -566,23 +596,33 @@ def _build_idempotent_want(self, want) -> None: and the information returned by ImageInstallOptions. Caller: self.get_need_merged() - - NOTES: - 1. There doesn't appear to be an API to query the controller for - the current EPLD version. Hence, currently, EPLD handling - is not idempotent. """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.have.ip_address = want["ip_address"] + # msg = f"DEBUG: {self.class_name}.{method_name}: " + # msg += f"self.have.ip_address: {self.have.ip_address}" + # self.log_msg(msg) + want["policy_changed"] = True # The switch does not have an image policy attached. # idempotent_want == want with policy_changed = True if self.have.serial_number is None: self.idempotent_want = copy.deepcopy(want) + # msg = f"DEBUG: {self.class_name}.{method_name}: " + # msg += "returning due to serial_number is None. " + # msg += "self.idempotent_want: " + # msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" + # self.log_msg(msg) return + # msg = f"DEBUG: {self.class_name}.{method_name}: " + # msg += f"self.have.serial_number: {self.have.serial_number}, " + # msg += f"want['policy']: {want['policy']}, " + # msg += f"self.have.policy: {self.have.policy}" + # self.log_msg(msg) + # The switch has an image policy attached which is # different from the want policy. # idempotent_want == want with policy_changed = True @@ -619,13 +659,35 @@ def _build_idempotent_want(self, want) -> None: instance = ImageInstallOptions(self.module) instance.policy_name = want["policy"] instance.serial_number = self.have.serial_number - instance.epld = want["upgrade"]["epld"] - instance.issu = want["upgrade"]["nxos"] + + instance.epld = want.get("upgrade", {}).get("epld", False) + instance.issu = want.get("upgrade", {}).get("nxos", False) + instance.package_install = want.get("options", {}).get("package", {}).get( + "install", False + ) instance.refresh() - if instance.epld_modules is None: + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += "Calling ImageInstallOptions.response " + msg += "instance.response: " + msg += f"{json.dumps(instance.response_data, indent=4, sort_keys=True)}" + self.log_msg(msg) + + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.idempotent_want PRE EPLD CHECK: " + msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" + self.log_msg(msg) + + # if InstallOptions indicates that EPLD is already upgraded, + # don't upgrade it again. + if self.needs_epld_upgrade(instance.epld_modules) is False: self.idempotent_want["upgrade"]["epld"] = False + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.idempotent_want POST EPLD CHECK: " + msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" + self.log_msg(msg) + def get_need_merged(self) -> None: """ Caller: main() @@ -637,19 +699,41 @@ def get_need_merged(self) -> None: method_name = inspect.stack()[0][3] need: List[Dict] = [] - for want_create in self.want: - self.have.ip_address = want_create["ip_address"] + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += "self.want: " + msg += f"{json.dumps(self.want, indent=4, sort_keys=True)}" + self.log_msg(msg) + + for want in self.want: + self.have.ip_address = want["ip_address"] + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.have.serial_number: {self.have.serial_number}" + self.log_msg(msg) if self.have.serial_number is not None: - self._build_idempotent_want(want_create) - if ( - self.idempotent_want["policy_changed"] is False - and self.idempotent_want["stage"] is False - and self.idempotent_want["upgrade"]["nxos"] is False - and self.idempotent_want["upgrade"]["epld"] is False - ): + self._build_idempotent_want(want) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += "self.idempotent_want: " + msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" + self.log_msg(msg) + test_idempotence = set() + test_idempotence.add(self.idempotent_want["policy_changed"]) + test_idempotence.add(self.idempotent_want["stage"]) + test_idempotence.add(self.idempotent_want["upgrade"]["nxos"]) + test_idempotence.add(self.idempotent_want["upgrade"]["epld"]) + test_idempotence.add(self.idempotent_want["options"]["package"]["install"]) + # TODO:2 InstallOptions doesn't seem to have a way to determine package uninstall. + # TODO:2 For now, we'll comment this out so that it doesn't muck up idempotence. + # test_idempotence.add(self.idempotent_want["options"]["package"]["uninstall"]) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"test_idempotence: {test_idempotence}" + self.log_msg(msg) + if True not in test_idempotence: continue need.append(self.idempotent_want) self.need = need + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"self.need: {json.dumps(self.need, indent=4, sort_keys=True)}" + self.log_msg(msg) def get_need_deleted(self) -> None: """ @@ -661,7 +745,7 @@ def get_need_deleted(self) -> None: Policies are detached only if the policy name matches. """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable need = [] for want in self.want: @@ -684,7 +768,7 @@ def get_need_query(self) -> None: policy name is ignored for query state. """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable need = [] for want in self.want: @@ -909,6 +993,18 @@ def _validate_input_for_query_state(self) -> None: msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) + def merge_dicts(self, d1, d2): + """ + Merge d2 into d1 and return d1. + Keys in d2 have precedence over keys in d1. + """ + for key in d2: + if key in d1 and isinstance(d1[key], Map) and isinstance(d2[key], Map): + self.merge_dicts(d1[key], d2[key]) + else: + d1[key] = d2[key] + return d1 + def _merge_global_and_switch_configs(self, config) -> None: """ Merge the global config with each switch config and return @@ -920,9 +1016,10 @@ def _merge_global_and_switch_configs(self, config) -> None: from global_config. 3. If a switch_config has a parameter, use it. 4. If global_config and switch_config are both missing an - optional parameter, use the parameter's default value. + optional parameter, use the parameter's default value + (done in ImageUpgrade.build_payload) 5. If global_config and switch_config are both missing a - mandatory parameter, fail. + mandatory parameter, fail (done in self._validate_switch_configs) """ method_name = inspect.stack()[0][3] @@ -931,16 +1028,28 @@ def _merge_global_and_switch_configs(self, config) -> None: msg += "playbook is missing list of switches" self.module.fail_json(msg) - global_config = {} - global_config["policy"] = config.get("policy") - global_config["stage"] = config.get("stage") - global_config["upgrade"] = config.get("upgrade") - global_config["options"] = config.get("options") - global_config["validate"] = config.get("validate") - self.switch_configs = [] for switch in config["switches"]: - switch_config = global_config.copy() | switch.copy() + # we need to rebuild global_config in this loop + # because merge_dicts modifies it in place + global_config = copy.deepcopy(config) + global_config.pop("switches", None) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += ( + f"global_config: {json.dumps(global_config, indent=4, sort_keys=True)}" + ) + self.log_msg(msg) + + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"switch PRE_MERGE : {json.dumps(switch, indent=4, sort_keys=True)}" + self.log_msg(msg) + + switch_config = self.merge_dicts(global_config, switch) + + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.switch_configs.append(switch_config) def _validate_switch_configs(self) -> None: @@ -951,7 +1060,8 @@ def _validate_switch_configs(self) -> None: NOTES: 1. Final application of missing default parameters is done in - ImageUpgrade.commit() + ImageUpgrade.build_payload, which calls + ImageUpgrade._merge_defaults_to_switch_config Callers: - self.get_want @@ -1043,7 +1153,7 @@ def _send_policy_attach_payload(self) -> None: Callers: - self.handle_merged_state """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable if len(self.payloads) == 0: return @@ -1071,6 +1181,10 @@ def _stage_images(self, serial_numbers) -> None: """ method_name = inspect.stack()[0][3] + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"serial_numbers: {serial_numbers}" + self.log_msg(msg) + instance = ImageStage(self.module) instance.serial_numbers = serial_numbers instance.commit() @@ -1084,6 +1198,10 @@ def _validate_images(self, serial_numbers) -> None: """ method_name = inspect.stack()[0][3] + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"serial_numbers: {serial_numbers}" + self.log_msg(msg) + instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers instance.commit() @@ -1129,14 +1247,27 @@ def _verify_install_options(self, devices) -> None: install_options = ImageInstallOptions(self.module) self.switch_details.refresh() - for device in devices: + verify_devices = copy.deepcopy(devices) + + for device in verify_devices: + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"device: {json.dumps(device, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.switch_details.ip_address = device.get("ip_address") install_options.serial_number = self.switch_details.serial_number - install_options.policy_name = device["policy"] - install_options.epld = device["upgrade"]["epld"] - install_options.issu = device["upgrade"]["nxos"] + install_options.policy_name = device.get("policy") + install_options.epld = device.get("upgrade", {}).get("epld", False) + install_options.issu = device.get("upgrade", {}).get("nxos", False) install_options.refresh() + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += "install_options.response_data: " + msg += ( + f"{json.dumps(install_options.response_data, indent=4, sort_keys=True)}" + ) + self.log_msg(msg) + if ( install_options.status not in ["Success", "Skipped"] and device["upgrade"]["nxos"] is True @@ -1148,10 +1279,18 @@ def _verify_install_options(self, devices) -> None: msg += "NX-OS image" self.module.fail_json(msg) - if ( - install_options.epld_modules is None - and device["upgrade"]["epld"] is True - ): + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"install_options.epld: {install_options.epld}" + self.log_msg(msg) + + + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += "install_options.epld_modules: " + msg += f"{json.dumps(install_options.epld_modules, indent=4, sort_keys=True)}" + self.log_msg(msg) + + + if install_options.epld_modules is None and install_options.epld is True: msg = f"{self.class_name}.{method_name}: " msg += "EPLD upgrade is set to True for switch " msg += f"{device['ip_address']}, but the image policy " @@ -1159,6 +1298,47 @@ def _verify_install_options(self, devices) -> None: msg += "EPLD image." self.module.fail_json(msg) + # if self.needs_epld_upgrade(install_options.epld_modules) is False: + # devices[devices.index(device)]["upgrade"]["epld"] = False + + # return devices + + def needs_epld_upgrade(self, epld_modules) -> bool: + """ + Determine if the switch needs an EPLD upgrade + + For all modules, compare EPLD oldVersion and newVersion. + Returns: + - True if newVersion > oldVersion for any module + - False otherwise + + Callers: + - self._build_idempotent_want + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + if epld_modules is None: + return False + if epld_modules.get("moduleList") is None: + return False + for module in epld_modules["moduleList"]: + new_version = module.get("newVersion", "0x0") + old_version = module.get("oldVersion", "0x0") + # int(str, 0) enables python to guess the base + # of the string when converting to int. An + # error is thrown without this. + if int(new_version, 0) > int(old_version, 0): + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"(device: {module.get('deviceName')}), " + msg += f"(IP: {module.get('ipAddress')}), " + msg += f"(module#: {module.get('module')}), " + msg += f"(module: {module.get('moduleType')}), " + msg += f"new_version {new_version} > old_version {old_version}, " + msg += "returning True" + self.log_msg(msg) + return True + return False + def _upgrade_images(self, devices) -> None: """ Upgrade the switch(es) to the specified image @@ -1166,7 +1346,7 @@ def _upgrade_images(self, devices) -> None: Callers: - handle_merged_state """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable upgrade = ImageUpgrade(self.module) upgrade.devices = devices @@ -1181,7 +1361,7 @@ def handle_merged_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self._build_policy_attach_payload() self._send_policy_attach_payload() @@ -1222,7 +1402,7 @@ def handle_deleted_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable detach_policy_devices: Dict[str, Any] = {} @@ -1256,7 +1436,7 @@ def handle_query_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py index 3bd7625b5..eae173520 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py @@ -11,27 +11,44 @@ # 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. +""" +Function to load unit test inputs -# Make coding more python3-ish +Imported by image_upgrade_utils.py +""" from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name import json import os +import sys fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") def load_fixture(filename): - path = os.path.join(fixture_path, "{0}.json".format(filename)) + """ + load test inputs from json files + """ + path = os.path.join(fixture_path, f"{filename}.json") - with open(path) as f: - data = f.read() + try: + with open(path, encoding="utf-8") as f: + data = f.read() + except IOError as exception: + msg = f"Exception opening test input file {filename}.json : " + msg += f"Exception detail: {exception}" + print(msg) + sys.exit(1) try: fixture = json.loads(data) - except Exception as exception: - print(f"Exception loading fixture {filename}. Exception detail: {exception}") + except json.JSONDecodeError as exception: + msg = "Exception reading JSON contents in " + msg += f"test input file {filename}.json : " + msg += f"Exception detail: {exception}" + print(msg) + sys.exit(1) return fixture diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json new file mode 100644 index 000000000..666417567 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json @@ -0,0 +1,124 @@ +{ + "test_image_mgmt_upgrade_task_00030a": { + "TEST_NOTES": [ + "switch 1.1.1.1 uses default options", + "switch 2.2.2.2 overrides default options" + ], + "config": { + "options": { + "epld": { + "golden": false, + "module": "ALL" + }, + "nxos": { + "bios_force": false, + "mode": "disruptive" + }, + "package": { + "install": false, + "uninstall": false + }, + "reboot": { + "config_reload": false, + "write_erase": false + } + }, + "policy": "NR3F", + "reboot": false, + "stage": true, + "switches": [ + { + "ip_address": "1.1.1.1" + }, + { + "ip_address": "2.2.2.2", + "options": { + "epld": { + "module": 1, + "golden": true + }, + "nxos": { + "bios_force": true, + "mode": "non_disruptive" + }, + "package": { + "install": true, + "uninstall": true + }, + "reboot": { + "config_reload": true, + "write_erase": true + } + }, + "reboot": true, + "stage": false, + "upgrade": { + "epld": true, + "nxos": false + }, + "validate": false + } + ], + "upgrade": { + "epld": false, + "nxos": true + }, + "validate": true + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00031a": { + "config": { + "options": { + "epld": { + "golden": false, + "module": "ALL" + }, + "nxos": { + "bios_force": false, + "mode": "disruptive" + }, + "package": { + "install": false, + "uninstall": false + }, + "reboot": { + "config_reload": false, + "write_erase": false + } + }, + "policy": "NR3F", + "reboot": false, + "stage": true, + "switches": [ + { + "ip_address": "1.1.1.1", + "options": { + "nxos": { + "bios_force": false, + "mode": "non_disruptive" + }, + "reboot": { + "write_erase": false + } + }, + "reboot": false, + "stage": false, + "validate": false + }, + { + "ip_address": "2.2.2.2", + "upgrade": { + "epld": true + } + } + ], + "upgrade": { + "epld": false, + "nxos": true + }, + "validate": true + }, + "state": "merged" + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index de5f6dff8..421d6f8b0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3648,5 +3648,33 @@ ], "message": "" } + }, + "test_image_mgmt_upgrade_task_00020a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "Two switches present", + "First switch requires ipAddress and deviceName", + "Second switch requires ipAddress, deviceName, serialNumber, fabric" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "ipAddress": "172.22.150.102", + "deviceName": "leaf1" + }, + { + "ipAddress": "172.22.150.108", + "deviceName": "cvd-2313-leaf", + "serialNumber": "FDO2112189M", + "fabric": "hard" + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index e3e9269bc..c2e83df96 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -16,38 +16,51 @@ Utilities for image_upgrade unit tests """ from __future__ import absolute_import, division, print_function -import pytest + from contextlib import contextmanager +from typing import Any, Dict +import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ ControllerVersion -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ - ImageInstallOptions from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ ImagePolicies from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ ImagePolicyAction from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ ImageStage -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ - ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ ImageUpgrade -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ - SwitchIssuDetailsByDeviceName -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ - SwitchIssuDetailsByIpAddress -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ - SwitchIssuDetailsBySerialNumber +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ + ImageValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ + ImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ + SwitchDetails +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import ( + SwitchIssuDetailsByDeviceName, SwitchIssuDetailsByIpAddress, + SwitchIssuDetailsBySerialNumber) +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ + ImageUpgradeTask + +from .fixture import load_fixture + class MockAnsibleModule: """ Mock the AnsibleModule class """ - params = {} + params = {"config": {"switches": [{"ip_address": "172.22.150.105"}]}} + argument_spec = { + "config": {"required": True, "type": "dict"}, + "state": {"default": "merged", "choices": ["merged", "deleted", "query"]}, + } + supports_check_mode = True @staticmethod def fail_json(msg) -> AnsibleFailJson: @@ -56,52 +69,234 @@ def fail_json(msg) -> AnsibleFailJson: """ raise AnsibleFailJson(msg) -#See the following for explanation of why fixtures are explicitely named -#https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html + def public_method_for_pylint(self) -> Any: + """ + Add one public method to appease pylint + """ + + +# See the following for explanation of why fixtures are explicitely named +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html + @pytest.fixture(name="controller_version") def controller_version_fixture(): + """ + mock ControllerVersion + """ return ControllerVersion(MockAnsibleModule) + @pytest.fixture(name="image_install_options") def image_install_options_fixture(): + """ + mock ImageInstallOptions + """ return ImageInstallOptions(MockAnsibleModule) + @pytest.fixture(name="image_policies") def image_policies_fixture(): + """ + mock ImagePolicies + """ return ImagePolicies(MockAnsibleModule) + @pytest.fixture(name="image_policy_action") def image_policy_action_fixture(): + """ + mock ImagePolicyAction + """ return ImagePolicyAction(MockAnsibleModule) + @pytest.fixture(name="image_stage") def image_stage_fixture(): + """ + mock ImageStage + """ return ImageStage(MockAnsibleModule) + @pytest.fixture(name="image_upgrade_common") def image_upgrade_common_fixture(): + """ + mock ImageUpgradeCommon + """ return ImageUpgradeCommon(MockAnsibleModule) + @pytest.fixture(name="image_upgrade") def image_upgrade_fixture(): + """ + mock ImageUpgrade + """ return ImageUpgrade(MockAnsibleModule) -@pytest.fixture(name="issu_details_by_ip_address") -def issu_details_by_ip_address_fixture(): - return SwitchIssuDetailsByIpAddress(MockAnsibleModule) + +@pytest.fixture(name="image_upgrade_task") +def image_upgrade_task_fixture(): + """ + mock ImageUpgradeTask + """ + return ImageUpgradeTask(MockAnsibleModule) + + +@pytest.fixture(name="image_validate") +def image_validate_fixture(): + """ + mock ImageValidate + """ + return ImageValidate(MockAnsibleModule) + @pytest.fixture(name="issu_details_by_device_name") def issu_details_by_device_name_fixture(): + """ + mock SwitchIssuDetailsByDeviceName + """ return SwitchIssuDetailsByDeviceName(MockAnsibleModule) + +@pytest.fixture(name="issu_details_by_ip_address") +def issu_details_by_ip_address_fixture(): + """ + mock SwitchIssuDetailsByIpAddress + """ + return SwitchIssuDetailsByIpAddress(MockAnsibleModule) + + @pytest.fixture(name="issu_details_by_serial_number") def issu_details_by_serial_number_fixture() -> SwitchIssuDetailsBySerialNumber: + """ + mock SwitchIssuDetailsBySerialNumber + """ return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) + +@pytest.fixture(name="switch_details") +def switch_details_fixture(): + """ + mock SwitchDetails + """ + return SwitchDetails(MockAnsibleModule) + + @contextmanager def does_not_raise(): """ A context manager that does not raise an exception. """ yield + + +def load_playbook_config(key: str) -> Dict[str, str]: + """ + Return playbook configs for ImageUpgradeTask + """ + playbook_file = "image_upgrade_playbook_configs" + playbook_config = load_fixture(playbook_file).get(key) + print(f"load_playbook_config: {key} : {playbook_config}") + return playbook_config + + +def payloads_image_upgrade(key: str) -> Dict[str, str]: + """ + Return payloads for ImageUpgrade + """ + payload_file = "image_upgrade_payloads_ImageUpgrade" + payload = load_fixture(payload_file).get(key) + print(f"payload_data_image_upgrade: {key} : {payload}") + return payload + + +def responses_controller_version(key: str) -> Dict[str, str]: + """ + Return ControllerVersion controller responses + """ + response_file = "image_upgrade_responses_ControllerVersion" + response = load_fixture(response_file).get(key) + print(f"responses_controller_version: {key} : {response}") + return response + + +def responses_image_install_options(key: str) -> Dict[str, str]: + """ + Return ImageInstallOptions controller responses + """ + response_file = "image_upgrade_responses_ImageInstallOptions" + response = load_fixture(response_file).get(key) + print(f"{key} : : {response}") + return response + + +def responses_image_policies(key: str) -> Dict[str, str]: + """ + Return ImagePolicies controller responses + """ + response_file = "image_upgrade_responses_ImagePolicies" + response = load_fixture(response_file).get(key) + print(f"responses_image_policies: {key} : {response}") + return response + + +def responses_image_policy_action(key: str) -> Dict[str, str]: + """ + Return ImagePolicyAction controller responses + """ + response_file = "image_upgrade_responses_ImagePolicyAction" + response = load_fixture(response_file).get(key) + print(f"responses_image_policy_action: {key} : {response}") + return response + + +def responses_image_stage(key: str) -> Dict[str, str]: + """ + Return ImageStage controller responses + """ + response_file = "image_upgrade_responses_ImageStage" + response = load_fixture(response_file).get(key) + print(f"responses_image_stage: {key} : {response}") + return response + + +def responses_image_upgrade(key: str) -> Dict[str, str]: + """ + Return ImageUpgrade controller responses + """ + response_file = "image_upgrade_responses_ImageUpgrade" + response = load_fixture(response_file).get(key) + print(f"response_data_image_upgrade: {key} : {response}") + return response + + +def responses_image_upgrade_common(key: str) -> Dict[str, str]: + """ + Return ImageUpgradeCommon controller responses + """ + response_file = "image_upgrade_responses_ImageUpgradeCommon" + response = load_fixture(response_file).get(key) + verb = response.get("METHOD") + print(f"{key} : {verb} : {response}") + return {"response": response, "verb": verb} + + +def responses_switch_details(key: str) -> Dict[str, str]: + """ + Return SwitchDetails controller responses + """ + response_file = "image_upgrade_responses_SwitchDetails" + response = load_fixture(response_file).get(key) + print(f"responses_switch_details: {key} : {response}") + return response + + +def responses_switch_issu_details(key: str) -> Dict[str, str]: + """ + Return SwitchIssuDetails controller responses + """ + response_file = "image_upgrade_responses_SwitchIssuDetails" + response = load_fixture(response_file).get(key) + print(f"responses_switch_issu_details: {key} : {response}") + return response diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py index 413875ef2..8174e3d40 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py @@ -30,34 +30,18 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from .fixture import load_fixture -from .image_upgrade_utils import controller_version_fixture +from .image_upgrade_utils import (controller_version_fixture, + responses_controller_version) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of ControllerVersion -""" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_COMMON = PATCH_MODULE_UTILS + "common." - DCNM_SEND_VERSION = PATCH_COMMON + "controller_version.dcnm_send" -def responses_controller_version(key: str) -> Dict[str, str]: - """ - Return the response from ControllerVersion - """ - response_file = "image_upgrade_responses_ControllerVersion" - response = load_fixture(response_file).get(key) - print(f"responses_controller_version: {key} : {response}") - return response - - def test_common_version_00001(controller_version) -> None: """ Function diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index f0edddf83..bc54ea658 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -32,8 +32,9 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints -from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule, does_not_raise, image_install_options_fixture +from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, + image_install_options_fixture, + responses_image_install_options) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -43,16 +44,6 @@ DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" -def responses_image_install_options(key: str) -> Dict[str, str]: - """ - Return the response from ImageInstallOptions - """ - response_file = "image_upgrade_responses_ImageInstallOptions" - response = load_fixture(response_file).get(key) - print(f"{key} : : {response}") - return response - - def test_image_mgmt_install_options_00001(image_install_options) -> None: """ Function @@ -139,6 +130,7 @@ def test_image_mgmt_install_options_00005(monkeypatch, image_install_options) -> - Properties are updated with expected values - endpoint: install-options """ + def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_install_options_00005a" return responses_image_install_options(key) @@ -419,7 +411,7 @@ def test_image_mgmt_install_options_00020(image_install_options) -> None: instance = image_install_options instance.policy_name = "KRM5" instance.serial_number = "BAR" - instance._build_payload() # pylint: disable=protected-access + instance._build_payload() # pylint: disable=protected-access assert instance.payload.get("devices")[0].get("policyName") == "KRM5" assert instance.payload.get("devices")[0].get("serialNumber") == "BAR" assert instance.payload.get("issu") is True @@ -445,7 +437,7 @@ def test_image_mgmt_install_options_00021(image_install_options) -> None: instance.issu = False instance.epld = True instance.package_install = True - instance._build_payload() # pylint: disable=protected-access + instance._build_payload() # pylint: disable=protected-access assert instance.payload.get("devices")[0].get("policyName") == "KRM5" assert instance.payload.get("devices")[0].get("serialNumber") == "BAR" assert instance.payload.get("issu") is False diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index c8f11e90c..c86e8e5fa 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -33,33 +33,18 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints -from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule, does_not_raise, image_policies_fixture +from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, + image_policies_fixture, + responses_image_policies) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of class ImagePolicies -""" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." - DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" -def responses_image_policies(key: str) -> Dict[str, str]: - """ - Return the response from ImagePolicies - """ - response_file = "image_upgrade_responses_ImagePolicies" - response = load_fixture(response_file).get(key) - print(f"responses_image_policies: {key} : {response}") - return response - - def test_image_mgmt_image_policies_00001(image_policies) -> None: """ Function @@ -317,4 +302,4 @@ def test_image_mgmt_image_policies_00040(image_policies) -> None: instance = image_policies with pytest.raises(AnsibleFailJson, match=match): - instance._get("imageName") # pylint: disable=protected-access + instance._get("imageName") # pylint: disable=protected-access diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 97b81305f..ef1ff9397 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -11,9 +11,11 @@ # 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. + """ ImagePolicyAction - unit tests """ + # See the following regarding *_fixture imports # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html # Due to the above, we also need to disable unused-import @@ -36,18 +38,17 @@ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture -from .image_upgrade_utils import ( - does_not_raise, image_policies_fixture, image_policy_action_fixture, - issu_details_by_serial_number_fixture) +from .image_upgrade_utils import (does_not_raise, image_policies_fixture, + image_policy_action_fixture, + issu_details_by_serial_number_fixture, + responses_image_policies, + responses_image_policy_action, + responses_switch_details, + responses_switch_issu_details) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -Description: Verify functionality of ImagePolicyAction -""" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." @@ -57,46 +58,6 @@ DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" -def responses_image_policies(key: str) -> Dict[str, str]: - """ - Return ImagePolicies controller responses - """ - response_file = "image_upgrade_responses_ImagePolicies" - response = load_fixture(response_file).get(key) - print(f"responses_image_policies: {key} : {response}") - return response - - -def responses_image_policy_action(key: str) -> Dict[str, str]: - """ - Return ImagePolicyAction controller responses - """ - response_file = "image_upgrade_responses_ImagePolicyAction" - response = load_fixture(response_file).get(key) - print(f"responses_image_policy_action: {key} : {response}") - return response - - -def responses_switch_details(key: str) -> Dict[str, str]: - """ - Return SwitchDetails controller responses - """ - response_file = "image_upgrade_responses_SwitchDetails" - response = load_fixture(response_file).get(key) - print(f"responses_switch_details: {key} : {response}") - return response - - -def responses_switch_issu_details(key: str) -> Dict[str, str]: - """ - Return SwitchIssuDetails controller responses - """ - response_file = "image_upgrade_responses_SwitchIssuDetails" - response = load_fixture(response_file).get(key) - print(f"responses_switch_issu_details: {key} : {response}") - return response - - def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: """ Function diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 7f028615b..959cdf28a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -34,10 +34,12 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber -from .fixture import load_fixture from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, image_stage_fixture, - issu_details_by_serial_number_fixture) + issu_details_by_serial_number_fixture, + responses_controller_version, + responses_image_stage, + responses_switch_issu_details) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -52,36 +54,6 @@ DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" -def responses_controller_version(key: str) -> Dict[str, str]: - """ - mock responses from ControllerVersion - """ - response_file = "image_upgrade_responses_ControllerVersion" - response = load_fixture(response_file).get(key) - print(f"responses_controller_version: {key} : {response}") - return response - - -def responses_image_stage(key: str) -> Dict[str, str]: - """ - mock responses from ImageStage - """ - response_file = "image_upgrade_responses_ImageStage" - response = load_fixture(response_file).get(key) - print(f"responses_image_stage: {key} : {response}") - return response - - -def responses_issu_details(key: str) -> Dict[str, str]: - """ - mock responses from SwitchIssuDetails - """ - response_file = "image_upgrade_responses_SwitchIssuDetails" - response = load_fixture(response_file).get(key) - print(f"responses_issu_details: {key} : {response}") - return response - - def test_image_mgmt_stage_00001(image_stage) -> None: """ Function @@ -174,7 +146,7 @@ def test_image_mgmt_stage_00004( def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00004a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -216,7 +188,7 @@ def test_image_mgmt_stage_00005( def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00005a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -265,7 +237,7 @@ def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00006a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) @@ -300,7 +272,7 @@ def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00007a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) @@ -354,7 +326,7 @@ def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00008a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -389,7 +361,7 @@ def test_image_mgmt_stage_00009( def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00009a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -399,7 +371,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: "FDO2112189M", ] instance.check_interval = 0 - instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access + instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 2 assert "FDO21120U5D" in instance.serial_numbers_done @@ -432,7 +404,7 @@ def test_image_mgmt_stage_00010( def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00010a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -446,7 +418,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: match += "cvd-2313-leaf, FDO2112189M, 172.22.150.108. image " match += "staged percent: 90" with pytest.raises(AnsibleFailJson, match=match): - instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access + instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 1 assert "FDO21120U5D" in instance.serial_numbers_done @@ -477,7 +449,7 @@ def test_image_mgmt_stage_00011( def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00011a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -495,7 +467,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" with pytest.raises(AnsibleFailJson, match=match): - instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access + instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 1 assert "FDO21120U5D" in instance.serial_numbers_done @@ -530,7 +502,7 @@ def test_image_mgmt_stage_00012( def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00012a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -540,7 +512,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: "FDO2112189M", ] instance.check_interval = 0 - instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access + instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 2 assert "FDO21120U5D" in instance.serial_numbers_done @@ -570,7 +542,7 @@ def test_image_mgmt_stage_00013( def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00013a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -588,7 +560,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: instance.check_timeout = 1 with pytest.raises(AnsibleFailJson, match=match): - instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access + instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 1 assert "FDO21120U5D" in instance.serial_numbers_done diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index fdff94050..bf5ad7391 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -35,13 +35,13 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ ImageUpgrade -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ - SwitchIssuDetailsByIpAddress -from .fixture import load_fixture -from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, - image_upgrade_fixture, - issu_details_by_ip_address_fixture) +from .image_upgrade_utils import (does_not_raise, image_upgrade_fixture, + issu_details_by_ip_address_fixture, + payloads_image_upgrade, + responses_image_install_options, + responses_image_upgrade, + responses_switch_issu_details) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -49,52 +49,11 @@ PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." - DCNM_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.dcnm_send" DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" -def payloads_image_upgrade(key: str) -> Dict[str, str]: - """ - Return payloads for ImageUpgrade - """ - payload_file = "image_upgrade_payloads_ImageUpgrade" - payload = load_fixture(payload_file).get(key) - print(f"payload_data_image_upgrade: {key} : {payload}") - return payload - - -def responses_image_upgrade(key: str) -> Dict[str, str]: - """ - Return responses for ImageUpgrade - """ - response_file = "image_upgrade_responses_ImageUpgrade" - response = load_fixture(response_file).get(key) - print(f"response_data_image_upgrade: {key} : {response}") - return response - - -def responses_install_options(key: str) -> Dict[str, str]: - """ - Return responses for ImageInstallOptions - """ - response_file = "image_upgrade_responses_ImageInstallOptions" - response = load_fixture(response_file).get(key) - print(f"response_data_install_options: {key} : {response}") - return response - - -def responses_issu_details(key: str) -> Dict[str, str]: - """ - Return responses for SwitchIssuDetails - """ - response_file = "image_upgrade_responses_SwitchIssuDetails" - response = load_fixture(response_file).get(key) - print(f"response_data_issu_details: {key} : {response}") - return response - - def test_image_mgmt_upgrade_00001(image_upgrade) -> None: """ Function @@ -194,7 +153,7 @@ def test_image_mgmt_upgrade_00004(monkeypatch, image_upgrade) -> None: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00005a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -752,10 +711,10 @@ def test_image_mgmt_upgrade_00018(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00019a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) @@ -791,7 +750,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] - match = r"ImageUpgrade.build_payload: upgrade.nxos must be a boolean. Got FOO\." + match = r"ImageUpgrade.build_payload_issu_upgrade: upgrade.nxos must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -828,10 +787,10 @@ def test_image_mgmt_upgrade_00019(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00019a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) @@ -900,10 +859,10 @@ def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00020a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) @@ -970,10 +929,10 @@ def test_image_mgmt_upgrade_00021(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00021a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) @@ -1009,7 +968,8 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: options.nxos.mode must be one of " + match = "ImageUpgrade.build_payload_issu_options_1: " + match += "options.nxos.mode must be one of " match += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " match += "Got FOO." with pytest.raises(AnsibleFailJson, match=match): @@ -1048,10 +1008,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1122,10 +1082,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1194,10 +1154,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1230,7 +1190,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: " + match = "ImageUpgrade.build_payload_issu_options_2: " match += r"options.nxos.bios_force must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1266,10 +1226,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1302,7 +1262,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: Invalid configuration for " + match = "ImageUpgrade.build_payload_epld: Invalid configuration for " match += "172.22.150.102. If options.epld.golden is True " match += "all other upgrade options, e.g. upgrade.nxos, " match += "must be False." @@ -1339,10 +1299,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1379,7 +1339,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: " + match = "ImageUpgrade.build_payload_epld: " match += "options.epld.module must either be 'ALL' " match += r"or an integer. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1415,10 +1375,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1455,7 +1415,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: " + match = "ImageUpgrade.build_payload_epld: " match += r"options.epld.golden must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1490,10 +1450,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1526,7 +1486,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: " + match = "ImageUpgrade.build_payload_reboot: " match += r"reboot must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1562,10 +1522,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1602,7 +1562,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: " + match = "ImageUpgrade.build_payload_reboot_options: " match += r"options.reboot.config_reload must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1638,10 +1598,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1678,7 +1638,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: " + match = "ImageUpgrade.build_payload_reboot_options: " match += r"options.reboot.write_erase must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1719,10 +1679,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1759,7 +1719,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: " + match = "ImageUpgrade.build_payload_package: " match += r"options.package.uninstall must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1796,10 +1756,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1873,10 +1833,10 @@ def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_install_options(key) + return responses_image_install_options(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1913,8 +1873,8 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload: " - match += r"upgrade.epld must be a boolean. Got FOO\." + match = "ImageInstallOptions.epld.setter: " + match += r"epld must be a boolean value. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1967,7 +1927,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) @@ -2034,7 +1994,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) @@ -2101,7 +2061,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_issu_details(key) + return responses_switch_issu_details(key) def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) @@ -2531,7 +2491,7 @@ def test_image_mgmt_upgrade_00080( def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00080a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2574,7 +2534,7 @@ def test_image_mgmt_upgrade_00081( def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00081a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2627,7 +2587,7 @@ def test_image_mgmt_upgrade_00090( def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00090a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2681,7 +2641,7 @@ def test_image_mgmt_upgrade_00091( def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00091a" - return responses_issu_details(key) + return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 6e6089129..4c508ee6f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -30,27 +30,14 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -# from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ -# ImageUpgradeCommon -from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule, does_not_raise, image_upgrade_common_fixture +from .image_upgrade_utils import (does_not_raise, image_upgrade_common_fixture, + responses_image_upgrade_common) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -def responses_image_upgrade_common(key: str) -> Dict[str, str]: - """ - Return responses from ImageUpgradeCommon - """ - response_file = "image_upgrade_responses_ImageUpgradeCommon" - response = load_fixture(response_file).get(key) - verb = response.get("METHOD") - print(f"{key} : {verb} : {response}") - return {"response": response, "verb": verb} - - def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: """ Function @@ -63,9 +50,11 @@ def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: - image_upgrade_common.fd is None - image_upgrade_common.logfile is /tmp/ansible_dcnm.log """ + test_params = {"config": {"switches": [{"ip_address": "172.22.150.105"}]}} + with does_not_raise(): instance = image_upgrade_common - assert instance.params == {} + assert instance.params == test_params assert instance.debug is False assert instance.fd is None assert instance.logfile == "/tmp/ansible_dcnm.log" @@ -107,7 +96,7 @@ def test_image_mgmt_image_upgrade_common_00020( data = responses_image_upgrade_common(key) with does_not_raise(): - result = instance._handle_response( # pylint: disable=protected-access + result = instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) assert result.get("success") == expected.get("success") @@ -150,7 +139,7 @@ def test_image_mgmt_image_upgrade_common_00030( data = responses_image_upgrade_common(key) with does_not_raise(): - result = instance._handle_response( # pylint: disable=protected-access + result = instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) assert result.get("success") == expected.get("success") @@ -193,7 +182,7 @@ def test_image_mgmt_image_upgrade_common_00040( data = responses_image_upgrade_common(key) with does_not_raise(): - result = instance._handle_response( # pylint: disable=protected-access + result = instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) assert result.get("success") == expected.get("success") @@ -234,7 +223,7 @@ def test_image_mgmt_image_upgrade_common_00050( instance = image_upgrade_common data = responses_image_upgrade_common(key) - result = instance._handle_response( # pylint: disable=protected-access + result = instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) assert result.get("success") == expected.get("success") @@ -253,7 +242,9 @@ def test_image_mgmt_image_upgrade_common_00060(image_upgrade_common) -> None: data = responses_image_upgrade_common("test_image_mgmt_image_upgrade_common_00060a") with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): - instance._handle_response(data.get("response"), data.get("verb")) # pylint: disable=protected-access + instance._handle_response( + data.get("response"), data.get("verb") + ) # pylint: disable=protected-access @pytest.mark.parametrize( @@ -292,7 +283,9 @@ def test_image_mgmt_image_upgrade_common_00070( data = responses_image_upgrade_common(key) with does_not_raise(): - result = instance._handle_get_response(data.get("response")) # pylint: disable=protected-access + result = instance._handle_get_response( + data.get("response") + ) # pylint: disable=protected-access assert result.get("success") == expected.get("success") assert result.get("changed") == expected.get("changed") @@ -330,7 +323,7 @@ def test_image_mgmt_image_upgrade_common_00080( data = responses_image_upgrade_common(key) with does_not_raise(): - result = instance._handle_post_put_delete_response( # pylint: disable=protected-access + result = instance._handle_post_put_delete_response( # pylint: disable=protected-access data.get("response") ) assert result.get("success") == expected.get("success") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py new file mode 100644 index 000000000..7b583984b --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py @@ -0,0 +1,328 @@ +# Copyright (c) 2020-2024 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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + + +""" +ImageUpgradeTask - unit tests +""" + +from __future__ import absolute_import, division, print_function + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ + ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ + SwitchDetails +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ + ImageUpgradeTask + +from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, + image_upgrade_fixture, + image_upgrade_task_fixture, + issu_details_by_ip_address_fixture, + load_playbook_config, payloads_image_upgrade, + responses_image_install_options, + responses_image_upgrade, + responses_switch_issu_details) + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." + +DCNM_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.dcnm_send" +DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" + + +# This fixture differs from image_upgrade_task_fixture +# in that it does not use a patched MockAnsibleModule. +# This is because we need to modify MockAnsibleModule for +# some of the test cases below. +@pytest.fixture(name="image_upgrade_task_bare") +def image_upgrade_task_bare_fixture(): + return ImageUpgradeTask + + +def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: + """ + Function + - __init__ + + Test + - Class attributes are initialized to expected values + """ + instance = image_upgrade_task_bare(MockAnsibleModule) + assert isinstance(instance, ImageUpgradeTask) + assert instance.class_name == "ImageUpgradeTask" + assert instance.have is None + assert instance.idempotent_want is None + assert instance.switch_configs == [] + assert instance.path is None + assert instance.verb is None + assert instance.payloads == [] + assert instance.config == {"switches": [{"ip_address": "172.22.150.105"}]} + assert instance.check_mode is False + assert instance.validated == {} + assert instance.want == [] + assert instance.need == [] + assert instance.result == {"changed": False, "diff": [], "response": []} + assert instance.mandatory_global_keys == {"switches"} + assert instance.mandatory_switch_keys == {"ip_address"} + assert isinstance(instance.switch_details, SwitchDetails) + assert isinstance(instance.image_policies, ImagePolicies) + + +def test_image_mgmt_upgrade_task_00002(image_upgrade_task_bare) -> None: + """ + Function + - __init__ + + Test + - fail_json is called because config is not a dict + """ + match = "ImageUpgradeTask.__init__: expected dict type for self.config. got str" + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = {"config": "foo"} + with pytest.raises(AnsibleFailJson, match=match): + instance = image_upgrade_task_bare(mock_ansible_module) + assert isinstance(instance, ImageUpgradeTask) + + +def test_image_mgmt_upgrade_task_00003(image_upgrade_task_bare) -> None: + """ + Function + - __init__ + + Test + - fail_json is called because config.switches is not a list + """ + match = "ImageUpgradeTask.__init__: expected list type for " + match += r"self.config\['switches'\]. got str" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = {"config": {"switches": "FOO"}} + with pytest.raises(AnsibleFailJson, match=match): + instance = image_upgrade_task_bare(mock_ansible_module) + assert isinstance(instance, ImageUpgradeTask) + + +def test_image_mgmt_upgrade_task_00004(image_upgrade_task_bare) -> None: + """ + Function + - __init__ + + Test + - fail_json is called because config.switches is empty + """ + match = "ImageUpgradeTask.__init__: missing list of switches " + match += "in playbook config." + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = {"config": {"switches": []}} + with pytest.raises(AnsibleFailJson, match=match): + instance = image_upgrade_task_bare(mock_ansible_module) + assert isinstance(instance, ImageUpgradeTask) + + +def test_image_mgmt_upgrade_task_00005(image_upgrade_task_bare) -> None: + """ + Function + - __init__ + + Test + - fail_json is called because mandatory keys are missing in + one of the switch configs + """ + match = "ImageUpgradeTask.__init__: missing mandatory " + match += r"key\(s\) in playbook switch config. expected " + match += r"\{'ip_address'\}, got dict_keys\(\['foo'\]\)" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = { + "config": {"switches": [{"ip_address": "1.2.3.4"}, {"foo": "bar"}]} + } + with pytest.raises(AnsibleFailJson, match=match): + instance = image_upgrade_task_bare(mock_ansible_module) + assert isinstance(instance, ImageUpgradeTask) + + +def test_image_mgmt_upgrade_task_00020(monkeypatch, image_upgrade_task) -> None: + """ + Function + - get_have + + Test + - SwitchIssuDetailsByIpAddress attributes are set to expected values + """ + key = "test_image_mgmt_upgrade_task_00020a" + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + instance = image_upgrade_task + instance.get_have() + instance.have.ip_address = "172.22.150.102" + assert instance.have.device_name == "leaf1" + instance.have.ip_address = "172.22.150.108" + assert instance.have.device_name == "cvd-2313-leaf" + assert instance.have.serial_number == "FDO2112189M" + assert instance.have.fabric == "hard" + + +def test_image_mgmt_upgrade_task_00030(monkeypatch, image_upgrade_task_bare) -> None: + """ + Function + - get_want + - _merge_global_and_switch_configs + - _validate_switch_configs + + Test + - global_config options are all set to default values + (see ImageUpgrade._init_defaults) + - switch_1 does not override any global_config options + so all values will be default + - switch_2 overrides all global_config options + so all values will be non-default + """ + key = "test_image_mgmt_upgrade_task_00030a" + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + instance.get_want() + switch_1 = instance.want[0] + switch_2 = instance.want[1] + assert switch_1.get("ip_address") == "1.1.1.1" + assert switch_1.get("options").get("epld").get("golden") is False + assert switch_1.get("options").get("epld").get("module") == "ALL" + assert switch_1.get("options").get("nxos").get("bios_force") is False + assert switch_1.get("options").get("nxos").get("mode") == "disruptive" + assert switch_1.get("options").get("package").get("install") is False + assert switch_1.get("options").get("package").get("uninstall") is False + assert switch_1.get("options").get("reboot").get("config_reload") is False + assert switch_1.get("options").get("reboot").get("write_erase") is False + assert switch_1.get("policy") == "NR3F" + assert switch_1.get("reboot") is False + assert switch_1.get("stage") is True + assert switch_1.get("upgrade").get("epld") is False + assert switch_1.get("upgrade").get("nxos") is True + assert switch_1.get("validate") is True + + assert switch_2.get("ip_address") == "2.2.2.2" + assert switch_2.get("options").get("epld").get("golden") is True + assert switch_2.get("options").get("epld").get("module") == 1 + assert switch_2.get("options").get("nxos").get("bios_force") is True + assert switch_2.get("options").get("nxos").get("mode") == "non_disruptive" + assert switch_2.get("options").get("package").get("install") is True + assert switch_2.get("options").get("package").get("uninstall") is True + assert switch_2.get("options").get("reboot").get("config_reload") is True + assert switch_2.get("options").get("reboot").get("write_erase") is True + assert switch_2.get("policy") == "NR3F" + assert switch_2.get("reboot") is True + assert switch_2.get("stage") is False + assert switch_2.get("upgrade").get("epld") is True + assert switch_2.get("upgrade").get("nxos") is False + assert switch_2.get("validate") is False + + +def test_image_mgmt_upgrade_task_00031(monkeypatch, image_upgrade_task_bare) -> None: + """ + Function + - get_want + - _merge_global_and_switch_configs + - _validate_switch_configs + + Test + - global_config options are all set to default values + - switch_1 overrides global_config.options.nxos.bios_force + with a default value (False) + - switch_1 overrides global_config.options.nxos.mode + with a non-default value (non_disruptive) + - switch_1 overrides global_config.options.reboot.write_erase + with default value (False) + - switch_1 overrides global_config.reboot with + a default value (False) + - switch_1 overrides global_config.stage with a + non-default value (False) + - switch_1 overrides global_config.validate with a + non-default value (False) + - switch_2 overrides global_config.upgrade.epld + with a non-default value (True) + - All other values for switch_1 and switch_2 are default + """ + key = "test_image_mgmt_upgrade_task_00031a" + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + instance.get_want() + switch_1 = instance.want[0] + switch_2 = instance.want[1] + assert switch_1.get("ip_address") == "1.1.1.1" + assert switch_1.get("options").get("epld").get("golden") is False + assert switch_1.get("options").get("epld").get("module") == "ALL" + assert switch_1.get("options").get("nxos").get("bios_force") is False + assert switch_1.get("options").get("nxos").get("mode") == "non_disruptive" + assert switch_1.get("options").get("package").get("install") is False + assert switch_1.get("options").get("package").get("uninstall") is False + assert switch_1.get("options").get("reboot").get("config_reload") is False + assert switch_1.get("options").get("reboot").get("write_erase") is False + assert switch_1.get("policy") == "NR3F" + assert switch_1.get("reboot") is False + assert switch_1.get("stage") is False + assert switch_1.get("upgrade").get("epld") is False + assert switch_1.get("upgrade").get("nxos") is True + assert switch_1.get("validate") is False + + assert switch_2.get("ip_address") == "2.2.2.2" + assert switch_2.get("options").get("epld").get("golden") is False + assert switch_2.get("options").get("epld").get("module") == "ALL" + assert switch_2.get("options").get("nxos").get("bios_force") is False + assert switch_2.get("options").get("nxos").get("mode") == "disruptive" + assert switch_2.get("options").get("package").get("install") is False + assert switch_2.get("options").get("package").get("uninstall") is False + assert switch_2.get("options").get("reboot").get("config_reload") is False + assert switch_2.get("options").get("reboot").get("write_erase") is False + assert switch_2.get("policy") == "NR3F" + assert switch_2.get("reboot") is False + assert switch_2.get("stage") is True + assert switch_2.get("upgrade").get("epld") is True + assert switch_2.get("upgrade").get("nxos") is True + assert switch_2.get("validate") is True diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index e32cf99ac..fc0ddebcc 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -12,6 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +""" +ImageUpgrade - unit tests +""" + from __future__ import absolute_import, division, print_function from typing import Any, Dict @@ -21,46 +34,23 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ - ImageValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber -from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule +from .image_upgrade_utils import (image_validate_fixture, + issu_details_by_serial_number_fixture, + responses_switch_issu_details) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of ImageValidate -""" - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." - -dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" -def response_data_issu_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchIssuDetails" - response = load_fixture(response_file).get(key) - print(f"response_data_issu_details: {key} : {response}") - return response - -@pytest.fixture -def module(): - return ImageValidate(MockAnsibleModule) - - -@pytest.fixture -def mock_issu_details() -> SwitchIssuDetailsBySerialNumber: - return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) - - -def test_image_mgmt_validate_00001(module) -> None: +def test_image_mgmt_validate_00001(image_validate) -> None: """ Function - __init__ @@ -68,13 +58,13 @@ def test_image_mgmt_validate_00001(module) -> None: Test - Class attributes are initialized to expected values """ - module.__init__(MockAnsibleModule) - assert module.class_name == "ImageValidate" - assert isinstance(module.endpoints, ApiEndpoints) - assert isinstance(module.issu_detail, SwitchIssuDetailsBySerialNumber) + instance = image_validate + assert instance.class_name == "ImageValidate" + assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.issu_detail, SwitchIssuDetailsBySerialNumber) -def test_image_mgmt_validate_00002(module) -> None: +def test_image_mgmt_validate_00002(image_validate) -> None: """ Function - _init_properties @@ -82,24 +72,26 @@ def test_image_mgmt_validate_00002(module) -> None: Test - Class properties are initialized to expected values """ - module._init_properties() - assert isinstance(module.properties, dict) - assert module.properties.get("check_interval") == 10 - assert module.properties.get("check_timeout") == 1800 - assert module.properties.get("response_data") == {} - assert module.properties.get("response") == {} - assert module.properties.get("result") == {} - assert module.properties.get("non_disruptive") is False - assert module.properties.get("serial_numbers") == [] - - -def test_image_mgmt_validate_00003(monkeypatch, module, mock_issu_details) -> None: + instance = image_validate + assert isinstance(instance.properties, dict) + assert instance.properties.get("check_interval") == 10 + assert instance.properties.get("check_timeout") == 1800 + assert instance.properties.get("response_data") == {} + assert instance.properties.get("response") == {} + assert instance.properties.get("result") == {} + assert instance.properties.get("non_disruptive") is False + assert instance.properties.get("serial_numbers") == [] + + +def test_image_mgmt_validate_00003( + monkeypatch, image_validate, issu_details_by_serial_number +) -> None: """ Function - prune_serial_numbers Test - - module.serial_numbers contains only serial numbers for which + - instance.serial_numbers contains only serial numbers for which "validated" == "none" - serial_numbers does not contain serial numbers for which "validated" == "Success" @@ -109,39 +101,38 @@ def test_image_mgmt_validate_00003(monkeypatch, module, mock_issu_details) -> No "validated" == "Success" (TODO: AND policy == ) Expected results: - 1. module.serial_numbers == ["FDO2112189M", "FDO211218AX", "FDO211218B5"] - 2. module.serial_numbers != ["FDO211218FV", "FDO211218GC"] + 1. instance.serial_numbers == ["FDO2112189M", "FDO211218AX", "FDO211218B5"] + 2. instance.serial_numbers != ["FDO211218FV", "FDO211218GC"] """ + instance = image_validate - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_validate_00003a" - return response_data_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO2112189M", "FDO211218AX", "FDO211218B5", "FDO211218FV", "FDO211218GC", ] - module.prune_serial_numbers() - assert isinstance(module.serial_numbers, list) - assert len(module.serial_numbers) == 3 - assert "FDO2112189M" in module.serial_numbers - assert "FDO211218AX" in module.serial_numbers - assert "FDO211218B5" in module.serial_numbers - assert "FDO211218FV" not in module.serial_numbers - assert "FDO211218GC" not in module.serial_numbers - - -# test_image_mgmt_validate_00004 -# test_validate_serial_numbers_failed (former name) - - -def test_image_mgmt_validate_00004(monkeypatch, module, mock_issu_details) -> None: + instance.prune_serial_numbers() + assert isinstance(instance.serial_numbers, list) + assert len(instance.serial_numbers) == 3 + assert "FDO2112189M" in instance.serial_numbers + assert "FDO211218AX" in instance.serial_numbers + assert "FDO211218B5" in instance.serial_numbers + assert "FDO211218FV" not in instance.serial_numbers + assert "FDO211218GC" not in instance.serial_numbers + + +def test_image_mgmt_validate_00004( + monkeypatch, image_validate, issu_details_by_serial_number +) -> None: """ Function - validate_serial_numbers @@ -155,26 +146,30 @@ def test_image_mgmt_validate_00004(monkeypatch, module, mock_issu_details) -> No FDO21120U5D should pass since validated == "Success" FDO2112189M should fail since validated == "Failed" """ + instance = image_validate - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_validate_00004a" - return response_data_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = ["FDO21120U5D", "FDO2112189M"] + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = ["FDO21120U5D", "FDO2112189M"] match = "ImageValidate.validate_serial_numbers: " match += "image validation is failing for the following switch: " match += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. If this " match += "persists, check the switch connectivity to the " match += "controller and try again." + with pytest.raises(AnsibleFailJson, match=match): - module.validate_serial_numbers() + instance.validate_serial_numbers() -def test_image_mgmt_validate_00005(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_validate_00005( + monkeypatch, image_validate, issu_details_by_serial_number +) -> None: """ Function - _wait_for_image_validate_to_complete @@ -191,31 +186,30 @@ def test_image_mgmt_validate_00005(monkeypatch, module, mock_issu_details) -> No In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ + instance = image_validate - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_validate_00005a" - return response_data_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 0 - module._wait_for_image_validate_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 2 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" in module.serial_numbers_done - + instance.check_interval = 0 + instance._wait_for_image_validate_to_complete() + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 2 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" in instance.serial_numbers_done -# test_image_mgmt_validate_00006 -# test_wait_for_image_validate_to_complete_validate_failed (former name) - -def test_image_mgmt_validate_00006(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_validate_00006( + monkeypatch, image_validate, issu_details_by_serial_number +) -> None: """ Function - _wait_for_image_validate_to_complete @@ -234,19 +228,20 @@ def test_image_mgmt_validate_00006(monkeypatch, module, mock_issu_details) -> No In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ + instance = image_validate - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_validate_00006a" - return response_data_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 0 + instance.check_interval = 0 match = "Seconds remaining 1800: validate image Failed for " match += "cvd-2313-leaf, 172.22.150.108, FDO2112189M, " match += "image validated percent: 100. Check the switch e.g. " @@ -254,15 +249,18 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += ". Or check Operations > Image Management > " match += "Devices > View Details > Validate on the controller " match += "GUI for more details." + with pytest.raises(AnsibleFailJson, match=match): - module._wait_for_image_validate_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 1 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" not in module.serial_numbers_done + instance._wait_for_image_validate_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 1 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_validate_00007(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_validate_00007( + monkeypatch, image_validate, issu_details_by_serial_number +) -> None: """ Function - _wait_for_image_validate_to_complete @@ -278,34 +276,38 @@ def test_image_mgmt_validate_00007(monkeypatch, module, mock_issu_details) -> No Description See test_wait_for_image_stage_to_complete for functional details. """ + instance = image_validate - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_validate_00007a" - return response_data_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 1 - module.check_timeout = 1 - - error_message = "ImageValidate._wait_for_image_validate_to_complete: " - error_message += "Timed out waiting for image validation to complete. " - error_message += "serial_numbers_done: FDO21120U5D, " - error_message += "serial_numbers_todo: FDO21120U5D,FDO2112189M" - with pytest.raises(AnsibleFailJson, match=error_message): - module._wait_for_image_validate_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 1 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" not in module.serial_numbers_done - - -def test_image_mgmt_validate_00008(monkeypatch, module, mock_issu_details) -> None: + instance.check_interval = 1 + instance.check_timeout = 1 + + match = "ImageValidate._wait_for_image_validate_to_complete: " + match += "Timed out waiting for image validation to complete. " + match += "serial_numbers_done: FDO21120U5D, " + match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + + with pytest.raises(AnsibleFailJson, match=match): + instance._wait_for_image_validate_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 1 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" not in instance.serial_numbers_done + + +def test_image_mgmt_validate_00008( + monkeypatch, image_validate, issu_details_by_serial_number +) -> None: """ Function - _wait_for_current_actions_to_complete @@ -326,27 +328,30 @@ def test_image_mgmt_validate_00008(monkeypatch, module, mock_issu_details) -> No ["imageStaged", "upgrade", "validated"] """ + instance = image_validate - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_validate_00008a" - return response_data_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 0 - module._wait_for_current_actions_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 2 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" in module.serial_numbers_done + instance.check_interval = 0 + instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 2 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_validate_00009(monkeypatch, module, mock_issu_details) -> None: +def test_image_mgmt_validate_00009( + monkeypatch, image_validate, issu_details_by_serial_number +) -> None: """ Function - _wait_for_current_actions_to_complete @@ -362,28 +367,30 @@ def test_image_mgmt_validate_00009(monkeypatch, module, mock_issu_details) -> No Description See test_wait_for_current_actions_to_complete for functional details. """ + instance = image_validate - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_validate_00009a" - return response_data_issu_details(key) + return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - module.issu_detail = mock_issu_details - module.serial_numbers = [ + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ "FDO21120U5D", "FDO2112189M", ] - module.check_interval = 1 - module.check_timeout = 1 + instance.check_interval = 1 + instance.check_timeout = 1 match = "ImageValidate._wait_for_current_actions_to_complete: " match += "Timed out waiting for actions to complete. " match += "serial_numbers_done: FDO21120U5D, " match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + with pytest.raises(AnsibleFailJson, match=match): - module._wait_for_current_actions_to_complete() - assert isinstance(module.serial_numbers_done, set) - assert len(module.serial_numbers_done) == 1 - assert "FDO21120U5D" in module.serial_numbers_done - assert "FDO2112189M" not in module.serial_numbers_done + instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access + assert isinstance(instance.serial_numbers_done, set) + assert len(instance.serial_numbers_done) == 1 + assert "FDO21120U5D" in instance.serial_numbers_done + assert "FDO2112189M" not in instance.serial_numbers_done diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 3b12e1885..818de7949 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -12,9 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +""" +SwitchDetails - unit tests +""" + from __future__ import absolute_import, division, print_function -from contextlib import contextmanager from typing import Any, Dict import pytest @@ -23,42 +35,16 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ SwitchDetails -from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule +from .image_upgrade_utils import (does_not_raise, responses_switch_details, + switch_details_fixture) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of SwitchDetails -""" - - -@contextmanager -def does_not_raise(): - """ - A context manager that does not raise an exception. - """ - yield - - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." - -dcnm_send_switch_details = patch_image_mgmt + "switch_details.dcnm_send" - - -def responses_switch_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchDetails" - response = load_fixture(response_file).get(key) - print(f"responses_switch_details: {key} : {response}") - return response - -@pytest.fixture -def switch_details(): - return SwitchDetails(MockAnsibleModule) +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.dcnm_send" def test_image_mgmt_switch_details_00001(switch_details) -> None: @@ -69,9 +55,9 @@ def test_image_mgmt_switch_details_00001(switch_details) -> None: Test - Class attributes are initialized to expected values """ - switch_details.__init__(MockAnsibleModule) - assert isinstance(switch_details, SwitchDetails) - assert switch_details.class_name == "SwitchDetails" + instance = switch_details + assert isinstance(instance, SwitchDetails) + assert instance.class_name == "SwitchDetails" def test_image_mgmt_switch_details_00002(switch_details) -> None: @@ -82,12 +68,13 @@ def test_image_mgmt_switch_details_00002(switch_details) -> None: Test - Class properties are initialized to expected values """ - switch_details._init_properties() - assert isinstance(switch_details.properties, dict) - assert switch_details.properties.get("ip_address") == None - assert switch_details.properties.get("response_data") == None - assert switch_details.properties.get("response") == None - assert switch_details.properties.get("result") == None + instance = switch_details + + assert isinstance(instance.properties, dict) + assert instance.properties.get("ip_address") is None + assert instance.properties.get("response_data") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None def test_image_mgmt_switch_details_00020(monkeypatch, switch_details) -> None: @@ -98,17 +85,18 @@ def test_image_mgmt_switch_details_00020(monkeypatch, switch_details) -> None: Test - response_data, response, result are dictionaries """ + instance = switch_details def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00020a" return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) - switch_details.refresh() - assert isinstance(switch_details.response_data, dict) - assert isinstance(switch_details.result, dict) - assert isinstance(switch_details.response, dict) + instance.refresh() + assert isinstance(instance.response_data, dict) + assert isinstance(instance.result, dict) + assert isinstance(instance.response, dict) def test_image_mgmt_switch_details_00021(monkeypatch, switch_details) -> None: @@ -121,30 +109,31 @@ def test_image_mgmt_switch_details_00021(monkeypatch, switch_details) -> None: - ip_address is set - getter properties will return values specific to ip_address """ + instance = switch_details def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00021a" return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) - switch_details.refresh() - assert isinstance(switch_details.response_data, dict) - switch_details.ip_address = "172.22.150.110" - assert switch_details.hostname == "cvd-1111-bgw" - switch_details.ip_address = "172.22.150.111" + instance.refresh() + assert isinstance(instance.response_data, dict) + instance.ip_address = "172.22.150.110" + assert instance.hostname == "cvd-1111-bgw" + instance.ip_address = "172.22.150.111" # We use the above IP address to test the remaining properties - assert switch_details.fabric_name == "easy" - assert switch_details.hostname == "cvd-1112-bgw" - assert switch_details.logical_name == "cvd-1112-bgw" - assert switch_details.model == "N9K-C9504" + assert instance.fabric_name == "easy" + assert instance.hostname == "cvd-1112-bgw" + assert instance.logical_name == "cvd-1112-bgw" + assert instance.model == "N9K-C9504" # This is derived from "model" and is not in the controller response - assert switch_details.platform == "N9K" - assert switch_details.role == "border gateway" - assert switch_details.serial_number == "FOX2109PGD1" + assert instance.platform == "N9K" + assert instance.role == "border gateway" + assert instance.serial_number == "FOX2109PGD1" -match_00022 = "Unable to retrieve switch information from the controller." +MATCH_00022 = "Unable to retrieve switch information from the controller." @pytest.mark.parametrize( @@ -153,11 +142,11 @@ def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: ("test_image_mgmt_switch_details_00022a", does_not_raise()), ( "test_image_mgmt_switch_details_00022b", - pytest.raises(AnsibleFailJson, match=match_00022), + pytest.raises(AnsibleFailJson, match=MATCH_00022), ), ( "test_image_mgmt_switch_details_00022c", - pytest.raises(AnsibleFailJson, match=match_00022), + pytest.raises(AnsibleFailJson, match=MATCH_00022), ), ], ) @@ -181,14 +170,15 @@ def test_image_mgmt_switch_details_00022( - 500 RETURN_CODE, MESSAGE ~= "Internal Server Error" - result == {'found': False, 'success': False} """ + instance = switch_details def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) with expected: - switch_details.refresh() + instance.refresh() @pytest.mark.parametrize( @@ -233,16 +223,17 @@ def test_image_mgmt_switch_details_00023( - converts value to NoneType - returns value unchanged """ + instance = switch_details def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00023a" return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) - switch_details.refresh() - switch_details.ip_address = "172.22.150.110" - assert switch_details._get(item) == expected + instance.refresh() + instance.ip_address = "172.22.150.110" + assert instance._get(item) == expected def test_image_mgmt_switch_details_00024(monkeypatch, switch_details) -> None: @@ -262,19 +253,21 @@ def test_image_mgmt_switch_details_00024(monkeypatch, switch_details) -> None: It returns the value of the requested property if the user has set a known ip_address. """ + instance = switch_details def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00024a" return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) - switch_details.refresh() - switch_details.ip_address = "1.1.1.1" match = "SwitchDetails._get: 1.1.1.1 does not exist " match += "on the controller." + + instance.refresh() + instance.ip_address = "1.1.1.1" with pytest.raises(AnsibleFailJson, match=match): - switch_details._get("hostName") + instance._get("hostName") def test_image_mgmt_switch_details_00025(monkeypatch, switch_details) -> None: @@ -292,18 +285,20 @@ def test_image_mgmt_switch_details_00025(monkeypatch, switch_details) -> None: It raises AnsibleFailJson if the user has not set ip_address or if the ip_address is unknown, or if an unknown property name is queried. """ + instance = switch_details def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00025a" return responses_switch_details(key) - monkeypatch.setattr(dcnm_send_switch_details, mock_dcnm_send_switch_details) + monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) - switch_details.refresh() - switch_details.ip_address = "172.22.150.110" match = "SwitchDetails._get: 172.22.150.110 does not have a key named FOO." + + instance.refresh() + instance.ip_address = "172.22.150.110" with pytest.raises(AnsibleFailJson, match=match): - switch_details._get("FOO") + instance._get("FOO") # setters @@ -327,6 +322,8 @@ def test_image_mgmt_switch_details_00060( - return IP address, if set - return None, if not set """ + instance = switch_details + if ip_address_is_set: - switch_details.ip_address = "1.2.3.4" - assert switch_details.ip_address == expected + instance.ip_address = "1.2.3.4" + assert instance.ip_address == expected diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index f6b780d2b..7e98190a6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -12,79 +12,76 @@ # See the License for the specific language governing permissions and # limitations under the License. +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument + +""" +SwitchIssuDetailsByDeviceName - unit tests +""" + from __future__ import absolute_import, division, print_function -from contextlib import contextmanager from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule, does_not_raise, issu_details_by_device_name_fixture +from .image_upgrade_utils import (does_not_raise, + issu_details_by_device_name_fixture, + responses_switch_issu_details) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsByDeviceName -""" - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." - -dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" - - -def responses_switch_issu_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchIssuDetails" - response = load_fixture(response_file).get(key) - print(f"responses_switch_issu_details: {key} : {response}") - return response +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" -# @pytest.fixture -# def issu_details(): -# return SwitchIssuDetailsByDeviceName(MockAnsibleModule) - - -def test_image_mgmt_switch_issu_details_by_device_name_00001(issu_details_by_device_name) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00001( + issu_details_by_device_name, +) -> None: """ Function - __init__ Test - fail_json is not called - - issu_details_by_device_name.properties is a dict + - instance.properties is a dict """ with does_not_raise(): - issu_details_by_device_name.__init__(MockAnsibleModule) - assert isinstance(issu_details_by_device_name.properties, dict) + instance = issu_details_by_device_name + assert isinstance(instance.properties, dict) -def test_image_mgmt_switch_issu_details_by_device_name_00002(issu_details_by_device_name) -> None: +def test_image_mgmt_switch_issu_details_by_device_name_00002( + issu_details_by_device_name, +) -> None: """ Function - _init_properties Test - Class properties initialized to expected values - - issu_details_by_device_name.properties is a dict - - issu_details_by_device_name.action_keys is a set + - instance.properties is a dict + - instance.action_keys is a set - action_keys contains expected values """ + instance = issu_details_by_device_name action_keys = {"imageStaged", "upgrade", "validated"} - issu_details_by_device_name._init_properties() - assert isinstance(issu_details_by_device_name.properties, dict) - assert isinstance(issu_details_by_device_name.properties.get("action_keys"), set) - assert issu_details_by_device_name.properties.get("action_keys") == action_keys - assert issu_details_by_device_name.properties.get("response_data") == None - assert issu_details_by_device_name.properties.get("response") == None - assert issu_details_by_device_name.properties.get("result") == None - assert issu_details_by_device_name.properties.get("device_name") == None + assert isinstance(instance.properties, dict) + assert isinstance(instance.properties.get("action_keys"), set) + assert instance.properties.get("action_keys") == action_keys + assert instance.properties.get("response_data") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None + assert instance.properties.get("device_name") is None def test_image_mgmt_switch_issu_details_by_device_name_00020( @@ -95,20 +92,22 @@ def test_image_mgmt_switch_issu_details_by_device_name_00020( - refresh Test - - issu_details_by_device_name.response is a dict - - issu_details_by_device_name.response_data is a list + - instance.response is a dict + - instance.response_data is a list """ + instance = issu_details_by_device_name + key = "test_image_mgmt_switch_issu_details_by_device_name_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_device_name.refresh() - assert isinstance(issu_details_by_device_name.response, dict) - assert isinstance(issu_details_by_device_name.response_data, list) + instance.refresh() + assert isinstance(instance.response, dict) + assert isinstance(instance.response_data, list) def test_image_mgmt_switch_issu_details_by_device_name_00021( @@ -122,71 +121,73 @@ def test_image_mgmt_switch_issu_details_by_device_name_00021( - Properties are set based on device_name - Expected property values are returned """ + instance = issu_details_by_device_name + key = "test_image_mgmt_switch_issu_details_by_device_name_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_device_name.refresh() - issu_details_by_device_name.device_name = "leaf1" - assert issu_details_by_device_name.device_name == "leaf1" - assert issu_details_by_device_name.serial_number == "FDO21120U5D" + instance.refresh() + instance.device_name = "leaf1" + assert instance.device_name == "leaf1" + assert instance.serial_number == "FDO21120U5D" # change device_name to a different switch, expect different information - issu_details_by_device_name.device_name = "cvd-2313-leaf" - assert issu_details_by_device_name.device_name == "cvd-2313-leaf" - assert issu_details_by_device_name.serial_number == "FDO2112189M" + instance.device_name = "cvd-2313-leaf" + assert instance.device_name == "cvd-2313-leaf" + assert instance.serial_number == "FDO2112189M" # verify remaining properties using current device_name - assert issu_details_by_device_name.eth_switch_id == 39890 - assert issu_details_by_device_name.fabric == "hard" - assert issu_details_by_device_name.fcoe_enabled is False - assert issu_details_by_device_name.group == "hard" + assert instance.eth_switch_id == 39890 + assert instance.fabric == "hard" + assert instance.fcoe_enabled is False + assert instance.group == "hard" # NOTE: For "id" see switch_id below - assert issu_details_by_device_name.image_staged == "Success" - assert issu_details_by_device_name.image_staged_percent == 100 - assert issu_details_by_device_name.ip_address == "172.22.150.108" - assert issu_details_by_device_name.issu_allowed == None - assert issu_details_by_device_name.last_upg_action == "2023-Oct-06 03:43" - assert issu_details_by_device_name.mds is False - assert issu_details_by_device_name.mode == "Normal" - assert issu_details_by_device_name.model == "N9K-C93180YC-EX" - assert issu_details_by_device_name.model_type == 0 - assert issu_details_by_device_name.peer == None - assert issu_details_by_device_name.platform == "N9K" - assert issu_details_by_device_name.policy == "KR5M" - assert issu_details_by_device_name.reason == "Upgrade" - assert issu_details_by_device_name.role == "leaf" - assert issu_details_by_device_name.status == "In-Sync" - assert issu_details_by_device_name.status_percent == 100 + assert instance.image_staged == "Success" + assert instance.image_staged_percent == 100 + assert instance.ip_address == "172.22.150.108" + assert instance.issu_allowed is None + assert instance.last_upg_action == "2023-Oct-06 03:43" + assert instance.mds is False + assert instance.mode == "Normal" + assert instance.model == "N9K-C93180YC-EX" + assert instance.model_type == 0 + assert instance.peer is None + assert instance.platform == "N9K" + assert instance.policy == "KR5M" + assert instance.reason == "Upgrade" + assert instance.role == "leaf" + assert instance.status == "In-Sync" + assert instance.status_percent == 100 # NOTE: switch_id appears in the response data as "id" # NOTE: "id" is a python reserved keyword, so we changed the property name - assert issu_details_by_device_name.switch_id == 2 - assert issu_details_by_device_name.sys_name == "cvd-2313-leaf" - assert issu_details_by_device_name.system_mode == "Normal" - assert issu_details_by_device_name.upg_groups == None - assert issu_details_by_device_name.upgrade == "Success" - assert issu_details_by_device_name.upgrade_percent == 100 - assert issu_details_by_device_name.validated == "Success" - assert issu_details_by_device_name.validated_percent == 100 - assert issu_details_by_device_name.version == "10.2(5)" + assert instance.switch_id == 2 + assert instance.sys_name == "cvd-2313-leaf" + assert instance.system_mode == "Normal" + assert instance.upg_groups is None + assert instance.upgrade == "Success" + assert instance.upgrade_percent == 100 + assert instance.validated == "Success" + assert instance.validated_percent == 100 + assert instance.version == "10.2(5)" # NOTE: Two vdc_id values exist in the response data for each switch. # NOTE: Namely, "vdcId" and "vdc_id" # NOTE: Properties are provided for both, as follows. # NOTE: vdc_id == vdcId # NOTE: vdc_id2 == vdc_id - assert issu_details_by_device_name.vdc_id == 0 - assert issu_details_by_device_name.vdc_id2 == -1 - assert issu_details_by_device_name.vpc_peer == None + assert instance.vdc_id == 0 + assert instance.vdc_id2 == -1 + assert instance.vpc_peer is None # NOTE: Two vpc role keys exist in the response data for each switch. # NOTE: Namely, "vpcRole" and "vpc_role" # NOTE: Properties are provided for both, as follows. # NOTE: vpc_role == vpcRole # NOTE: vpc_role2 == vpc_role # NOTE: Values are synthesized in the response for this test - assert issu_details_by_device_name.vpc_role == "FOO" - assert issu_details_by_device_name.vpc_role2 == "BAR" + assert instance.vpc_role == "FOO" + assert instance.vpc_role2 == "BAR" def test_image_mgmt_switch_issu_details_by_device_name_00022( @@ -197,21 +198,23 @@ def test_image_mgmt_switch_issu_details_by_device_name_00022( - refresh Test - - issu_details_by_device_name.result is a dict - - issu_details_by_device_name.result contains expected key/values for 200 RESULT_CODE + - instance.result is a dict + - instance.result contains expected key/values for 200 RESULT_CODE """ + instance = issu_details_by_device_name + key = "test_image_mgmt_switch_issu_details_by_device_name_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_device_name.refresh() - assert isinstance(issu_details_by_device_name.result, dict) - assert issu_details_by_device_name.result.get("found") is True - assert issu_details_by_device_name.result.get("success") is True + instance.refresh() + assert isinstance(instance.result, dict) + assert instance.result.get("found") is True + assert instance.result.get("success") is True def test_image_mgmt_switch_issu_details_by_device_name_00023( @@ -225,16 +228,18 @@ def test_image_mgmt_switch_issu_details_by_device_name_00023( - refresh calls handle_response, which calls json_fail on 404 response - Error message matches expectation """ + instance = issu_details_by_device_name + key = "test_image_mgmt_switch_issu_details_by_device_name_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "Bad result when retriving switch information from the controller" with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_device_name.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_device_name_00024( @@ -248,17 +253,19 @@ def test_image_mgmt_switch_issu_details_by_device_name_00024( - fail_json is called on 200 response with empty DATA key - Error message matches expectation """ + instance = issu_details_by_device_name + key = "test_image_mgmt_switch_issu_details_by_device_name_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "SwitchIssuDetailsByDeviceName.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_device_name.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_device_name_00025( @@ -272,18 +279,20 @@ def test_image_mgmt_switch_issu_details_by_device_name_00025( - fail_json is called on 200 response with DATA.lastOperDataObject length 0 - Error message matches expectation """ + instance = issu_details_by_device_name + key = "test_image_mgmt_switch_issu_details_by_device_name_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "SwitchIssuDetailsByDeviceName.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_device_name.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_device_name_00040( @@ -303,19 +312,20 @@ def test_image_mgmt_switch_issu_details_by_device_name_00040( It returns the value of the requested property if the user has set a known device_name. """ + instance = issu_details_by_device_name def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_issu_details_by_device_name_00040a" return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_device_name.refresh() - issu_details_by_device_name.device_name = "FOO" + instance.refresh() + instance.device_name = "FOO" match = "SwitchIssuDetailsByDeviceName._get: FOO does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_device_name._get("serialNumber") + instance._get("serialNumber") # pylint: disable=protected-access def test_image_mgmt_switch_issu_details_by_device_name_00041( @@ -336,16 +346,17 @@ def test_image_mgmt_switch_issu_details_by_device_name_00041( It returns the value of the requested property if the user has set a known ip_address. """ + instance = issu_details_by_device_name def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_issu_details_by_device_name_00041a" return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_device_name.refresh() - issu_details_by_device_name.device_name = "leaf1" + instance.refresh() + instance.device_name = "leaf1" match = "SwitchIssuDetailsByDeviceName._get: leaf1 unknown " - match += f"property name: FOO" + match += "property name: FOO" with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_device_name._get("FOO") + instance._get("FOO") # pylint: disable=protected-access diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 65c6bb83d..de2ff2bdf 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -12,6 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument + +""" +SwitchIssuDetailsByIpAddress - unit tests +""" + from __future__ import absolute_import, division, print_function from typing import Any, Dict @@ -20,67 +31,60 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule, does_not_raise, issu_details_by_ip_address_fixture - +from .image_upgrade_utils import (does_not_raise, + issu_details_by_ip_address_fixture, + responses_switch_issu_details) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsByIpAddress -""" - - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." - -dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" -def responses_switch_issu_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchIssuDetails" - response = load_fixture(response_file).get(key) - print(f"responses_switch_issu_details: {key} : {response}") - return response - -def test_image_mgmt_switch_issu_details_by_ip_address_00001(issu_details_by_ip_address) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00001( + issu_details_by_ip_address, +) -> None: """ Function - __init__ Test - fail_json is not called - - issu_details_by_ip_address.properties is a dict + - instance.properties is a dict """ with does_not_raise(): - issu_details_by_ip_address.__init__(MockAnsibleModule) - assert isinstance(issu_details_by_ip_address.properties, dict) + instance = issu_details_by_ip_address + assert isinstance(instance.properties, dict) -def test_image_mgmt_switch_issu_details_by_ip_address_00002(issu_details_by_ip_address) -> None: +def test_image_mgmt_switch_issu_details_by_ip_address_00002( + issu_details_by_ip_address, +) -> None: """ Function - _init_properties Test - Class properties initialized to expected values - - issu_details_by_ip_address.properties is a dict - - issu_details_by_ip_address.action_keys is a set + - instance.properties is a dict + - instance.action_keys is a set - action_keys contains expected values """ + instance = issu_details_by_ip_address + action_keys = {"imageStaged", "upgrade", "validated"} - issu_details_by_ip_address._init_properties() - assert isinstance(issu_details_by_ip_address.properties, dict) - assert isinstance(issu_details_by_ip_address.properties.get("action_keys"), set) - assert issu_details_by_ip_address.properties.get("action_keys") == action_keys - assert issu_details_by_ip_address.properties.get("response_data") is None - assert issu_details_by_ip_address.properties.get("response") is None - assert issu_details_by_ip_address.properties.get("result") is None - assert issu_details_by_ip_address.properties.get("ip_address") is None + instance._init_properties() # pylint: disable=protected-access + assert isinstance(instance.properties, dict) + assert isinstance(instance.properties.get("action_keys"), set) + assert instance.properties.get("action_keys") == action_keys + assert instance.properties.get("response_data") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None + assert instance.properties.get("ip_address") is None def test_image_mgmt_switch_issu_details_by_ip_address_00020( @@ -91,20 +95,22 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00020( - refresh Test - - issu_details_by_ip_address.response is a dict - - issu_details_by_ip_address.response_data is a list + - instance.response is a dict + - instance.response_data is a list """ + instance = issu_details_by_ip_address + key = "test_image_mgmt_switch_issu_details_by_ip_address_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_ip_address.refresh() - assert isinstance(issu_details_by_ip_address.response, dict) - assert isinstance(issu_details_by_ip_address.response_data, list) + instance.refresh() + assert isinstance(instance.response, dict) + assert isinstance(instance.response_data, list) def test_image_mgmt_switch_issu_details_by_ip_address_00021( @@ -118,71 +124,73 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00021( - Properties are set based on device_name - Expected property values are returned """ + instance = issu_details_by_ip_address + key = "test_image_mgmt_switch_issu_details_by_ip_address_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_ip_address.refresh() - issu_details_by_ip_address.ip_address = "172.22.150.102" - assert issu_details_by_ip_address.device_name == "leaf1" - assert issu_details_by_ip_address.serial_number == "FDO21120U5D" + instance.refresh() + instance.ip_address = "172.22.150.102" + assert instance.device_name == "leaf1" + assert instance.serial_number == "FDO21120U5D" # change ip_address to a different switch, expect different information - issu_details_by_ip_address.ip_address = "172.22.150.108" - assert issu_details_by_ip_address.device_name == "cvd-2313-leaf" - assert issu_details_by_ip_address.serial_number == "FDO2112189M" + instance.ip_address = "172.22.150.108" + assert instance.device_name == "cvd-2313-leaf" + assert instance.serial_number == "FDO2112189M" # verify remaining properties using current ip_address - assert issu_details_by_ip_address.eth_switch_id == 39890 - assert issu_details_by_ip_address.fabric == "hard" - assert issu_details_by_ip_address.fcoe_enabled is False - assert issu_details_by_ip_address.group == "hard" + assert instance.eth_switch_id == 39890 + assert instance.fabric == "hard" + assert instance.fcoe_enabled is False + assert instance.group == "hard" # NOTE: For "id" see switch_id below - assert issu_details_by_ip_address.image_staged == "Success" - assert issu_details_by_ip_address.image_staged_percent == 100 - assert issu_details_by_ip_address.ip_address == "172.22.150.108" - assert issu_details_by_ip_address.issu_allowed is None - assert issu_details_by_ip_address.last_upg_action == "2023-Oct-06 03:43" - assert issu_details_by_ip_address.mds is False - assert issu_details_by_ip_address.mode == "Normal" - assert issu_details_by_ip_address.model == "N9K-C93180YC-EX" - assert issu_details_by_ip_address.model_type == 0 - assert issu_details_by_ip_address.peer is None - assert issu_details_by_ip_address.platform == "N9K" - assert issu_details_by_ip_address.policy == "KR5M" - assert issu_details_by_ip_address.reason == "Upgrade" - assert issu_details_by_ip_address.role == "leaf" - assert issu_details_by_ip_address.status == "In-Sync" - assert issu_details_by_ip_address.status_percent == 100 + assert instance.image_staged == "Success" + assert instance.image_staged_percent == 100 + assert instance.ip_address == "172.22.150.108" + assert instance.issu_allowed is None + assert instance.last_upg_action == "2023-Oct-06 03:43" + assert instance.mds is False + assert instance.mode == "Normal" + assert instance.model == "N9K-C93180YC-EX" + assert instance.model_type == 0 + assert instance.peer is None + assert instance.platform == "N9K" + assert instance.policy == "KR5M" + assert instance.reason == "Upgrade" + assert instance.role == "leaf" + assert instance.status == "In-Sync" + assert instance.status_percent == 100 # NOTE: switch_id appears in the response data as "id" # NOTE: "id" is a python reserved keyword, so we changed the property name - assert issu_details_by_ip_address.switch_id == 2 - assert issu_details_by_ip_address.sys_name == "cvd-2313-leaf" - assert issu_details_by_ip_address.system_mode == "Normal" - assert issu_details_by_ip_address.upg_groups is None - assert issu_details_by_ip_address.upgrade == "Success" - assert issu_details_by_ip_address.upgrade_percent == 100 - assert issu_details_by_ip_address.validated == "Success" - assert issu_details_by_ip_address.validated_percent == 100 - assert issu_details_by_ip_address.version == "10.2(5)" + assert instance.switch_id == 2 + assert instance.sys_name == "cvd-2313-leaf" + assert instance.system_mode == "Normal" + assert instance.upg_groups is None + assert instance.upgrade == "Success" + assert instance.upgrade_percent == 100 + assert instance.validated == "Success" + assert instance.validated_percent == 100 + assert instance.version == "10.2(5)" # NOTE: Two vdc_id values exist in the response data for each switch. # NOTE: Namely, "vdcId" and "vdc_id" # NOTE: Properties are provided for both, as follows. # NOTE: vdc_id == vdcId # NOTE: vdc_id2 == vdc_id - assert issu_details_by_ip_address.vdc_id == 0 - assert issu_details_by_ip_address.vdc_id2 == -1 - assert issu_details_by_ip_address.vpc_peer is None + assert instance.vdc_id == 0 + assert instance.vdc_id2 == -1 + assert instance.vpc_peer is None # NOTE: Two vpc role keys exist in the response data for each switch. # NOTE: Namely, "vpcRole" and "vpc_role" # NOTE: Properties are provided for both, as follows. # NOTE: vpc_role == vpcRole # NOTE: vpc_role2 == vpc_role # NOTE: Values are synthesized in the response for this test - assert issu_details_by_ip_address.vpc_role == "FOO" - assert issu_details_by_ip_address.vpc_role2 == "BAR" + assert instance.vpc_role == "FOO" + assert instance.vpc_role2 == "BAR" def test_image_mgmt_switch_issu_details_by_ip_address_00022( @@ -193,21 +201,23 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00022( - refresh Test - - issu_details_by_ip_address.result is a dict - - issu_details_by_ip_address.result contains expected key/values for 200 RESULT_CODE + - instance.result is a dict + - instance.result contains expected key/values for 200 RESULT_CODE """ + instance = issu_details_by_ip_address + key = "test_image_mgmt_switch_issu_details_by_ip_address_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_ip_address.refresh() - assert isinstance(issu_details_by_ip_address.result, dict) - assert issu_details_by_ip_address.result.get("found") is True - assert issu_details_by_ip_address.result.get("success") is True + instance.refresh() + assert isinstance(instance.result, dict) + assert instance.result.get("found") is True + assert instance.result.get("success") is True def test_image_mgmt_switch_issu_details_by_ip_address_00023( @@ -221,17 +231,19 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00023( - refresh calls handle_response, which calls json_fail on 404 response - Error message matches expectation """ + instance = issu_details_by_ip_address + key = "test_image_mgmt_switch_issu_details_by_ip_address_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "Bad result when retriving switch information from the controller" with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_ip_address.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_ip_address_00024( @@ -245,17 +257,19 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00024( - fail_json is called on 200 response with empty DATA key - Error message matches expectation """ + instance = issu_details_by_ip_address + key = "test_image_mgmt_switch_issu_details_by_ip_address_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "SwitchIssuDetailsByIpAddress.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_ip_address.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_ip_address_00025( @@ -269,18 +283,20 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00025( - fail_json is called on 200 response with DATA.lastOperDataObject length 0 - Error message matches expectation """ + instance = issu_details_by_ip_address + key = "test_image_mgmt_switch_issu_details_by_ip_address_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "SwitchIssuDetailsByIpAddress.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_ip_address.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_ip_address_00040( @@ -300,19 +316,20 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00040( 1. fail_json is called with appropriate error message since an unknown ip_address is set. """ + instance = issu_details_by_ip_address def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_issu_details_by_ip_address_00040a" return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_ip_address.refresh() - issu_details_by_ip_address.ip_address = "1.1.1.1" + instance.refresh() + instance.ip_address = "1.1.1.1" match = "SwitchIssuDetailsByIpAddress._get: 1.1.1.1 does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_ip_address._get("serialNumber") + instance._get("serialNumber") # pylint: disable=protected-access def test_image_mgmt_switch_issu_details_by_ip_address_00041( @@ -332,16 +349,17 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00041( 1. fail_json is called with appropriate error message since an unknown property is queried. """ + instance = issu_details_by_ip_address def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_issu_details_by_ip_address_00041a" return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details_by_ip_address.refresh() - issu_details_by_ip_address.ip_address = "172.22.150.102" + instance.refresh() + instance.ip_address = "172.22.150.102" match = "SwitchIssuDetailsByIpAddress._get: 172.22.150.102 unknown " - match += f"property name: FOO" + match += "property name: FOO" with pytest.raises(AnsibleFailJson, match=match): - issu_details_by_ip_address._get("FOO") + instance._get("FOO") # pylint: disable=protected-access diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 9ab020e55..4449f224d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -12,119 +12,109 @@ # See the License for the specific language governing permissions and # limitations under the License. +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument + +""" +SwitchIssuDetailsBySerialNumber - unit tests +""" + from __future__ import absolute_import, division, print_function -from contextlib import contextmanager from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ - SwitchIssuDetailsBySerialNumber -from .fixture import load_fixture -from .image_upgrade_utils import MockAnsibleModule, issu_details_by_serial_number_fixture +from .image_upgrade_utils import (does_not_raise, + issu_details_by_serial_number_fixture, + responses_switch_issu_details) __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -""" -controller_version: 12 -description: Verify functionality of subclass SwitchIssuDetailsBySerialNumber -""" - - -@contextmanager -def does_not_raise(): - """ - A context manager that does not raise an exception. - """ - yield - - -patch_module_utils = "ansible_collections.cisco.dcnm.plugins.module_utils." -patch_image_mgmt = patch_module_utils + "image_mgmt." - -dcnm_send_issu_details = patch_image_mgmt + "switch_issu_details.dcnm_send" - - - -def responses_switch_issu_details(key: str) -> Dict[str, str]: - response_file = f"image_upgrade_responses_SwitchIssuDetails" - response = load_fixture(response_file).get(key) - print(f"responses_switch_issu_details: {key} : {response}") - return response +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" -@pytest.fixture -def issu_details(): - return SwitchIssuDetailsBySerialNumber(MockAnsibleModule) - -def test_image_mgmt_switch_issu_details_by_serial_number_00001(issu_details) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00001( + issu_details_by_serial_number, +) -> None: """ Function - __init__ Test - fail_json is not called - - issu_details.properties is a dict + - instance.properties is a dict """ with does_not_raise(): - issu_details.__init__(MockAnsibleModule) - assert isinstance(issu_details.properties, dict) + instance = issu_details_by_serial_number + assert isinstance(instance.properties, dict) -def test_image_mgmt_switch_issu_details_by_serial_number_00002(issu_details) -> None: +def test_image_mgmt_switch_issu_details_by_serial_number_00002( + issu_details_by_serial_number, +) -> None: """ Function - _init_properties Test - Class properties initialized to expected values - - issu_details.properties is a dict - - issu_details.action_keys is a set + - instance.properties is a dict + - instance.action_keys is a set - action_keys contains expected values """ + instance = issu_details_by_serial_number + action_keys = {"imageStaged", "upgrade", "validated"} - issu_details._init_properties() - assert isinstance(issu_details.properties, dict) - assert isinstance(issu_details.properties.get("action_keys"), set) - assert issu_details.properties.get("action_keys") == action_keys - assert issu_details.properties.get("response_data") == None - assert issu_details.properties.get("response") == None - assert issu_details.properties.get("result") == None - assert issu_details.properties.get("serial_number") == None + instance._init_properties() # pylint: disable=protected-access + assert isinstance(instance.properties, dict) + assert isinstance(instance.properties.get("action_keys"), set) + assert instance.properties.get("action_keys") == action_keys + assert instance.properties.get("response_data") is None + assert instance.properties.get("response") is None + assert instance.properties.get("result") is None + assert instance.properties.get("serial_number") is None def test_image_mgmt_switch_issu_details_by_serial_number_00020( - monkeypatch, issu_details + monkeypatch, issu_details_by_serial_number ) -> None: """ Function - refresh Test - - issu_details.response is a dict - - issu_details.response_data is a list + - instance.response is a dict + - instance.response_data is a list """ + instance = issu_details_by_serial_number + key = "test_image_mgmt_switch_issu_details_by_serial_number_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details.refresh() - assert isinstance(issu_details.response, dict) - assert isinstance(issu_details.response_data, list) + instance.refresh() + assert isinstance(instance.response, dict) + assert isinstance(instance.response_data, list) def test_image_mgmt_switch_issu_details_by_serial_number_00021( - monkeypatch, issu_details + monkeypatch, issu_details_by_serial_number ) -> None: """ Function @@ -134,100 +124,102 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00021( - Properties are set based on device_name - Expected property values are returned """ + instance = issu_details_by_serial_number + key = "test_image_mgmt_switch_issu_details_by_serial_number_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.serial_number = "FDO21120U5D" - assert issu_details.device_name == "leaf1" - assert issu_details.serial_number == "FDO21120U5D" + instance.refresh() + instance.serial_number = "FDO21120U5D" + assert instance.device_name == "leaf1" + assert instance.serial_number == "FDO21120U5D" # change serial_number to a different switch, expect different information - issu_details.serial_number = "FDO2112189M" - assert issu_details.device_name == "cvd-2313-leaf" - assert issu_details.serial_number == "FDO2112189M" + instance.serial_number = "FDO2112189M" + assert instance.device_name == "cvd-2313-leaf" + assert instance.serial_number == "FDO2112189M" # verify remaining properties using current serial_number - assert issu_details.eth_switch_id == 39890 - assert issu_details.fabric == "hard" - assert issu_details.fcoe_enabled is False - assert issu_details.group == "hard" + assert instance.eth_switch_id == 39890 + assert instance.fabric == "hard" + assert instance.fcoe_enabled is False + assert instance.group == "hard" # NOTE: For "id" see switch_id below - assert issu_details.image_staged == "Success" - assert issu_details.image_staged_percent == 100 - assert issu_details.ip_address == "172.22.150.108" - assert issu_details.issu_allowed == None - assert issu_details.last_upg_action == "2023-Oct-06 03:43" - assert issu_details.mds is False - assert issu_details.mode == "Normal" - assert issu_details.model == "N9K-C93180YC-EX" - assert issu_details.model_type == 0 - assert issu_details.peer == None - assert issu_details.platform == "N9K" - assert issu_details.policy == "KR5M" - assert issu_details.reason == "Upgrade" - assert issu_details.role == "leaf" - assert issu_details.status == "In-Sync" - assert issu_details.status_percent == 100 + assert instance.image_staged == "Success" + assert instance.image_staged_percent == 100 + assert instance.ip_address == "172.22.150.108" + assert instance.issu_allowed is None + assert instance.last_upg_action == "2023-Oct-06 03:43" + assert instance.mds is False + assert instance.mode == "Normal" + assert instance.model == "N9K-C93180YC-EX" + assert instance.model_type == 0 + assert instance.peer is None + assert instance.platform == "N9K" + assert instance.policy == "KR5M" + assert instance.reason == "Upgrade" + assert instance.role == "leaf" + assert instance.status == "In-Sync" + assert instance.status_percent == 100 # NOTE: switch_id appears in the response data as "id" # NOTE: "id" is a python reserved keyword, so we changed the property name - assert issu_details.switch_id == 2 - assert issu_details.sys_name == "cvd-2313-leaf" - assert issu_details.system_mode == "Normal" - assert issu_details.upg_groups == None - assert issu_details.upgrade == "Success" - assert issu_details.upgrade_percent == 100 - assert issu_details.validated == "Success" - assert issu_details.validated_percent == 100 - assert issu_details.version == "10.2(5)" + assert instance.switch_id == 2 + assert instance.sys_name == "cvd-2313-leaf" + assert instance.system_mode == "Normal" + assert instance.upg_groups is None + assert instance.upgrade == "Success" + assert instance.upgrade_percent == 100 + assert instance.validated == "Success" + assert instance.validated_percent == 100 + assert instance.version == "10.2(5)" # NOTE: Two vdc_id values exist in the response data for each switch. # NOTE: Namely, "vdcId" and "vdc_id" # NOTE: Properties are provided for both, as follows. # NOTE: vdc_id == vdcId # NOTE: vdc_id2 == vdc_id - assert issu_details.vdc_id == 0 - assert issu_details.vdc_id2 == -1 - assert issu_details.vpc_peer == None + assert instance.vdc_id == 0 + assert instance.vdc_id2 == -1 + assert instance.vpc_peer is None # NOTE: Two vpc role keys exist in the response data for each switch. # NOTE: Namely, "vpcRole" and "vpc_role" # NOTE: Properties are provided for both, as follows. # NOTE: vpc_role == vpcRole # NOTE: vpc_role2 == vpc_role # NOTE: Values are synthesized in the response for this test - assert issu_details.vpc_role == "FOO" - assert issu_details.vpc_role2 == "BAR" + assert instance.vpc_role == "FOO" + assert instance.vpc_role2 == "BAR" def test_image_mgmt_switch_issu_details_by_serial_number_00022( - monkeypatch, issu_details + monkeypatch, issu_details_by_serial_number ) -> None: """ Function - refresh Test - - issu_details.result is a dict - - issu_details.result contains expected key/values for 200 RESULT_CODE + - instance.result is a dict + - instance.result contains expected key/values for 200 RESULT_CODE """ + instance = issu_details_by_serial_number + key = "test_image_mgmt_switch_issu_details_by_serial_number_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details.refresh() - assert isinstance(issu_details.result, dict) - assert issu_details.result.get("found") is True - assert issu_details.result.get("success") is True + instance.refresh() + assert isinstance(instance.result, dict) + assert instance.result.get("found") is True + assert instance.result.get("success") is True def test_image_mgmt_switch_issu_details_by_serial_number_00023( - monkeypatch, issu_details + monkeypatch, issu_details_by_serial_number ) -> None: """ Function @@ -237,21 +229,22 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00023( - refresh calls handle_response, which calls json_fail on 404 response - Error message matches expectation """ + instance = issu_details_by_serial_number + key = "test_image_mgmt_switch_issu_details_by_serial_number_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "Bad result when retriving switch information from the controller" with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_serial_number_00024( - monkeypatch, issu_details + monkeypatch, issu_details_by_serial_number ) -> None: """ Function @@ -261,21 +254,23 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00024( - fail_json is called on 200 response with empty DATA key - Error message matches expectation """ + instance = issu_details_by_serial_number + key = "test_image_mgmt_switch_issu_details_by_serial_number_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "SwitchIssuDetailsBySerialNumber.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_serial_number_00025( - monkeypatch, issu_details + monkeypatch, issu_details_by_serial_number ) -> None: """ Function @@ -285,22 +280,23 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00025( - fail_json is called on 200 response with DATA.lastOperDataObject length 0 - Error message matches expectation """ + instance = issu_details_by_serial_number + key = "test_image_mgmt_switch_issu_details_by_serial_number_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) match = "SwitchIssuDetailsBySerialNumber.refresh: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): - issu_details.refresh() + instance.refresh() def test_image_mgmt_switch_issu_details_by_serial_number_00040( - monkeypatch, issu_details + monkeypatch, issu_details_by_serial_number ) -> None: """ Function description: @@ -316,23 +312,25 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00040( 1. fail_json is called with appropriate error message since an unknown serial_number is set. """ + instance = issu_details_by_serial_number def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_issu_details_by_serial_number_00040a" return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.serial_number = "FOO00000BAR" match = "SwitchIssuDetailsBySerialNumber._get: FOO00000BAR does not exist " match += "on the controller." + + instance.refresh() + instance.serial_number = "FOO00000BAR" with pytest.raises(AnsibleFailJson, match=match): - issu_details._get("serialNumber") + instance._get("serialNumber") # pylint: disable=protected-access def test_image_mgmt_switch_issu_details_by_serial_number_00041( - monkeypatch, issu_details + monkeypatch, issu_details_by_serial_number ) -> None: """ Function description: @@ -348,16 +346,18 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00041( 1. fail_json is called with appropriate error message since an unknown property is queried. """ + instance = issu_details_by_serial_number def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_issu_details_by_serial_number_00041a" return responses_switch_issu_details(key) - monkeypatch.setattr(dcnm_send_issu_details, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - issu_details.refresh() - issu_details.serial_number = "FDO21120U5D" match = "SwitchIssuDetailsBySerialNumber._get: FDO21120U5D unknown " - match += f"property name: FOO" + match += "property name: FOO" + + instance.refresh() + instance.serial_number = "FDO21120U5D" with pytest.raises(AnsibleFailJson, match=match): - issu_details._get("FOO") + instance._get("FOO") # pylint: disable=protected-access From f33062c6f68c17c49abe0858f8974c6bca304045 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 17 Nov 2023 09:48:39 -1000 Subject: [PATCH 127/300] Use load_playbook_config for all inputs, more... - More cleanup for pylint - image_upgrade.py - Add comment about key misspelling - Update TEST_NOTES for some inputs - Run thru black, isort, pylint --- .../module_utils/image_mgmt/api_endpoints.py | 2 +- .../module_utils/image_mgmt/image_policies.py | 6 +- .../image_mgmt/image_policy_action.py | 8 +- .../module_utils/image_mgmt/image_stage.py | 2 +- .../module_utils/image_mgmt/image_upgrade.py | 113 ++++++++---------- .../image_mgmt/image_upgrade_common.py | 6 +- .../module_utils/image_mgmt/image_validate.py | 2 +- .../image_mgmt/install_options.py | 2 +- .../module_utils/image_mgmt/switch_details.py | 2 +- .../image_mgmt/switch_issu_details.py | 2 +- plugins/modules/dcnm_image_upgrade.py | 71 +++++------ .../image_upgrade_playbook_configs.json | 37 ++++++ ...e_upgrade_responses_SwitchIssuDetails.json | 7 +- .../test_image_upgrade_image_upgrade.py | 49 ++++---- ...pgrade_image_upgrade_image_upgrade_task.py | 33 +++-- 15 files changed, 185 insertions(+), 157 deletions(-) diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py index 322be540d..550cdc925 100644 --- a/plugins/module_utils/image_mgmt/api_endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name class ApiEndpoints: diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 61a200bfc..448954e5a 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name import inspect @@ -43,12 +43,12 @@ class ImagePolicies(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): - self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties = {} self.properties["policy_name"] = None self.properties["response_data"] = None diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index d1ec6652d..821bd889f 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name import inspect import json @@ -52,7 +52,7 @@ class ImagePolicyAction(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() self._init_properties() self.image_policies = ImagePolicies(self.module) @@ -63,7 +63,7 @@ def __init__(self, module): self.verb = None def _init_properties(self): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties = {} self.properties["action"] = None self.properties["response"] = None @@ -322,7 +322,7 @@ def policy_name(self): @policy_name.setter def policy_name(self, value): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties["policy_name"] = value @property diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index c56a7baed..58af6cb11 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name import copy import inspect diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 47a281841..92dd57daa 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -121,9 +121,14 @@ class ImageUpgrade(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = self.__class__.__name__ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() + self.ipv4_done = set() + self.ipv4_todo = set() + self.payload: Dict[str, Any] = {} + self.path = None + self.verb = None self._init_defaults() self._init_properties() @@ -132,7 +137,7 @@ def __init__(self, module): self.log_msg("DEBUG: ImageUpgrade.__init__ DONE") def _init_defaults(self) -> None: - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.defaults: Dict[str, Any] = {} self.defaults["options"] = {} @@ -156,15 +161,19 @@ def _init_defaults(self) -> None: self.defaults["validate"] = True def _init_properties(self) -> None: - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + """ + Initialize properties used by this class. + + Review these later since we are no longer calling this class + per-switch given the payload structure is not amenable to that. + Consider removing some of these. + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable # self.ip_addresses is used in: # self._wait_for_current_actions_to_complete() # self._wait_for_image_upgrade_to_complete() self.ip_addresses: Set[str] = set() - # TODO:1 Review these properties since we are no longer - # calling this class per-switch given the payload structure - # is not amenable to that. self.properties: Dict[str, Any] = {} self.properties["bios_force"] = False self.properties["check_interval"] = 10 # seconds @@ -190,46 +199,18 @@ def _init_properties(self) -> None: self.valid_nxos_mode.add("non_disruptive") self.valid_nxos_mode.add("force_non_disruptive") - # def prune_devices(self): - # """ - # If the image is already upgraded on a device, remove that device - # from self.devices. self.devices dict has already been validated, - # so no further error checking is needed here. - - # TODO:1 This prunes devices only based on the image upgrade state. - # TODO:1 It does not check other image states and EPLD states. - # """ - # # issu = SwitchIssuDetailsBySerialNumber(self.module) - # pruned_devices = set() - # instance = SwitchIssuDetailsByIpAddress(self.module) - # instance.refresh() - # for device in self.devices: - # msg = f"REMOVE: {self.class_name}.prune_devices() device: {device}" - # self.log_msg(msg) - # instance.ip_address = device.get("ip_address") - # instance.refresh() - # if instance.upgrade == "Success": - # msg = f"REMOVE: {self.class_name}.prune_devices: " - # msg = "image already upgraded for " - # msg += f"{instance.device_name}, " - # msg += f"{instance.serial_number}, " - # msg += f"{instance.ip_address}" - # self.log_msg(msg) - # pruned_devices.add(instance.ip_address) - # self.devices = [ - # device - # for device in self.devices - # if device.get("ip_address") not in pruned_devices - # ] - - def validate_devices(self) -> None: + # We used to have a prune_devices() method here, but this + # is now done in dcnm_image_upgrade.py. Consider moving + # that code here later. + + def _validate_devices(self) -> None: """ 1. Perform any pre-upgrade validations (currently none) 2. Populate self.ip_addresses with the ip_address of all switches which can be upgraded. This is used in _wait_for_current_actions_to_complete """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable for device in self.devices: self.issu_detail.ip_address = device.get("ip_address") @@ -246,7 +227,7 @@ def validate_devices(self) -> None: self.ip_addresses.add(str(self.issu_detail.ip_address)) def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable if config.get("stage") is None: config["stage"] = self.defaults["stage"] @@ -302,7 +283,7 @@ def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: ]["uninstall"] return config - def build_payload(self, device) -> None: + def _build_payload(self, device) -> None: """ Build the request payload to upgrade the switches. """ @@ -344,23 +325,23 @@ def build_payload(self, device) -> None: self.payload: Dict[str, Any] = {} self.payload["devices"] = devices_to_upgrade - self.build_payload_issu_upgrade(device) - self.build_payload_issu_options_1(device) - self.build_payload_issu_options_2(device) - self.build_payload_epld(device) - self.build_payload_reboot(device) - self.build_payload_reboot_options(device) - self.build_payload_package(device) + self._build_payload_issu_upgrade(device) + self._build_payload_issu_options_1(device) + self._build_payload_issu_options_2(device) + self._build_payload_epld(device) + self._build_payload_reboot(device) + self._build_payload_reboot_options(device) + self._build_payload_package(device) msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log_msg(msg) - def build_payload_issu_upgrade(self, device) -> None: + def _build_payload_issu_upgrade(self, device) -> None: """ Build the issuUpgrade portion of the payload. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable nxos_upgrade = device.get("upgrade").get("nxos") nxos_upgrade = self.make_boolean(nxos_upgrade) @@ -371,7 +352,10 @@ def build_payload_issu_upgrade(self, device) -> None: self.module.fail_json(msg) self.payload["issuUpgrade"] = nxos_upgrade - def build_payload_issu_options_1(self, device) -> None: + def _build_payload_issu_options_1(self, device) -> None: + """ + Build the issuUpgradeOptions1 portion of the payload. + """ method_name = inspect.stack()[0][3] # nxos_mode: The choices for nxos_mode are mutually-exclusive. @@ -401,7 +385,10 @@ def build_payload_issu_options_1(self, device) -> None: verify_nxos_mode_list.append(True) self.payload["issuUpgradeOptions1"]["forceNonDisruptive"] = True - def build_payload_issu_options_2(self, device) -> None: + def _build_payload_issu_options_2(self, device) -> None: + """ + Build the issuUpgradeOptions2 portion of the payload. + """ method_name = inspect.stack()[0][3] bios_force = device.get("options").get("nxos").get("bios_force") @@ -415,7 +402,7 @@ def build_payload_issu_options_2(self, device) -> None: self.payload["issuUpgradeOptions2"] = {} self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force - def build_payload_epld(self, device) -> None: + def _build_payload_epld(self, device) -> None: """ Build the epldUpgrade and epldOptions portions of the payload. """ @@ -462,7 +449,7 @@ def build_payload_epld(self, device) -> None: self.payload["epldOptions"]["moduleNumber"] = epld_module self.payload["epldOptions"]["golden"] = epld_golden - def build_payload_reboot(self, device) -> None: + def _build_payload_reboot(self, device) -> None: """ Build the reboot portion of the payload. """ @@ -477,7 +464,7 @@ def build_payload_reboot(self, device) -> None: self.module.fail_json(msg) self.payload["reboot"] = reboot - def build_payload_reboot_options(self, device) -> None: + def _build_payload_reboot_options(self, device) -> None: """ Build the rebootOptions portion of the payload. """ @@ -504,8 +491,7 @@ def build_payload_reboot_options(self, device) -> None: self.payload["rebootOptions"]["configReload"] = config_reload self.payload["rebootOptions"]["writeErase"] = write_erase - - def build_payload_package(self, device) -> None: + def _build_payload_package(self, device) -> None: """ Build the packageInstall and packageUnInstall portions of the payload. """ @@ -532,6 +518,9 @@ def build_payload_package(self, device) -> None: msg += f"Got {package_uninstall}." self.module.fail_json(msg) + # Yes, these keys are misspelled. The controller + # wants them to be misspelled. Need to keep an + # eye out for future releases correcting the spelling. self.payload["pacakgeInstall"] = package_install self.payload["pacakgeUnInstall"] = package_uninstall @@ -551,7 +540,7 @@ def commit(self) -> None: msg += "call instance.devices before calling commit." self.module.fail_json(msg) - self.validate_devices() + self._validate_devices() self._wait_for_current_actions_to_complete() self.path: str = self.endpoints.image_upgrade.get("path") @@ -566,7 +555,7 @@ def commit(self) -> None: msg += f"device: {json.dumps(device, indent=4, sort_keys=True)}" self.log_msg(msg) - self.build_payload(device) + self._build_payload(device) msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" @@ -578,7 +567,9 @@ def commit(self) -> None: self.properties["result"] = self._handle_response(self.response, self.verb) msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" + msg += ( + f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" + ) self.log_msg(msg) if not self.result["success"]: diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 320798207..95d36f6a6 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name import inspect @@ -131,7 +131,9 @@ def log_msg(self, msg): try: # since we need self.fd open throughout several classes # we are disabling pylint R1732 - self.fd = open(f"{self.logfile}", "a+", encoding="UTF-8") # pylint: disable=consider-using-with + self.fd = open( + f"{self.logfile}", "a+", encoding="UTF-8" + ) # pylint: disable=consider-using-with except IOError as err: msg = f"error opening logfile {self.logfile}. " msg += f"detail: {err}" diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index f19021317..a01529306 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name import copy import inspect diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index f838b4444..b120ed156 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name import inspect import json diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index 2ade8a7d0..671189dae 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name import inspect diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index c7857bdd6..e786a295a 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, division, print_function # disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name import inspect diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 06d4177a3..fec98b6db 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -54,7 +54,7 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, validate_list_of_dicts) -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -450,11 +450,6 @@ def __init__(self, module): self.class_name = self.__class__.__name__ method_name = inspect.stack()[0][3] - self.params = self.module.params - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.params: {json.dumps(self.params, indent=4, sort_keys=True)}" - self.log_msg(msg) - self.endpoints = ApiEndpoints() self.have = None self.idempotent_want = None @@ -469,10 +464,6 @@ def __init__(self, module): self.config = module.params.get("config", {}) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.config: {json.dumps(self.config, indent=4, sort_keys=True)}" - self.log_msg(msg) - if not isinstance(self.config, dict): msg = f"{self.class_name}.{method_name}: " msg += "expected dict type for self.config. " @@ -523,9 +514,9 @@ def get_have(self) -> None: """ Caller: main() - Determine current switch ISSU state on NDFC + Determine current switch ISSU state on the controller """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.have = SwitchIssuDetailsByIpAddress(self.module) self.have.refresh() @@ -597,7 +588,7 @@ def _build_idempotent_want(self, want) -> None: Caller: self.get_need_merged() """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.have.ip_address = want["ip_address"] @@ -662,8 +653,8 @@ def _build_idempotent_want(self, want) -> None: instance.epld = want.get("upgrade", {}).get("epld", False) instance.issu = want.get("upgrade", {}).get("nxos", False) - instance.package_install = want.get("options", {}).get("package", {}).get( - "install", False + instance.package_install = ( + want.get("options", {}).get("package", {}).get("install", False) ) instance.refresh() @@ -674,7 +665,7 @@ def _build_idempotent_want(self, want) -> None: self.log_msg(msg) msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.idempotent_want PRE EPLD CHECK: " + msg += "self.idempotent_want PRE EPLD CHECK: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" self.log_msg(msg) @@ -684,7 +675,7 @@ def _build_idempotent_want(self, want) -> None: self.idempotent_want["upgrade"]["epld"] = False msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.idempotent_want POST EPLD CHECK: " + msg += "self.idempotent_want POST EPLD CHECK: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" self.log_msg(msg) @@ -720,20 +711,16 @@ def get_need_merged(self) -> None: test_idempotence.add(self.idempotent_want["stage"]) test_idempotence.add(self.idempotent_want["upgrade"]["nxos"]) test_idempotence.add(self.idempotent_want["upgrade"]["epld"]) - test_idempotence.add(self.idempotent_want["options"]["package"]["install"]) - # TODO:2 InstallOptions doesn't seem to have a way to determine package uninstall. - # TODO:2 For now, we'll comment this out so that it doesn't muck up idempotence. + test_idempotence.add( + self.idempotent_want["options"]["package"]["install"] + ) + # NOTE: InstallOptions doesn't seem to have a way to determine package uninstall. + # NOTE: For now, we'll comment this out so that it doesn't muck up idempotence. # test_idempotence.add(self.idempotent_want["options"]["package"]["uninstall"]) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"test_idempotence: {test_idempotence}" - self.log_msg(msg) if True not in test_idempotence: continue need.append(self.idempotent_want) self.need = need - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.need: {json.dumps(self.need, indent=4, sort_keys=True)}" - self.log_msg(msg) def get_need_deleted(self) -> None: """ @@ -745,7 +732,7 @@ def get_need_deleted(self) -> None: Policies are detached only if the policy name matches. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable need = [] for want in self.want: @@ -768,7 +755,7 @@ def get_need_query(self) -> None: policy name is ignored for query state. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable need = [] for want in self.want: @@ -1148,12 +1135,13 @@ def _build_policy_attach_payload(self) -> None: def _send_policy_attach_payload(self) -> None: """ - Send the policy attach payload to NDFC and handle the response + Send the policy attach payload to the controller and + handle the response Callers: - self.handle_merged_state """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable if len(self.payloads) == 0: return @@ -1208,7 +1196,7 @@ def _validate_images(self, serial_numbers) -> None: def _verify_install_options(self, devices) -> None: """ - Verify that the install options for the devices(es) are valid + Verify that the install options for the device(s) are valid Example devices structure: @@ -1283,13 +1271,13 @@ def _verify_install_options(self, devices) -> None: msg += f"install_options.epld: {install_options.epld}" self.log_msg(msg) - msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "install_options.epld_modules: " - msg += f"{json.dumps(install_options.epld_modules, indent=4, sort_keys=True)}" + msg += ( + f"{json.dumps(install_options.epld_modules, indent=4, sort_keys=True)}" + ) self.log_msg(msg) - if install_options.epld_modules is None and install_options.epld is True: msg = f"{self.class_name}.{method_name}: " msg += "EPLD upgrade is set to True for switch " @@ -1298,11 +1286,6 @@ def _verify_install_options(self, devices) -> None: msg += "EPLD image." self.module.fail_json(msg) - # if self.needs_epld_upgrade(install_options.epld_modules) is False: - # devices[devices.index(device)]["upgrade"]["epld"] = False - - # return devices - def needs_epld_upgrade(self, epld_modules) -> bool: """ Determine if the switch needs an EPLD upgrade @@ -1315,7 +1298,7 @@ def needs_epld_upgrade(self, epld_modules) -> bool: Callers: - self._build_idempotent_want """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable if epld_modules is None: return False @@ -1346,7 +1329,7 @@ def _upgrade_images(self, devices) -> None: Callers: - handle_merged_state """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable upgrade = ImageUpgrade(self.module) upgrade.devices = devices @@ -1361,7 +1344,7 @@ def handle_merged_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self._build_policy_attach_payload() self._send_policy_attach_payload() @@ -1402,7 +1385,7 @@ def handle_deleted_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable detach_policy_devices: Dict[str, Any] = {} @@ -1436,7 +1419,7 @@ def handle_query_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json index 666417567..7bebf20f3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json @@ -1,4 +1,32 @@ { + "test_image_mgmt_upgrade_task_00003a": { + "TEST_NOTES": [ + "fail_json is called because config.switches is not a list" + ], + "config": { + "switches": "FOO" + } + }, + "test_image_mgmt_upgrade_task_00004a": { + "TEST_NOTES": [ + "fail_json is called because config.switches is empty" + ], + "config": { + "switches": [] + } + }, + "test_image_mgmt_upgrade_task_00005a": { + "TEST_NOTES": [ + "fail_json is called because mandatory keys are missing" + ], + "config": { + "switches": [ + { + "ip_address": "1.2.3.4"}, + {"foo": "bar"} + ] + } + }, "test_image_mgmt_upgrade_task_00030a": { "TEST_NOTES": [ "switch 1.1.1.1 uses default options", @@ -68,6 +96,15 @@ "state": "merged" }, "test_image_mgmt_upgrade_task_00031a": { + "TEST_NOTES": [ + "switch 1.1.1.1 overrides global_config.options.nxos.bios_force with default value (False)", + "switch 1.1.1.1 overrides global_config.options.nxos.mode with a non-default value (non_disruptive)", + "switch 1.1.1.1 overrides global_config.options.reboot.write_erase with default value (False)", + "switch 1.1.1.1 overrides global_config.reboot with default value (False)", + "switch 1.1.1.1 overrides global_config.stage with a non-default value (False)", + "switch 1.1.1.1 overrides global_config.validate with a non-default value (False)", + "switch 2.2.2.2 overrides global_config.upgrade.epld with a non-default value (True)" + ], "config": { "options": { "epld": { diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 421d6f8b0..709e94458 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3654,7 +3654,8 @@ "RETURN_CODE 200", "Two switches present", "First switch requires ipAddress and deviceName", - "Second switch requires ipAddress, deviceName, serialNumber, fabric" + "Second switch requires ipAddress, deviceName, serialNumber, fabric", + "Test: SwitchIssuDetailsByIpAddress attributes are set to expected values" ], "RETURN_CODE": 200, "METHOD": "GET", @@ -3664,11 +3665,11 @@ "status": "SUCCESS", "lastOperDataObject": [ { - "ipAddress": "172.22.150.102", + "ipAddress": "1.1.1.1", "deviceName": "leaf1" }, { - "ipAddress": "172.22.150.108", + "ipAddress": "2.2.2.2", "deviceName": "cvd-2313-leaf", "serialNumber": "FDO2112189M", "fabric": "hard" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index bf5ad7391..cb05d0d5c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -64,7 +64,12 @@ def test_image_mgmt_upgrade_00001(image_upgrade) -> None: """ instance = image_upgrade assert isinstance(instance, ImageUpgrade) + assert isinstance(instance.ipv4_done, set) + assert isinstance(instance.ipv4_todo, set) + assert isinstance(instance.payload, dict) assert instance.class_name == "ImageUpgrade" + assert instance.path is None + assert instance.verb is None def test_image_mgmt_upgrade_00002(image_upgrade) -> None: @@ -160,7 +165,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] instance.devices = devices - instance.validate_devices() + instance._validate_devices() # pylint: disable=protected-access assert isinstance(instance.ip_addresses, set) assert len(instance.ip_addresses) == 2 assert "172.22.150.102" in instance.ip_addresses @@ -704,7 +709,7 @@ def test_image_mgmt_upgrade_00018(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit will call build_payload which will call fail_json + 1. commit will call _build_payload which will call fail_json """ instance = image_upgrade @@ -750,7 +755,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] - match = r"ImageUpgrade.build_payload_issu_upgrade: upgrade.nxos must be a boolean. Got FOO\." + match = r"ImageUpgrade._build_payload_issu_upgrade: upgrade.nxos must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -922,7 +927,7 @@ def test_image_mgmt_upgrade_00021(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload, which calls fail_json + 1. commit calls _build_payload, which calls fail_json """ instance = image_upgrade @@ -968,7 +973,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_issu_options_1: " + match = "ImageUpgrade._build_payload_issu_options_1: " match += "options.nxos.mode must be one of " match += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " match += "Got FOO." @@ -1144,7 +1149,7 @@ def test_image_mgmt_upgrade_00024(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json """ instance = image_upgrade @@ -1190,7 +1195,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_issu_options_2: " + match = "ImageUpgrade._build_payload_issu_options_2: " match += r"options.nxos.bios_force must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1216,7 +1221,7 @@ def test_image_mgmt_upgrade_00025(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json """ instance = image_upgrade @@ -1262,7 +1267,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_epld: Invalid configuration for " + match = "ImageUpgrade._build_payload_epld: Invalid configuration for " match += "172.22.150.102. If options.epld.golden is True " match += "all other upgrade options, e.g. upgrade.nxos, " match += "must be False." @@ -1289,7 +1294,7 @@ def test_image_mgmt_upgrade_00026(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json """ instance = image_upgrade @@ -1339,7 +1344,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_epld: " + match = "ImageUpgrade._build_payload_epld: " match += "options.epld.module must either be 'ALL' " match += r"or an integer. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1365,7 +1370,7 @@ def test_image_mgmt_upgrade_00027(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json """ instance = image_upgrade @@ -1415,7 +1420,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_epld: " + match = "ImageUpgrade._build_payload_epld: " match += r"options.epld.golden must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1440,7 +1445,7 @@ def test_image_mgmt_upgrade_00028(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json """ instance = image_upgrade @@ -1486,7 +1491,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_reboot: " + match = "ImageUpgrade._build_payload_reboot: " match += r"reboot must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1512,7 +1517,7 @@ def test_image_mgmt_upgrade_00029(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json """ instance = image_upgrade @@ -1562,7 +1567,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_reboot_options: " + match = "ImageUpgrade._build_payload_reboot_options: " match += r"options.reboot.config_reload must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1588,7 +1593,7 @@ def test_image_mgmt_upgrade_00030(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json """ instance = image_upgrade @@ -1638,7 +1643,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_reboot_options: " + match = "ImageUpgrade._build_payload_reboot_options: " match += r"options.reboot.write_erase must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1663,7 +1668,7 @@ def test_image_mgmt_upgrade_00031(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json NOTES: 1. The corresponding test for options.package.install is missing. @@ -1719,7 +1724,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageUpgrade.build_payload_package: " + match = "ImageUpgrade._build_payload_package: " match += r"options.package.uninstall must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -1823,7 +1828,7 @@ def test_image_mgmt_upgrade_00033(monkeypatch, image_upgrade) -> None: Expected results: - 1. commit calls build_payload which calls fail_json + 1. commit calls _build_payload which calls fail_json """ instance = image_upgrade diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py index 7b583984b..5c340b90a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py @@ -60,12 +60,14 @@ DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" -# This fixture differs from image_upgrade_task_fixture -# in that it does not use a patched MockAnsibleModule. -# This is because we need to modify MockAnsibleModule for -# some of the test cases below. @pytest.fixture(name="image_upgrade_task_bare") def image_upgrade_task_bare_fixture(): + """ + This fixture differs from image_upgrade_task_fixture + in that it does not use a patched MockAnsibleModule. + This is because we need to modify MockAnsibleModule for + some of the test cases below. + """ return ImageUpgradeTask @@ -106,7 +108,9 @@ def test_image_mgmt_upgrade_task_00002(image_upgrade_task_bare) -> None: Test - fail_json is called because config is not a dict """ - match = "ImageUpgradeTask.__init__: expected dict type for self.config. got str" + match = "ImageUpgradeTask.__init__: expected dict type " + match += "for self.config. got str" + mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = {"config": "foo"} with pytest.raises(AnsibleFailJson, match=match): @@ -122,11 +126,13 @@ def test_image_mgmt_upgrade_task_00003(image_upgrade_task_bare) -> None: Test - fail_json is called because config.switches is not a list """ + key = "test_image_mgmt_upgrade_task_00003a" + match = "ImageUpgradeTask.__init__: expected list type for " match += r"self.config\['switches'\]. got str" mock_ansible_module = MockAnsibleModule() - mock_ansible_module.params = {"config": {"switches": "FOO"}} + mock_ansible_module.params = load_playbook_config(key) with pytest.raises(AnsibleFailJson, match=match): instance = image_upgrade_task_bare(mock_ansible_module) assert isinstance(instance, ImageUpgradeTask) @@ -140,10 +146,13 @@ def test_image_mgmt_upgrade_task_00004(image_upgrade_task_bare) -> None: Test - fail_json is called because config.switches is empty """ + key = "test_image_mgmt_upgrade_task_00004a" + match = "ImageUpgradeTask.__init__: missing list of switches " match += "in playbook config." + mock_ansible_module = MockAnsibleModule() - mock_ansible_module.params = {"config": {"switches": []}} + mock_ansible_module.params = load_playbook_config(key) with pytest.raises(AnsibleFailJson, match=match): instance = image_upgrade_task_bare(mock_ansible_module) assert isinstance(instance, ImageUpgradeTask) @@ -158,14 +167,14 @@ def test_image_mgmt_upgrade_task_00005(image_upgrade_task_bare) -> None: - fail_json is called because mandatory keys are missing in one of the switch configs """ + key = "test_image_mgmt_upgrade_task_00005a" + match = "ImageUpgradeTask.__init__: missing mandatory " match += r"key\(s\) in playbook switch config. expected " match += r"\{'ip_address'\}, got dict_keys\(\['foo'\]\)" mock_ansible_module = MockAnsibleModule() - mock_ansible_module.params = { - "config": {"switches": [{"ip_address": "1.2.3.4"}, {"foo": "bar"}]} - } + mock_ansible_module.params = load_playbook_config(key) with pytest.raises(AnsibleFailJson, match=match): instance = image_upgrade_task_bare(mock_ansible_module) assert isinstance(instance, ImageUpgradeTask) @@ -188,9 +197,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance = image_upgrade_task instance.get_have() - instance.have.ip_address = "172.22.150.102" + instance.have.ip_address = "1.1.1.1" assert instance.have.device_name == "leaf1" - instance.have.ip_address = "172.22.150.108" + instance.have.ip_address = "2.2.2.2" assert instance.have.device_name == "cvd-2313-leaf" assert instance.have.serial_number == "FDO2112189M" assert instance.have.fabric == "hard" From 9813d83e7104b48aead438f2a3ea9e540b09a56b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 5 Dec 2023 15:25:20 -1000 Subject: [PATCH 128/300] merge_dicts() handle case where no suboptions are specified in options dict There's a case where the user specifies an options dict (e.g. "upgrade"), but does not include the suboptions. This results in the options dict given a value of null when converting from YAML to a Python dict. Rewrite merge_dicts() to handle this case. Also, moved merge_dicts to image_upgrade_common.py so it can be shared in the future. Lastly, so that all switch config processing is performed in one place, moved _merge_defaults_to_switch_config() from image_upgrade.py (ImageUpgrade class) to dcnm_image_upgrade.py (ImageUpgradeTask class). These changes required modifying/moving several unit tests. --- .../module_utils/image_mgmt/image_upgrade.py | 90 +- .../image_mgmt/image_upgrade_common.py | 35 + plugins/modules/dcnm_image_upgrade.py | 76 +- .../image_upgrade_payloads_ImageUpgrade.json | 10 +- .../test_image_upgrade_image_upgrade.py | 877 +++++++----------- ...pgrade_image_upgrade_image_upgrade_task.py | 521 +++++++++++ 6 files changed, 951 insertions(+), 658 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 92dd57daa..74435c5d7 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -130,36 +130,11 @@ def __init__(self, module): self.path = None self.verb = None - self._init_defaults() self._init_properties() self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) self.install_options = ImageInstallOptions(self.module) self.log_msg("DEBUG: ImageUpgrade.__init__ DONE") - def _init_defaults(self) -> None: - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - - self.defaults: Dict[str, Any] = {} - self.defaults["options"] = {} - self.defaults["options"]["epld"] = {} - self.defaults["options"]["epld"]["module"] = "ALL" - self.defaults["options"]["epld"]["golden"] = False - self.defaults["options"]["nxos"] = {} - self.defaults["options"]["nxos"]["mode"] = "disruptive" - self.defaults["options"]["nxos"]["bios_force"] = False - self.defaults["options"]["package"] = {} - self.defaults["options"]["package"]["install"] = False - self.defaults["options"]["package"]["uninstall"] = False - self.defaults["options"]["reboot"] = {} - self.defaults["options"]["reboot"]["config_reload"] = False - self.defaults["options"]["reboot"]["write_erase"] = False - self.defaults["reboot"] = False - self.defaults["stage"] = True - self.defaults["upgrade"] = {} - self.defaults["upgrade"]["epld"] = False - self.defaults["upgrade"]["nxos"] = True - self.defaults["validate"] = True - def _init_properties(self) -> None: """ Initialize properties used by this class. @@ -226,63 +201,6 @@ def _validate_devices(self) -> None: # used in self._wait_for_current_actions_to_complete() self.ip_addresses.add(str(self.issu_detail.ip_address)) - def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - - if config.get("stage") is None: - config["stage"] = self.defaults["stage"] - if config.get("reboot") is None: - config["reboot"] = self.defaults["reboot"] - if config.get("validate") is None: - config["validate"] = self.defaults["validate"] - if config.get("upgrade") is None: - config["upgrade"] = self.defaults["upgrade"] - if config.get("upgrade").get("nxos") is None: - config["upgrade"]["nxos"] = self.defaults["upgrade"]["nxos"] - if config.get("upgrade").get("epld") is None: - config["upgrade"]["epld"] = self.defaults["upgrade"]["epld"] - if config.get("options") is None: - config["options"] = self.defaults["options"] - if config["options"].get("nxos") is None: - config["options"]["nxos"] = self.defaults["options"]["nxos"] - if config["options"]["nxos"].get("mode") is None: - config["options"]["nxos"]["mode"] = self.defaults["options"]["nxos"]["mode"] - if config["options"]["nxos"].get("bios_force") is None: - config["options"]["nxos"]["bios_force"] = self.defaults["options"]["nxos"][ - "bios_force" - ] - if config["options"].get("epld") is None: - config["options"]["epld"] = self.defaults["options"]["epld"] - if config["options"]["epld"].get("module") is None: - config["options"]["epld"]["module"] = self.defaults["options"]["epld"][ - "module" - ] - if config["options"]["epld"].get("golden") is None: - config["options"]["epld"]["golden"] = self.defaults["options"]["epld"][ - "golden" - ] - if config["options"].get("reboot") is None: - config["options"]["reboot"] = self.defaults["options"]["reboot"] - if config["options"]["reboot"].get("config_reload") is None: - config["options"]["reboot"]["config_reload"] = self.defaults["options"][ - "reboot" - ]["config_reload"] - if config["options"]["reboot"].get("write_erase") is None: - config["options"]["reboot"]["write_erase"] = self.defaults["options"][ - "reboot" - ]["write_erase"] - if config["options"].get("package") is None: - config["options"]["package"] = self.defaults["options"]["package"] - if config["options"]["package"].get("install") is None: - config["options"]["package"]["install"] = self.defaults["options"][ - "package" - ]["install"] - if config["options"]["package"].get("uninstall") is None: - config["options"]["package"]["uninstall"] = self.defaults["options"][ - "package" - ]["uninstall"] - return config - def _build_payload(self, device) -> None: """ Build the request payload to upgrade the switches. @@ -290,13 +208,7 @@ def _build_payload(self, device) -> None: method_name = inspect.stack()[0][3] msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"device PRE_DEFAULTS : {json.dumps(device, indent=4, sort_keys=True)}" - self.log_msg(msg) - - device = self._merge_defaults_to_switch_config(device) - - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"device POST_DEFAULTS: {json.dumps(device, indent=4, sort_keys=True)}" + msg += f"device FINAL: {json.dumps(device, indent=4, sort_keys=True)}" self.log_msg(msg) # TODO:2 Validate ip_address diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 95d36f6a6..dbe9b9635 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -7,6 +7,7 @@ __metaclass__ = type # pylint: disable=invalid-name import inspect +from collections.abc import MutableMapping as Map class ImageUpgradeCommon: @@ -170,3 +171,37 @@ def make_none(self, value): if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: return None return value + + def merge_dicts(self, dict1, dict2): + """ + Merge dict2 into dict1 and return dict1. + Keys in dict2 have precedence over keys in dict1. + """ + for key in dict2: + # self.log_msg(f"DEBUG: {self.class_name}.merge_dicts: key: {key}") + + if isinstance(dict1.get(key, None), Map) and dict2.get(key, None) is None: + # This is to handle a case where the playbook contains an + # options dict that is supposed to contain sub-options + # (in the example below, 'upgrade' should contain 'nxos' and + # 'epld'), but the dict is empty in the playbook. + # For example, below, upgrade is specified, but upgrade.nxos + # and upgrade.epld are not: + # + # - ip_address: 172.22.150.110 + # upgrade: + # options: + # epld: + # module: 27 + # golden: false + # In this case, we copy the entire 'upgrade' dict from dict1 to dict2 + dict2[key] = dict1[key] + elif ( + key in dict1 + and isinstance(dict1[key], Map) + and isinstance(dict2[key], Map) + ): + self.merge_dicts(dict1[key], dict2[key]) + else: + dict1[key] = dict2[key] + return dict1 diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index fec98b6db..98e7af532 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -27,7 +27,6 @@ import copy import inspect import json -from collections.abc import MutableMapping as Map from typing import Any, Dict, List from ansible.module_utils.basic import AnsibleModule @@ -450,6 +449,7 @@ def __init__(self, module): self.class_name = self.__class__.__name__ method_name = inspect.stack()[0][3] + self._init_defaults() self.endpoints = ApiEndpoints() self.have = None self.idempotent_want = None @@ -510,6 +510,33 @@ def __init__(self, module): self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) + def _init_defaults(self): + """ + Default values for playbook parameters. + These are merged with playbook parameters in + self._merge_defaults_to_switch_config() + """ + self.defaults: Dict[str, Any] = {} + self.defaults["options"] = {} + self.defaults["options"]["epld"] = {} + self.defaults["options"]["epld"]["module"] = "ALL" + self.defaults["options"]["epld"]["golden"] = False + self.defaults["options"]["nxos"] = {} + self.defaults["options"]["nxos"]["mode"] = "disruptive" + self.defaults["options"]["nxos"]["bios_force"] = False + self.defaults["options"]["package"] = {} + self.defaults["options"]["package"]["install"] = False + self.defaults["options"]["package"]["uninstall"] = False + self.defaults["options"]["reboot"] = {} + self.defaults["options"]["reboot"]["config_reload"] = False + self.defaults["options"]["reboot"]["write_erase"] = False + self.defaults["reboot"] = False + self.defaults["stage"] = True + self.defaults["upgrade"] = {} + self.defaults["upgrade"]["epld"] = False + self.defaults["upgrade"]["nxos"] = True + self.defaults["validate"] = True + def get_have(self) -> None: """ Caller: main() @@ -590,6 +617,10 @@ def _build_idempotent_want(self, want) -> None: """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"want: {json.dumps(want, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.have.ip_address = want["ip_address"] # msg = f"DEBUG: {self.class_name}.{method_name}: " @@ -980,17 +1011,6 @@ def _validate_input_for_query_state(self) -> None: msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) - def merge_dicts(self, d1, d2): - """ - Merge d2 into d1 and return d1. - Keys in d2 have precedence over keys in d1. - """ - for key in d2: - if key in d1 and isinstance(d1[key], Map) and isinstance(d2[key], Map): - self.merge_dicts(d1[key], d2[key]) - else: - d1[key] = d2[key] - return d1 def _merge_global_and_switch_configs(self, config) -> None: """ @@ -1037,8 +1057,37 @@ def _merge_global_and_switch_configs(self, config) -> None: msg += f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" self.log_msg(msg) + switch_config = self._merge_defaults_to_switch_config(switch_config) + self.switch_configs.append(switch_config) + + def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: + """ + For any items in config which are not set, apply the default + value from self.defaults. + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + # self._init_defaults() + + default_config = copy.deepcopy(self.defaults) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"default_config: {json.dumps(default_config, indent=4, sort_keys=True)}" + self.log_msg(msg) + + switch_config = copy.deepcopy(config) + merged_config = self.merge_dicts(default_config, switch_config) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"default_config: {json.dumps(default_config, indent=4, sort_keys=True)}" + self.log_msg(msg) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"switch_config: {json.dumps(switch_config, indent=4, sort_keys=True)}" + self.log_msg(msg) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"merged_config: {json.dumps(merged_config, indent=4, sort_keys=True)}" + self.log_msg(msg) + return self.merge_dicts(default_config, switch_config) + def _validate_switch_configs(self) -> None: """ Ensure mandatory parameters are present for each switch @@ -1047,8 +1096,7 @@ def _validate_switch_configs(self) -> None: NOTES: 1. Final application of missing default parameters is done in - ImageUpgrade.build_payload, which calls - ImageUpgrade._merge_defaults_to_switch_config + self._merge_defaults_to_switch_config Callers: - self.get_want diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json index 985fc0671..14324084e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json @@ -7,8 +7,8 @@ } ], "epldOptions": { - "golden": false, - "moduleNumber": "ALL" + "golden": true, + "moduleNumber": 27 }, "epldUpgrade": true, "issuUpgrade": false, @@ -20,11 +20,11 @@ "issuUpgradeOptions2": { "biosForce": true }, - "pacakgeInstall": false, + "pacakgeInstall": true, "pacakgeUnInstall": false, "reboot": false, "rebootOptions": { - "configReload": false, + "configReload": true, "writeErase": false } }, @@ -49,7 +49,7 @@ "issuUpgradeOptions2": { "biosForce": false }, - "pacakgeInstall": false, + "pacakgeInstall": true, "pacakgeUnInstall": false, "reboot": false, "rebootOptions": { diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index cb05d0d5c..274dc32bd 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -72,30 +72,6 @@ def test_image_mgmt_upgrade_00001(image_upgrade) -> None: assert instance.verb is None -def test_image_mgmt_upgrade_00002(image_upgrade) -> None: - """ - Function - - _init_defaults - - Test - - defaults dictionary is initialized with expected keys, values - """ - instance = image_upgrade - instance._init_defaults() - assert isinstance(instance.defaults, dict) - assert instance.defaults["reboot"] is False - assert instance.defaults["stage"] is True - assert instance.defaults["validate"] is True - assert instance.defaults["upgrade"]["nxos"] is True - assert instance.defaults["upgrade"]["epld"] is False - assert instance.defaults["options"]["nxos"]["mode"] == "disruptive" - assert instance.defaults["options"]["nxos"]["bios_force"] is False - assert instance.defaults["options"]["epld"]["module"] == "ALL" - assert instance.defaults["options"]["epld"]["golden"] is False - assert instance.defaults["options"]["reboot"]["config_reload"] is False - assert instance.defaults["options"]["reboot"]["write_erase"] is False - assert instance.defaults["options"]["package"]["install"] is False - assert instance.defaults["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_00003(image_upgrade) -> None: @@ -165,7 +141,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] instance.devices = devices - instance._validate_devices() # pylint: disable=protected-access + instance._validate_devices() # pylint: disable=protected-access assert isinstance(instance.ip_addresses, set) assert len(instance.ip_addresses) == 2 assert "172.22.150.102" in instance.ip_addresses @@ -187,509 +163,6 @@ def test_image_mgmt_upgrade_00005(image_upgrade) -> None: instance.commit() -def test_image_mgmt_upgrade_00006(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - values missing that have defaults defined (see ImageUpgrade._init_defaults) - - Test - - merged_config contains expected default values - """ - instance = image_upgrade - - config = {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00007(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except upgrade.nxos. This forces the code - to take the upgrade.epld is None path. - - Test - - merged_config contains expected default values - - merged_config contains expected non-default values - - Description - Force code coverage of the upgrade.epld is None path - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "upgrade": {"nxos": False}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is False - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00008(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except upgrade.epld. This forces the code - to take the upgrade.nxos is None path. - - Test - - merged_config contains expected default values - - merged_config contains expected non-default values - - Description - Force code coverage of the upgrade.nxos is None path - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "upgrade": {"epld": True}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is True - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00009(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options, which is empty. - This forces the code to take the options is None path - and provide default values for options.nxos and options.epld. - - Test - - merged_config contains expected default values - - merged_config contains expected non-default values - - Description - Force code coverage of the options is None path - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00010(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.nxos.mode. - This forces the code to take the options.nxos.bios_force - is None path. - - Test - - merged_config contains expected default values - - merged_config contains expected non-default values - - Description - Force code coverage of the options.nxos.bios_force is None path - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"nxos": {"mode": "non_disruptive"}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "non_disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00011(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Test - - Force code coverage of the options.nxos.mode is None path - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.nxos.bios_force. This forces the code - to take the options.nxos.mode is None path. - - Expected results: - - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"nxos": {"bios_force": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is True - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00012(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Test - - Force code coverage of the options.epld.golden is None path - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.epld.module. This forces - the code to take the options.epld.golden is None path. - - Expected results: - - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"epld": {"module": 27}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == 27 - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00013(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Test - - Force code coverage of the options.epld.module is None path - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.epld.golden. This forces - the code to take the options.epld.module is None path. - - Expected results: - - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"epld": {"golden": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is True - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00014(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Test - - Force code coverage of the options.reboot.write_erase is None path - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.reboot.config_reload. This - forces the code to take the options.reboot.write_erase is None path. - - Expected results: - - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"reboot": {"config_reload": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is True - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00015(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Test - - Force code coverage of the options.reboot.config_reload is None path - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.reboot.write_erase. This - forces the code to take the options.reboot.config_reload is None path. - - Expected results: - - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"reboot": {"write_erase": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is True - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00016(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Test - - Force code coverage of the options.package.uninstall is None path - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.package.install. This - forces the code to take the options.package.uninstall is None path. - - Expected results: - - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"package": {"install": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is True - assert merged_config["options"]["package"]["uninstall"] is False - - -def test_image_mgmt_upgrade_00017(image_upgrade) -> None: - """ - Function - - _merge_defaults_to_switch_config - - Test - - Force code coverage of the options.package.install is None path - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.package.uninstall. This - forces the code to take the options.package.install is None path. - - Expected results: - - 1. merged_config will contain the expected default values - 2. merged_config will contain the expected non-default values - """ - instance = image_upgrade - - config = { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"package": {"uninstall": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is True - - def test_image_mgmt_upgrade_00018(monkeypatch, image_upgrade) -> None: """ Function @@ -749,7 +222,20 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy": "KR5M", "stage": True, "upgrade": {"nxos": "FOO", "epld": True}, - "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": True + }, + "package": { + "install": False, + "uninstall": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": False, @@ -823,9 +309,27 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "KR5M", + "reboot": False, "stage": True, "upgrade": {"nxos": False, "epld": True}, - "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": True + }, + "package": { + "install": True, + "uninstall": False + }, + "epld": { + "module": 27, + "golden": True + }, + "reboot": { + "config_reload": True, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": False, @@ -895,14 +399,33 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, "upgrade": {"nxos": True, "epld": True}, - "options": {"nxos": {"mode": "disruptive", "bios_force": False}}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": True, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": True, } ] + instance.commit() assert instance.payload == payloads_image_upgrade(key) @@ -965,14 +488,33 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, "upgrade": {"nxos": True, "epld": True}, - "options": {"nxos": {"mode": "FOO", "bios_force": False}}, + "options": { + "nxos": { + "mode": "FOO", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": True, } ] + match = "ImageUpgrade._build_payload_issu_options_1: " match += "options.nxos.mode must be one of " match += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " @@ -1041,14 +583,33 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, "upgrade": {"nxos": True, "epld": True}, - "options": {"nxos": {"mode": "non_disruptive", "bios_force": False}}, + "options": { + "nxos": { + "mode": "non_disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": True, } ] + instance.commit() assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False assert instance.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is False @@ -1115,9 +676,27 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, "upgrade": {"nxos": True, "epld": True}, - "options": {"nxos": {"mode": "force_non_disruptive", "bios_force": False}}, + "options": { + "nxos": { + "mode": "force_non_disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": True, @@ -1187,9 +766,27 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, "upgrade": {"nxos": True, "epld": True}, - "options": {"nxos": {"mode": "disruptive", "bios_force": "FOO"}}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": "FOO" + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": True, @@ -1259,14 +856,33 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, "upgrade": {"nxos": True, "epld": True}, - "options": {"epld": {"module": "ALL", "golden": True}}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": True + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": True, } ] + match = "ImageUpgrade._build_payload_epld: Invalid configuration for " match += "172.22.150.102. If options.epld.golden is True " match += "all other upgrade options, e.g. upgrade.nxos, " @@ -1332,11 +948,25 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, "epld": { "module": "FOO", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False } }, "validate": True, @@ -1408,11 +1038,25 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, "epld": { - "golden": "FOO", + "module": "ALL", + "golden": "FOO" + }, + "reboot": { + "config_reload": False, + "write_erase": False } }, "validate": True, @@ -1483,9 +1127,27 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": "FOO", "stage": True, "upgrade": {"nxos": True, "epld": True}, - "reboot": "FOO", + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": True, @@ -1555,11 +1217,25 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": True, "stage": True, - "upgrade": {"nxos": True, "epld": False}, + "upgrade": {"nxos": True, "epld": True}, "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, "reboot": { "config_reload": "FOO", + "write_erase": False } }, "validate": True, @@ -1631,11 +1307,25 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": True, "stage": True, - "upgrade": {"nxos": True, "epld": False}, + "upgrade": {"nxos": True, "epld": True}, "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, "reboot": { - "write_erase": "FOO", + "config_reload": False, + "write_erase": "FOO" } }, "validate": True, @@ -1712,11 +1402,25 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": True, "stage": True, - "upgrade": {"nxos": True, "epld": False}, + "upgrade": {"nxos": True, "epld": True}, "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, "package": { - "uninstall": "FOO", + "install": False, + "uninstall": "FOO" + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False } }, "validate": True, @@ -1789,8 +1493,27 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "NR3F", + "reboot": False, "stage": True, - "upgrade": {"nxos": True, "epld": False}, + "upgrade": {"nxos": True, "epld": True}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": True, @@ -1959,13 +1682,31 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { - "policy": "KR5M", + "policy": "NR3F", + "reboot": False, "stage": True, - "upgrade": {"nxos": False, "epld": True}, - "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "upgrade": {"nxos": True, "epld": True}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": False + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", - "policy_changed": False, + "policy_changed": True, } ] instance.commit() @@ -2027,9 +1768,27 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "KR5M", + "reboot": False, "stage": True, "upgrade": {"nxos": False, "epld": True}, - "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": True + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": False, @@ -2094,9 +1853,27 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.devices = [ { "policy": "KR5M", + "reboot": False, "stage": True, "upgrade": {"nxos": False, "epld": True}, - "options": {"nxos": {"mode": "disruptive", "bios_force": True}}, + "options": { + "nxos": { + "mode": "disruptive", + "bios_force": True + }, + "package": { + "install": False, + "uninstall": False + }, + "epld": { + "module": "ALL", + "golden": False + }, + "reboot": { + "config_reload": False, + "write_erase": False + } + }, "validate": True, "ip_address": "172.22.150.102", "policy_changed": False, diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py index 5c340b90a..ada664879 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py @@ -180,6 +180,31 @@ def test_image_mgmt_upgrade_task_00005(image_upgrade_task_bare) -> None: assert isinstance(instance, ImageUpgradeTask) +def test_image_mgmt_upgrade_task_00006(image_upgrade_task) -> None: + """ + Function + - _init_defaults + + Test + - defaults dictionary is initialized with expected keys, values + """ + instance = image_upgrade_task + instance._init_defaults() + assert isinstance(instance.defaults, dict) + assert instance.defaults["reboot"] is False + assert instance.defaults["stage"] is True + assert instance.defaults["validate"] is True + assert instance.defaults["upgrade"]["nxos"] is True + assert instance.defaults["upgrade"]["epld"] is False + assert instance.defaults["options"]["nxos"]["mode"] == "disruptive" + assert instance.defaults["options"]["nxos"]["bios_force"] is False + assert instance.defaults["options"]["epld"]["module"] == "ALL" + assert instance.defaults["options"]["epld"]["golden"] is False + assert instance.defaults["options"]["reboot"]["config_reload"] is False + assert instance.defaults["options"]["reboot"]["write_erase"] is False + assert instance.defaults["options"]["package"]["install"] is False + assert instance.defaults["options"]["package"]["uninstall"] is False + def test_image_mgmt_upgrade_task_00020(monkeypatch, image_upgrade_task) -> None: """ Function @@ -335,3 +360,499 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert switch_2.get("upgrade").get("epld") is True assert switch_2.get("upgrade").get("nxos") is True assert switch_2.get("validate") is True + +#--------------------------------------------------------- + +def test_image_mgmt_upgrade_task_00040(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + values missing that have defaults defined + (see ImageUpgradeTask._init_defaults) + + Test + - merged_config contains expected default values + """ + instance = image_upgrade_task + + config = {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00041(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except upgrade.nxos. + + Test + - merged_config contains expected default values + - merged_config contains expected non-default values + + Description + Force code coverage of the upgrade.epld is None path + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "upgrade": {"nxos": False}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is False + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00042(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except upgrade.epld. + + Test + - merged_config contains expected default values + - merged_config contains expected non-default values + + Description + Force code coverage of the upgrade.nxos is None path + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "upgrade": {"epld": True}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is True + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00043(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options, which is empty. + + Test + - merged_config contains expected default values + - merged_config contains expected non-default values + + Description + When options is empty, the default values for all sub-options are added + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00044(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Setup + - _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.nxos.mode. + + Test + - Default value for options.nxos.bios_force is added + - merged_config contains expected default values + - merged_config contains expected non-default values + + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"nxos": {"mode": "non_disruptive"}}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "non_disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00045(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Test + - Default value of options.nxos.mode is added + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.nxos.bios_force. + + Expected results: + + 1. merged_config contains the expected default values + 2. merged_config contains the expected non-default values + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"nxos": {"bios_force": True}}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is True + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00046(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Test + - Default value of options.epld.golden is added + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.epld.module. + + Expected results: + + 1. merged_config contains the expected default values + 2. merged_config contains the expected non-default values + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"epld": {"module": 27}}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == 27 + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00047(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Test + - Default value for options.epld.module is added + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.epld.golden. + + Expected results: + + 1. options.epld.module is set to ALL + 2. merged_config contains the expected default values + 3. merged_config contains the expected non-default values + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"epld": {"golden": True}}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is True + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00048(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Test + - Default value for options.reboot.write_erase is added + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.reboot.config_reload. + + Expected results: + + 1. options.reboot.write_erase is set to False + 2. merged_config contains the expected default values + 3. merged_config contains the expected non-default values + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"reboot": {"config_reload": True}}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is True + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00049(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Test + - Default value for options.reboot.config_reload is added + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.reboot.write_erase. + + Expected results: + + 1. options.reboot.config_reload is set to False + 2. merged_config contains the expected default values + 3. merged_config contains the expected non-default values + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"reboot": {"write_erase": True}}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is True + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00050(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Test + - Default value for options.package.uninstall is added + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.package.install. + + Expected results: + + 1. options.package.uninstall is set to False + 2. merged_config contains expected default values + 3. merged_config contains expected non-default values + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"package": {"install": True}}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is True + assert merged_config["options"]["package"]["uninstall"] is False + + +def test_image_mgmt_upgrade_task_00051(image_upgrade_task) -> None: + """ + Function + - _merge_defaults_to_switch_config + + Test + - Default value for options.package.install is added + + Setup: + 1. _merge_defaults_to_switch_config is passed a dictionary with all + default values missing except options.package.uninstall. + + Expected results: + + 1. options.package.install is set to False + 2. merged_config contains the expected default values + 3. merged_config contains the expected non-default values + """ + instance = image_upgrade_task + + config = { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"package": {"uninstall": True}}, + } + + merged_config = instance._merge_defaults_to_switch_config(config) + assert merged_config["reboot"] is False + assert merged_config["stage"] is True + assert merged_config["validate"] is True + assert merged_config["upgrade"]["nxos"] is True + assert merged_config["upgrade"]["epld"] is False + assert merged_config["options"]["nxos"]["mode"] == "disruptive" + assert merged_config["options"]["nxos"]["bios_force"] is False + assert merged_config["options"]["epld"]["module"] == "ALL" + assert merged_config["options"]["epld"]["golden"] is False + assert merged_config["options"]["reboot"]["config_reload"] is False + assert merged_config["options"]["reboot"]["write_erase"] is False + assert merged_config["options"]["package"]["install"] is False + assert merged_config["options"]["package"]["uninstall"] is True From 8307f7e0f10a59f78d25fe151b1ac2fb54a3859a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 6 Dec 2023 09:14:35 -1000 Subject: [PATCH 129/300] ansible-sanity related cleanup --- .../module_utils/common/controller_version.py | 4 +- .../module_utils/image_mgmt/api_endpoints.py | 3 +- .../module_utils/image_mgmt/image_policies.py | 3 +- .../image_mgmt/image_policy_action.py | 3 +- .../module_utils/image_mgmt/image_stage.py | 3 +- .../module_utils/image_mgmt/image_upgrade.py | 3 +- .../image_mgmt/image_upgrade_common.py | 3 +- .../module_utils/image_mgmt/image_validate.py | 15 ++- .../image_mgmt/install_options.py | 8 +- .../module_utils/image_mgmt/switch_details.py | 3 +- .../image_mgmt/switch_issu_details.py | 7 +- plugins/modules/dcnm_image_upgrade.py | 98 +++++++++---------- .../test_image_upgrade_image_policy_action.py | 2 +- .../test_image_upgrade_image_upgrade.py | 2 - ...pgrade_image_upgrade_image_upgrade_task.py | 2 +- 15 files changed, 71 insertions(+), 88 deletions(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index c643e90c1..621bb0f28 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -1,7 +1,6 @@ from __future__ import (absolute_import, division, print_function) -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, @@ -9,6 +8,7 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints + class ControllerVersion(ImageUpgradeCommon): """ Return image version information from the Controller diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py index 550cdc925..0cd3d9fd1 100644 --- a/plugins/module_utils/image_mgmt/api_endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -3,8 +3,7 @@ """ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type class ApiEndpoints: diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 448954e5a..08cc73304 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -4,8 +4,7 @@ """ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import inspect diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 821bd889f..80ba2f81f 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -3,8 +3,7 @@ """ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import inspect import json diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 58af6cb11..028f646c5 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -3,8 +3,7 @@ """ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import copy import inspect diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 74435c5d7..cf56a90c6 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -3,8 +3,7 @@ """ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import copy import inspect diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index dbe9b9635..43d03b522 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -3,8 +3,7 @@ """ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import inspect from collections.abc import MutableMapping as Map diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index a01529306..07421746e 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import copy import inspect @@ -187,10 +186,10 @@ def _wait_for_current_actions_to_complete(self) -> None: if self.serial_numbers_done != serial_numbers_todo: msg = f"{self.class_name}.{self.method_name}: " - msg += f"Timed out waiting for actions to complete. " - msg += f"serial_numbers_done: " + msg += "Timed out waiting for actions to complete. " + msg += "serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " + msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" self.module.fail_json(msg) @@ -238,10 +237,10 @@ def _wait_for_image_validate_to_complete(self) -> None: if self.serial_numbers_done != serial_numbers_todo: msg = f"{self.class_name}.{self.method_name}: " - msg += f"Timed out waiting for image validation to complete. " - msg += f"serial_numbers_done: " + msg += "Timed out waiting for image validation to complete. " + msg += "serial_numbers_done: " msg += f"{','.join(sorted(self.serial_numbers_done))}, " - msg += f"serial_numbers_todo: " + msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" self.module.fail_json(msg) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index b120ed156..dd89343d2 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -1,11 +1,9 @@ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import inspect import json -from time import sleep from typing import Any, Dict from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -155,8 +153,8 @@ def refresh(self) -> None: if self.serial_number is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"instance.serial_number must be set before " - msg += f"calling refresh()" + msg += "instance.serial_number must be set before " + msg += "calling refresh()" self.module.fail_json(msg) self.path = self.endpoints.install_options.get("path") diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index 671189dae..2e7d19b75 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import inspect diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index e786a295a..9385e60af 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, division, print_function -# disabling pylint invalid-name for Ansible standard boilerplate -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import inspect @@ -104,7 +103,7 @@ def refresh(self) -> None: self.properties["response"] = dcnm_send(self.module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) - if self.result["success"] is False or self.result["found"] == False: + if self.result["success"] is False or self.result["found"] is False: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retriving switch " msg += "information from the controller" @@ -870,7 +869,7 @@ def _get(self, item): if self.data_subclass.get(self.device_name) is None: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.device_name} does not exist " - msg += f"on the controller." + msg += "on the controller." self.module.fail_json(msg) if self.data_subclass[self.device_name].get(item) is None: diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 98e7af532..bd855f68b 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -13,49 +13,12 @@ # 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. -""" -Classes and methods for Ansible support of Nexus image upgrade. - -Ansible states "merged", "deleted", and "query" are implemented. - -merged: stage, validate, upgrade image for one or more devices -deleted: delete image policy from one or more devices -query: return switch issu details for one or more devices -""" from __future__ import absolute_import, division, print_function -import copy -import inspect -import json -from typing import Any, Dict, List - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ - ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ - ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ - ImagePolicyAction -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ - ImageStage -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ - ImageUpgrade -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ - ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ - ImageValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ - ImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ - SwitchDetails -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ - SwitchIssuDetailsByIpAddress -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, validate_list_of_dicts) - -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +__email__ = "arobel@cisco.com" DOCUMENTATION = """ --- @@ -66,7 +29,7 @@ - Stage, validate, upgrade images. - Attach, detach, image policies. - Query device issu details. -author: Cisco Systems, Inc. +author: Allen Robel (@quantumonion) options: state: description: @@ -82,13 +45,13 @@ description: - A dictionary containing the image policy configuration. type: dict + required: true suboptions: policy: description: - Image policy name type: str required: true - default: False stage: description: - Stage (True) or unstage (False) an image policy @@ -141,7 +104,7 @@ description: - nxos upgrade mode - Choose between distruptive, non_disruptive, force_non_disruptive - type: string + type: str required: false default: distruptive bios_force: @@ -159,7 +122,7 @@ description: - The switch module to upgrade - Choose between ALL, or integer values - type: string + type: str required: false default: ALL golden: @@ -219,7 +182,6 @@ - Image policy name type: str required: true - default: False stage: description: - Stage (True) or unstage (False) an image policy @@ -272,7 +234,7 @@ description: - nxos upgrade mode - Choose between distruptive, non_disruptive, force_non_disruptive - type: string + type: str required: false default: distruptive bios_force: @@ -290,7 +252,7 @@ description: - The switch module to upgrade - Choose between ALL, or integer values - type: string + type: str required: false default: ALL golden: @@ -347,7 +309,7 @@ # # query: # Return ISSU details for one or more devices. -# +# # deleted: # Delete image policy from one or more devices # @@ -439,9 +401,45 @@ """ +import copy +import inspect +import json +from typing import Any, Dict, List + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ + ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ + ImagePolicyAction +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ + ImageStage +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ + ImageUpgrade +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ + ImageValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ + ImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ + SwitchDetails +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ + SwitchIssuDetailsByIpAddress +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, validate_list_of_dicts) + + class ImageUpgradeTask(ImageUpgradeCommon): """ - Ansible support for image policy attach, detach, and query. + Classes and methods for Ansible support of Nexus image upgrade. + + Ansible states "merged", "deleted", and "query" are implemented. + + merged: stage, validate, upgrade image for one or more devices + deleted: delete image policy from one or more devices + query: return switch issu details for one or more devices """ def __init__(self, module): @@ -1011,7 +1009,6 @@ def _validate_input_for_query_state(self) -> None: msg += f"{','.join(invalid_params)}" self.module.fail_json(msg) - def _merge_global_and_switch_configs(self, config) -> None: """ Merge the global config with each switch config and return @@ -1061,7 +1058,6 @@ def _merge_global_and_switch_configs(self, config) -> None: self.switch_configs.append(switch_config) - def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: """ For any items in config which are not set, apply the default @@ -1356,7 +1352,7 @@ def needs_epld_upgrade(self, epld_modules) -> bool: new_version = module.get("newVersion", "0x0") old_version = module.get("oldVersion", "0x0") # int(str, 0) enables python to guess the base - # of the string when converting to int. An + # of the str when converting to int. An # error is thrown without this. if int(new_version, 0) > int(old_version, 0): msg = f"DEBUG: {self.class_name}.{method_name}: " diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index ef1ff9397..c157faf0e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -13,7 +13,7 @@ # limitations under the License. """ -ImagePolicyAction - unit tests +ImagePolicyAction - unit tests """ # See the following regarding *_fixture imports diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 274dc32bd..ae058de47 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -72,8 +72,6 @@ def test_image_mgmt_upgrade_00001(image_upgrade) -> None: assert instance.verb is None - - def test_image_mgmt_upgrade_00003(image_upgrade) -> None: """ Function diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py index ada664879..b351c8fa0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py @@ -205,6 +205,7 @@ def test_image_mgmt_upgrade_task_00006(image_upgrade_task) -> None: assert instance.defaults["options"]["package"]["install"] is False assert instance.defaults["options"]["package"]["uninstall"] is False + def test_image_mgmt_upgrade_task_00020(monkeypatch, image_upgrade_task) -> None: """ Function @@ -361,7 +362,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert switch_2.get("upgrade").get("nxos") is True assert switch_2.get("validate") is True -#--------------------------------------------------------- def test_image_mgmt_upgrade_task_00040(image_upgrade_task) -> None: """ From 403b44db62cb427835e89c0640427798a068e7ce Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 7 Dec 2023 07:30:45 -1000 Subject: [PATCH 130/300] Ignore missing-gplv3-license for dcnm_image_upgrade.py --- tests/sanity/ignore-2.10.txt | 1 + tests/sanity/ignore-2.11.txt | 1 + tests/sanity/ignore-2.12.txt | 1 + tests/sanity/ignore-2.13.txt | 1 + tests/sanity/ignore-2.14.txt | 1 + tests/sanity/ignore-2.9.txt | 1 + 6 files changed, 6 insertions(+) diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 139297635..00e618d18 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -1,5 +1,6 @@ plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 2361dc569..5304e1273 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -1,5 +1,6 @@ plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 9c0b33aeb..e0566313c 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -1,5 +1,6 @@ plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index 504210f69..6e059a8b0 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -1,5 +1,6 @@ plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 225d528f4..bd64c646c 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -1,5 +1,6 @@ plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 139297635..00e618d18 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -1,5 +1,6 @@ plugins/modules/dcnm_vrf.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_network.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_image_upgrade.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_interface.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_inventory.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_policy.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module From 32496d132cbc63dab2c489e391b419161fbfea9c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 12 Dec 2023 19:40:23 -1000 Subject: [PATCH 131/300] Move all parameter validation to ParamsValidator class The goal here is to separate parameter validation from things like applying defaults and to isolate it so that it's separately testable. A few testcases need to be moved/rewritten as a result of this commit. These are now commented out: test_image_mgmt_upgrade_task_00003 test_image_mgmt_upgrade_task_00004 test_image_mgmt_upgrade_task_00005 --- .../image_mgmt/params_validator.py | 202 +++++++++++++ plugins/modules/dcnm_image_upgrade.py | 265 ++++-------------- .../image_upgrade_playbook_configs.json | 2 +- ...pgrade_image_upgrade_image_upgrade_task.py | 127 ++++----- 4 files changed, 325 insertions(+), 271 deletions(-) create mode 100644 plugins/module_utils/image_mgmt/params_validator.py diff --git a/plugins/module_utils/image_mgmt/params_validator.py b/plugins/module_utils/image_mgmt/params_validator.py new file mode 100644 index 000000000..3ed0f3b72 --- /dev/null +++ b/plugins/module_utils/image_mgmt/params_validator.py @@ -0,0 +1,202 @@ +""" +Validate that parameters conform to specification params_spec +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import inspect +import ipaddress +from collections.abc import MutableMapping as Map +from typing import Any, List + +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon + + +class ParamsValidator(ImageUpgradeCommon): + """ + Validate playbook parameters. + + This expects the following: + 1. parameters: fully-merged dictionary of parameters + 2. params_spec: Dictionary that describes each parameter + in parameters + + Usage (where module is an instance of AnsibleModule): + + Assume the following params_spec describing parameters + ip_address and foo. + ip_address is a required parameter of type ipv4. + foo is an optional parameter of type dict. + foo contains a parameter named bar that is an optional + parameter of type str with a default value of bingo. + bar can be assigned one of three values: bingo, bango, or bongo. + + params_spec: Dict[str, Any] = {} + params_spec["ip_address"] = {} + params_spec["ip_address"]["required"] = False + params_spec["ip_address"]["type"] = "ipv4" + params_spec["foo"] = {} + params_spec["foo"]["required"] = False + params_spec["foo"]["type"] = "dict" + params_spec["foo"]["default"] = {} + params_spec["foo"]["bar"] = {} + params_spec["foo"]["bar"]["required"] = False + params_spec["foo"]["bar"]["type"] = "str" + params_spec["foo"]["bar"]["default"] = "bingo" + params_spec["foo"]["bar"]["choices"] = ["bingo", "bango", "bongo"] + + Which describes the following YAML: + + ip_address: 1.2.3.4 + foo: + bar: bingo + + validator = ParamsValidator(module) + validator.parameters = module.params + validator.params_spec = params_spec + """ + + def __init__(self, module): + super().__init__(module) + self.class_name = __class__.__name__ + self.properties = {} + self.properties["parameters"] = None + self.properties["params_spec"] = None + self.reserved_params = set() + self.reserved_params.add("required") + self.reserved_params.add("type") + self.reserved_params.add("default") + self.reserved_params.add("choices") + + def validate(self) -> None: + """ + Verify that parameters in self.parameters conform to self.params_spec + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.validate_parameters(self.params_spec, self.parameters) + + def validate_parameters(self, spec, parameters): + """ + Recursively traverse parameters and verify conformity with spec + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + for param in spec: + if param in self.reserved_params: + continue + + self.log_msg(f"DEBUG: {self.class_name}.{method_name}: param: {param}") + if isinstance(spec[param], Map): + self.validate_parameters(spec[param], parameters.get(param, {})) + + # We shouldn't hit this since defaults are merged for all + # missing parameters, but just in case... + if ( + parameters.get(param, None) is None and + spec[param].get("required", False) is True + ): + msg = f"{self.class_name}.{method_name}: " + msg += f"Playbook is missing mandatory parameter: {param}." + self.module.fail_json(msg) + + self.verify_type(spec[param]["type"], parameters[param], param) + self.verify_choices(spec[param].get("choices", None), parameters[param], param) + + def verify_choices(self, choices: List[Any], value: Any, param: str) -> None: + """ + Verify that the value is one of the choices + """ + method_name = inspect.stack()[0][3] + if choices is None: + return + + if value not in choices: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid value for parameter '{param}'. " + msg += f"Expected one of {choices}. " + msg += f"Got {value}" + self.module.fail_json(msg) + + def verify_type(self, expected_type: str, value: Any, param: str) -> None: + """ + Verify that the type of value matches the expected type + """ + method_name = inspect.stack()[0][3] + + invalid = False + if expected_type == "str": + if not isinstance(value, str): + invalid = True + if expected_type == "bool": + if not isinstance(value, bool): + invalid = True + if expected_type == "int": + if not isinstance(value, int): + invalid = True + if expected_type == "dict": + if not isinstance(value, dict): + invalid = True + if expected_type == "list": + if not isinstance(value, list): + invalid = True + if expected_type == "set": + if not isinstance(value, set): + invalid = True + if expected_type == "tuple": + if not isinstance(value, tuple): + invalid = True + if expected_type == "float": + if not isinstance(value, float): + invalid = True + if expected_type == "ipv4": + try: + ipaddress.IPv4Address(value) + except ipaddress.AddressValueError: + invalid = True + if expected_type == "ipv6": + try: + ipaddress.IPv6Address(value) + except ipaddress.AddressValueError: + invalid = True + if expected_type == "ipv4_subnet": + try: + ipaddress.IPv4Network(value) + except ValueError: + invalid = True + if expected_type == "ipv6_subnet": + try: + ipaddress.IPv6Network(value) + except ValueError: + invalid = True + + if invalid is True: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid type for parameter '{param}'. " + msg += f"Expected {expected_type}. " + msg += f"Got '{value}'." + self.module.fail_json(msg) + + @property + def parameters(self): + """ + The parameters to validate. + parameters have the same structure as params_spec. + """ + return self.properties["parameters"] + + @parameters.setter + def parameters(self, value): + self.properties["parameters"] = value + + @property + def params_spec(self): + """ + The param specification used to validate the parameters + """ + return self.properties["params_spec"] + + @params_spec.setter + def params_spec(self, value): + self.properties["params_spec"] = value diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index bd855f68b..023a06a0d 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -429,6 +429,8 @@ SwitchIssuDetailsByIpAddress from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_send, validate_list_of_dicts) +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.params_validator import \ + ParamsValidator class ImageUpgradeTask(ImageUpgradeCommon): @@ -477,33 +479,6 @@ def __init__(self, module): self.result = {"changed": False, "diff": [], "response": []} self.mandatory_global_keys = {"switches"} - self.mandatory_switch_keys = {"ip_address"} - - if not self.mandatory_global_keys.issubset(self.config): - msg = f"{self.class_name}.{method_name}: " - msg += "Missing mandatory key(s) in playbook global config. " - msg += f"expected {self.mandatory_global_keys}, " - msg += f"got {self.config.keys()}" - self.module.fail_json(msg) - - if not isinstance(self.config["switches"], list): - msg = f"{self.class_name}.{method_name}: " - msg += "expected list type for self.config['switches']. " - msg += f"got {type(self.config['switches']).__name__}" - self.module.fail_json(msg) - - if len(self.config["switches"]) == 0: - msg = f"{self.class_name}.{method_name}: " - msg += "missing list of switches in playbook config." - self.module.fail_json(msg) - - for switch in self.config["switches"]: - if not self.mandatory_switch_keys.issubset(switch): - msg = f"{self.class_name}.{method_name}: " - msg += "missing mandatory key(s) in playbook switch config. " - msg += f"expected {self.mandatory_switch_keys}, " - msg += f"got {switch.keys()}" - self.module.fail_json(msg) self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) @@ -567,8 +542,6 @@ def get_want(self) -> None: self.log_msg(msg) self._validate_switch_configs() - if not self.switch_configs: - return self.want = self.switch_configs @@ -576,6 +549,10 @@ def get_want(self) -> None: msg += f"self.want: {json.dumps(self.want, indent=4, sort_keys=True)}" self.log_msg(msg) + if len(self.want) == 0: + self.result["changed"] = False + self.exit_json(**self.result) + def _build_idempotent_want(self, want) -> None: """ Build an itempotent want item based on the have item contents. @@ -583,6 +560,8 @@ def _build_idempotent_want(self, want) -> None: The have item is obtained from an instance of SwitchIssuDetails created in self.get_have(). + Caller: self.get_need_merged() + want structure passed to this method: { @@ -611,7 +590,6 @@ def _build_idempotent_want(self, want) -> None: and values are modified based on results from the have item, and the information returned by ImageInstallOptions. - Caller: self.get_need_merged() """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable @@ -621,28 +599,13 @@ def _build_idempotent_want(self, want) -> None: self.have.ip_address = want["ip_address"] - # msg = f"DEBUG: {self.class_name}.{method_name}: " - # msg += f"self.have.ip_address: {self.have.ip_address}" - # self.log_msg(msg) - want["policy_changed"] = True # The switch does not have an image policy attached. # idempotent_want == want with policy_changed = True if self.have.serial_number is None: self.idempotent_want = copy.deepcopy(want) - # msg = f"DEBUG: {self.class_name}.{method_name}: " - # msg += "returning due to serial_number is None. " - # msg += "self.idempotent_want: " - # msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" - # self.log_msg(msg) return - # msg = f"DEBUG: {self.class_name}.{method_name}: " - # msg += f"self.have.serial_number: {self.have.serial_number}, " - # msg += f"want['policy']: {want['policy']}, " - # msg += f"self.have.policy: {self.have.policy}" - # self.log_msg(msg) - # The switch has an image policy attached which is # different from the want policy. # idempotent_want == want with policy_changed = True @@ -726,15 +689,19 @@ def get_need_merged(self) -> None: for want in self.want: self.have.ip_address = want["ip_address"] + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"self.have.serial_number: {self.have.serial_number}" self.log_msg(msg) + if self.have.serial_number is not None: self._build_idempotent_want(want) + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "self.idempotent_want: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" self.log_msg(msg) + test_idempotence = set() test_idempotence.add(self.idempotent_want["policy_changed"]) test_idempotence.add(self.idempotent_want["stage"]) @@ -797,10 +764,14 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: Build the specs for the parameters expected when state == merged. Caller: _validate_input_for_merged_state() - Return: params_spec, a dictionary containing the set of - playbook parameter specifications. + Return: params_spec, a dictionary containing playbook + parameter specifications. """ params_spec: Dict[str, Any] = {} + params_spec["ip_address"] = {} + params_spec["ip_address"]["required"] = True + params_spec["ip_address"]["type"] = "ipv4" + params_spec["policy"] = {} params_spec["policy"]["required"] = False params_spec["policy"]["type"] = "str" @@ -836,6 +807,9 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec[section][sub_section]["mode"]["required"] = False params_spec[section][sub_section]["mode"]["type"] = "str" params_spec[section][sub_section]["mode"]["default"] = "disruptive" + params_spec[section][sub_section]["mode"]["choices"] = [ + "disruptive", "non_disruptive", "force_non_disruptive"] + params_spec[section][sub_section]["bios_force"] = {} params_spec[section][sub_section]["bios_force"]["required"] = False @@ -852,6 +826,10 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec[section][sub_section]["module"]["required"] = False params_spec[section][sub_section]["module"]["type"] = "str" params_spec[section][sub_section]["module"]["default"] = "ALL" + params_spec[section][sub_section]["module"]["choices"] = [ + str(x) for x in range(1, 19)] + params_spec[section][sub_section]["module"]["choices"].extend([x for x in range(1,19)]) + params_spec[section][sub_section]["module"]["choices"].append("ALL") params_spec[section][sub_section]["golden"] = {} params_spec[section][sub_section]["golden"]["required"] = False @@ -892,122 +870,6 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: return copy.deepcopy(params_spec) - def validate_input(self) -> None: - """ - Caller: main() - - Validate the playbook parameters - """ - method_name = inspect.stack()[0][3] - - state = self.params["state"] - - if state not in ["merged", "deleted", "query"]: - msg = f"{self.class_name}.{method_name}: " - msg += "This module supports deleted, merged, and query states. " - msg += f"Got state {state}" - self.module.fail_json(msg) - - if state == "merged": - self._validate_input_for_merged_state() - return - if state == "deleted": - self._validate_input_for_deleted_state() - return - if state == "query": - self._validate_input_for_query_state() - return - - def _validate_input_for_merged_state(self) -> None: - """ - Caller: self.validate_input() - - Validate that self.config contains appropriate values for merged state - """ - method_name = inspect.stack()[0][3] - - if not self.config: - msg = f"{self.class_name}.{method_name}: " - msg += "config: element is mandatory for state merged" - self.module.fail_json(msg) - - params_spec = self._build_params_spec_for_merged_state() - - valid_params, invalid_params = validate_list_of_dicts( - self.config.get("switches"), params_spec, self.module - ) - # We're not using self.validated. Keeping this to avoid - # linter error due to non-use of valid_params - self.validated = copy.deepcopy(valid_params) - - if invalid_params: - msg = f"{self.class_name}.{method_name}: " - msg += "Invalid parameters in playbook: " - msg += f"{','.join(invalid_params)}" - self.module.fail_json(msg) - - def _validate_input_for_deleted_state(self) -> None: - """ - Caller: self.validate_input() - - Validate that self.config contains appropriate values for deleted state - - NOTES: - 1. This is currently identical to _validate_input_for_merged_state() - 2. Adding in case there are differences in the future - """ - method_name = inspect.stack()[0][3] - - params_spec = self._build_params_spec_for_merged_state() - if not self.config: - msg = f"{self.class_name}.{method_name}: " - msg += "config: element is mandatory for state deleted" - self.module.fail_json(msg) - - valid_params, invalid_params = validate_list_of_dicts( - self.config.get("switches"), params_spec, self.module - ) - # We're not using self.validated. Keeping this to avoid - # linter error due to non-use of valid_params - self.validated = copy.deepcopy(valid_params) - - if invalid_params: - msg = f"{self.class_name}.{method_name}: " - msg += "Invalid parameters in playbook: " - msg += f"{','.join(invalid_params)}" - self.module.fail_json(msg) - - def _validate_input_for_query_state(self) -> None: - """ - Caller: self.validate_input() - - Validate that self.config contains appropriate values for query state - - NOTES: - 1. This is currently identical to _validate_input_for_merged_state() - 2. Adding in case there are differences in the future - """ - method_name = inspect.stack()[0][3] - - params_spec = self._build_params_spec_for_merged_state() - - if not self.config: - msg = f"{self.class_name}.{method_name}: " - msg += "config: element is mandatory for state query" - self.module.fail_json(msg) - - valid_params, invalid_params = validate_list_of_dicts( - self.config.get("switches"), params_spec, self.module - ) - # We're not using self.validated. Keeping this to avoid - # linter error due to non-use of valid_params - self.validated = copy.deepcopy(valid_params) - - if invalid_params: - msg = f"{self.class_name}.{method_name}: " - msg += "Invalid parameters in playbook: " - msg += f"{','.join(invalid_params)}" - self.module.fail_json(msg) def _merge_global_and_switch_configs(self, config) -> None: """ @@ -1064,58 +926,46 @@ def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: value from self.defaults. """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - # self._init_defaults() default_config = copy.deepcopy(self.defaults) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"default_config: {json.dumps(default_config, indent=4, sort_keys=True)}" - self.log_msg(msg) - switch_config = copy.deepcopy(config) merged_config = self.merge_dicts(default_config, switch_config) + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"default_config: {json.dumps(default_config, indent=4, sort_keys=True)}" self.log_msg(msg) + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"switch_config: {json.dumps(switch_config, indent=4, sort_keys=True)}" self.log_msg(msg) + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"merged_config: {json.dumps(merged_config, indent=4, sort_keys=True)}" self.log_msg(msg) + return self.merge_dicts(default_config, switch_config) def _validate_switch_configs(self) -> None: """ - Ensure mandatory parameters are present for each switch - - fail_json if this isn't the case - Set defaults for missing optional parameters - - NOTES: - 1. Final application of missing default parameters is done in - self._merge_defaults_to_switch_config + Verify parameters for each switch + - fail_json if any parameters are not valid Callers: - self.get_want """ method_name = inspect.stack()[0][3] + validator = ParamsValidator(self.module) + validator.params_spec = self._build_params_spec_for_merged_state() + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"params_spec[options][epld][module][choices]: " + msg += f"{validator.params_spec['options']['epld']['module']['choices']}" + self.log_msg(msg) - for switch in self.switch_configs: - if not switch.get("ip_address"): - msg = f"{self.class_name}.{method_name}: " - msg = "playbook is missing ip_address for at least one switch" - self.module.fail_json(msg) - # for query state, the only mandatory parameter is ip_address - # so skip the remaining checks - if self.params.get("state") == "query": - continue + for switch in self.switch_configs: + validator.parameters = switch + validator.validate() - if switch.get("policy") is None: - msg = f"{self.class_name}.{method_name}: " - msg += "playbook is missing image policy for switch " - msg += f"{switch.get('ip_address')} " - msg += "and global image policy is not defined." - self.module.fail_json(msg) def _build_policy_attach_payload(self) -> None: """ @@ -1400,6 +1250,10 @@ def handle_merged_state(self) -> None: self.switch_details.refresh() for switch in self.need: + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"switch: {json.dumps(switch, indent=4, sort_keys=True)}" + self.log_msg(msg) + self.switch_details.ip_address = switch.get("ip_address") device = {} device["serial_number"] = self.switch_details.serial_number @@ -1521,9 +1375,9 @@ def main(): ansible_module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) task_module = ImageUpgradeTask(ansible_module) - task_module.validate_input() - task_module.get_have() + task_module.get_want() + task_module.get_have() if ansible_module.params["state"] == "merged": task_module.get_need_merged() @@ -1532,25 +1386,22 @@ def main(): elif ansible_module.params["state"] == "query": task_module.get_need_query() - if ansible_module.params["state"] == "query": - task_module.result["changed"] = False - if ansible_module.params["state"] in ["merged", "deleted"]: - if task_module.need: - task_module.result["changed"] = True - else: - ansible_module.exit_json(**task_module.result) + task_module.result["changed"] = False + if len(task_module.need) == 0: + ansible_module.exit_json(**task_module.result) if ansible_module.check_mode: - task_module.result["changed"] = False ansible_module.exit_json(**task_module.result) - if task_module.need: - if ansible_module.params["state"] == "merged": - task_module.handle_merged_state() - elif ansible_module.params["state"] == "deleted": - task_module.handle_deleted_state() - elif ansible_module.params["state"] == "query": - task_module.handle_query_state() + if ansible_module.params["state"] in ["merged", "deleted"]: + task_module.result["changed"] = True + + if ansible_module.params["state"] == "merged": + task_module.handle_merged_state() + elif ansible_module.params["state"] == "deleted": + task_module.handle_deleted_state() + elif ansible_module.params["state"] == "query": + task_module.handle_query_state() ansible_module.exit_json(**task_module.result) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json index 7bebf20f3..430b54a91 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json @@ -62,7 +62,7 @@ "ip_address": "2.2.2.2", "options": { "epld": { - "module": 1, + "module": "1", "golden": true }, "nxos": { diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py index b351c8fa0..6057d4705 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py @@ -94,8 +94,8 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: assert instance.want == [] assert instance.need == [] assert instance.result == {"changed": False, "diff": [], "response": []} - assert instance.mandatory_global_keys == {"switches"} - assert instance.mandatory_switch_keys == {"ip_address"} + # assert instance.mandatory_global_keys == {"switches"} + # assert instance.mandatory_switch_keys == {"ip_address"} assert isinstance(instance.switch_details, SwitchDetails) assert isinstance(instance.image_policies, ImagePolicies) @@ -118,66 +118,67 @@ def test_image_mgmt_upgrade_task_00002(image_upgrade_task_bare) -> None: assert isinstance(instance, ImageUpgradeTask) -def test_image_mgmt_upgrade_task_00003(image_upgrade_task_bare) -> None: - """ - Function - - __init__ - - Test - - fail_json is called because config.switches is not a list - """ - key = "test_image_mgmt_upgrade_task_00003a" - - match = "ImageUpgradeTask.__init__: expected list type for " - match += r"self.config\['switches'\]. got str" - - mock_ansible_module = MockAnsibleModule() - mock_ansible_module.params = load_playbook_config(key) - with pytest.raises(AnsibleFailJson, match=match): - instance = image_upgrade_task_bare(mock_ansible_module) - assert isinstance(instance, ImageUpgradeTask) - - -def test_image_mgmt_upgrade_task_00004(image_upgrade_task_bare) -> None: - """ - Function - - __init__ - - Test - - fail_json is called because config.switches is empty - """ - key = "test_image_mgmt_upgrade_task_00004a" - - match = "ImageUpgradeTask.__init__: missing list of switches " - match += "in playbook config." - - mock_ansible_module = MockAnsibleModule() - mock_ansible_module.params = load_playbook_config(key) - with pytest.raises(AnsibleFailJson, match=match): - instance = image_upgrade_task_bare(mock_ansible_module) - assert isinstance(instance, ImageUpgradeTask) - - -def test_image_mgmt_upgrade_task_00005(image_upgrade_task_bare) -> None: - """ - Function - - __init__ - - Test - - fail_json is called because mandatory keys are missing in - one of the switch configs - """ - key = "test_image_mgmt_upgrade_task_00005a" - - match = "ImageUpgradeTask.__init__: missing mandatory " - match += r"key\(s\) in playbook switch config. expected " - match += r"\{'ip_address'\}, got dict_keys\(\['foo'\]\)" - - mock_ansible_module = MockAnsibleModule() - mock_ansible_module.params = load_playbook_config(key) - with pytest.raises(AnsibleFailJson, match=match): - instance = image_upgrade_task_bare(mock_ansible_module) - assert isinstance(instance, ImageUpgradeTask) +# This functionality is now in params_validator.py +# def test_image_mgmt_upgrade_task_00003(image_upgrade_task_bare) -> None: +# """ +# Function +# - __init__ + +# Test +# - fail_json is called because config.switches is not a list +# """ +# key = "test_image_mgmt_upgrade_task_00003a" + +# match = "ImageUpgradeTask.__init__: expected list type for " +# match += r"self.config\['switches'\]. got str" + +# mock_ansible_module = MockAnsibleModule() +# mock_ansible_module.params = load_playbook_config(key) +# with pytest.raises(AnsibleFailJson, match=match): +# instance = image_upgrade_task_bare(mock_ansible_module) +# assert isinstance(instance, ImageUpgradeTask) + +# This functionality is now in params_validator.py +# def test_image_mgmt_upgrade_task_00004(image_upgrade_task_bare) -> None: +# """ +# Function +# - __init__ + +# Test +# - fail_json is called because config.switches is empty +# """ +# key = "test_image_mgmt_upgrade_task_00004a" + +# match = "ImageUpgradeTask.__init__: missing list of switches " +# match += "in playbook config." + +# mock_ansible_module = MockAnsibleModule() +# mock_ansible_module.params = load_playbook_config(key) +# with pytest.raises(AnsibleFailJson, match=match): +# instance = image_upgrade_task_bare(mock_ansible_module) +# assert isinstance(instance, ImageUpgradeTask) + +# This functionality is now in params_validator.py +# def test_image_mgmt_upgrade_task_00005(image_upgrade_task_bare) -> None: +# """ +# Function +# - __init__ + +# Test +# - fail_json is called because mandatory keys are missing in +# one of the switch configs +# """ +# key = "test_image_mgmt_upgrade_task_00005a" + +# match = "ImageUpgradeTask.__init__: missing mandatory " +# match += r"key\(s\) in playbook switch config. expected " +# match += r"\{'ip_address'\}, got dict_keys\(\['foo'\]\)" + +# mock_ansible_module = MockAnsibleModule() +# mock_ansible_module.params = load_playbook_config(key) +# with pytest.raises(AnsibleFailJson, match=match): +# instance = image_upgrade_task_bare(mock_ansible_module) +# assert isinstance(instance, ImageUpgradeTask) def test_image_mgmt_upgrade_task_00006(image_upgrade_task) -> None: @@ -277,7 +278,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert switch_2.get("ip_address") == "2.2.2.2" assert switch_2.get("options").get("epld").get("golden") is True - assert switch_2.get("options").get("epld").get("module") == 1 + assert switch_2.get("options").get("epld").get("module") == "1" assert switch_2.get("options").get("nxos").get("bios_force") is True assert switch_2.get("options").get("nxos").get("mode") == "non_disruptive" assert switch_2.get("options").get("package").get("install") is True From 98f773439f411dcde573b42aacbf6a81ec3a9e34 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 13 Dec 2023 11:23:33 -1000 Subject: [PATCH 132/300] Decouple _merge_defaults_to_switch_configs from _merge_global_and_switch_configs Make _merge_global_and_switch_configs self-contained ParamsValidator: Add ability for a param to match more than one type These changes required changes to the unit tests associated with _merge_defaults_to_switch_configs --- ...params_validator.py => params_validate.py} | 69 +- plugins/modules/dcnm_image_upgrade.py | 101 +-- ...pgrade_image_upgrade_image_upgrade_task.py | 643 +++++++++--------- 3 files changed, 465 insertions(+), 348 deletions(-) rename plugins/module_utils/image_mgmt/{params_validator.py => params_validate.py} (72%) diff --git a/plugins/module_utils/image_mgmt/params_validator.py b/plugins/module_utils/image_mgmt/params_validate.py similarity index 72% rename from plugins/module_utils/image_mgmt/params_validator.py rename to plugins/module_utils/image_mgmt/params_validate.py index 3ed0f3b72..fc667a111 100644 --- a/plugins/module_utils/image_mgmt/params_validator.py +++ b/plugins/module_utils/image_mgmt/params_validate.py @@ -14,7 +14,7 @@ ImageUpgradeCommon -class ParamsValidator(ImageUpgradeCommon): +class ParamsValidate(ImageUpgradeCommon): """ Validate playbook parameters. @@ -124,7 +124,9 @@ def verify_type(self, expected_type: str, value: Any, param: str) -> None: Verify that the type of value matches the expected type """ method_name = inspect.stack()[0][3] - + if isinstance(expected_type, list): + self.verify_multitype(expected_type, value, param) + return invalid = False if expected_type == "str": if not isinstance(value, str): @@ -178,6 +180,69 @@ def verify_type(self, expected_type: str, value: Any, param: str) -> None: msg += f"Got '{value}'." self.module.fail_json(msg) + def verify_multitype(self, expected_types: List[str], value: Any, param: str) -> None: + """ + Verify that the type of value matches one of the expected types + """ + method_name = inspect.stack()[0][3] + invalid = True + for expected_type in expected_types: + if expected_type == "str": + if isinstance(value, str): + invalid = False + if expected_type == "bool": + if isinstance(value, bool): + invalid = False + if expected_type == "int": + if isinstance(value, int): + invalid = False + if expected_type == "dict": + if isinstance(value, dict): + invalid = False + if expected_type == "list": + if isinstance(value, list): + invalid = False + if expected_type == "set": + if isinstance(value, set): + invalid = False + if expected_type == "tuple": + if isinstance(value, tuple): + invalid = False + if expected_type == "float": + if isinstance(value, float): + invalid = False + if expected_type == "ipv4": + try: + ipaddress.IPv4Address(value) + invalid = False + except ipaddress.AddressValueError: + pass + if expected_type == "ipv6": + try: + ipaddress.IPv6Address(value) + invalid = False + except ipaddress.AddressValueError: + pass + if expected_type == "ipv4_subnet": + try: + ipaddress.IPv4Network(value) + invalid = False + except ValueError: + pass + if expected_type == "ipv6_subnet": + try: + ipaddress.IPv6Network(value) + invalid = False + except ValueError: + pass + + if invalid is True: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid type for parameter '{param}'. " + msg += f"Expected one of {expected_types}. " + msg += f"Got '{value}'." + self.module.fail_json(msg) + @property def parameters(self): """ diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 023a06a0d..11516b44a 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -406,6 +406,8 @@ import json from typing import Any, Dict, List +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -423,14 +425,12 @@ ImageValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ ImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.params_validate import \ + ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ SwitchDetails from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsByIpAddress -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, validate_list_of_dicts) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.params_validator import \ - ParamsValidator class ImageUpgradeTask(ImageUpgradeCommon): @@ -536,6 +536,8 @@ def get_want(self) -> None: self._merge_global_and_switch_configs(self.config) + self._merge_defaults_to_switch_configs() + msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "Calling _validate_switch_configs with self.switch_configs: " msg += f"{json.dumps(self.switch_configs, indent=4, sort_keys=True)}" @@ -763,7 +765,7 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: """ Build the specs for the parameters expected when state == merged. - Caller: _validate_input_for_merged_state() + Caller: _validate_switch_configs() Return: params_spec, a dictionary containing playbook parameter specifications. """ @@ -824,11 +826,11 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec[section][sub_section]["module"] = {} params_spec[section][sub_section]["module"]["required"] = False - params_spec[section][sub_section]["module"]["type"] = "str" + params_spec[section][sub_section]["module"]["type"] = ["str", "int"] params_spec[section][sub_section]["module"]["default"] = "ALL" params_spec[section][sub_section]["module"]["choices"] = [ - str(x) for x in range(1, 19)] - params_spec[section][sub_section]["module"]["choices"].extend([x for x in range(1,19)]) + str(x) for x in range(1, 33)] + params_spec[section][sub_section]["module"]["choices"].extend([x for x in range(1,33)]) params_spec[section][sub_section]["module"]["choices"].append("ALL") params_spec[section][sub_section]["golden"] = {} @@ -871,21 +873,38 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: return copy.deepcopy(params_spec) + @staticmethod + def _build_params_spec_for_query_state() -> Dict[str, Any]: + """ + Build the specs for the parameters expected when state == query. + + Caller: _validate_switch_configs() + Return: params_spec, a dictionary containing playbook + parameter specifications. + """ + params_spec: Dict[str, Any] = {} + params_spec["ip_address"] = {} + params_spec["ip_address"]["required"] = True + params_spec["ip_address"]["type"] = "ipv4" + + return copy.deepcopy(params_spec) + + def _merge_global_and_switch_configs(self, config) -> None: """ - Merge the global config with each switch config and return - a dict of switch configs keyed on switch ip_address. + Merge the global config with each switch config and + populate list of merged configs self.switch_configs. Merge rules: 1. switch_config takes precedence over global_config. 2. If switch_config is missing a parameter, use parameter from global_config. - 3. If a switch_config has a parameter, use it. - 4. If global_config and switch_config are both missing an - optional parameter, use the parameter's default value - (done in ImageUpgrade.build_payload) + 3. If switch_config has a parameter, use it. + 4. If global_config and switch_config are both missing a + parameter, use the parameter's default value, if there + is one (see self._merge_defaults_to_switch_configs) 5. If global_config and switch_config are both missing a - mandatory parameter, fail (done in self._validate_switch_configs) + mandatory parameter, fail (see self._validate_switch_configs) """ method_name = inspect.stack()[0][3] @@ -895,6 +914,7 @@ def _merge_global_and_switch_configs(self, config) -> None: self.module.fail_json(msg) self.switch_configs = [] + merged_configs = [] for switch in config["switches"]: # we need to rebuild global_config in this loop # because merge_dicts modifies it in place @@ -916,51 +936,54 @@ def _merge_global_and_switch_configs(self, config) -> None: msg += f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" self.log_msg(msg) - switch_config = self._merge_defaults_to_switch_config(switch_config) + merged_configs.append(switch_config) + self.switch_configs = copy.copy(merged_configs) - self.switch_configs.append(switch_config) - def _merge_defaults_to_switch_config(self, config) -> Dict[str, Any]: + def _merge_defaults_to_switch_configs(self) -> None: """ For any items in config which are not set, apply the default value from self.defaults. """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - default_config = copy.deepcopy(self.defaults) - switch_config = copy.deepcopy(config) - merged_config = self.merge_dicts(default_config, switch_config) - - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"default_config: {json.dumps(default_config, indent=4, sort_keys=True)}" - self.log_msg(msg) + configs_to_merge = copy.copy(self.switch_configs) + merged_configs = [] + for switch_config in configs_to_merge: + # we need to rebuild default_config in this loop + # because merge_dicts modifies it in place + merged_config = self.merge_dicts( + copy.deepcopy(self.defaults), + copy.deepcopy(switch_config)) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"switch_config: {json.dumps(switch_config, indent=4, sort_keys=True)}" - self.log_msg(msg) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"switch_config: {json.dumps(switch_config, indent=4, sort_keys=True)}" + self.log_msg(msg) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"merged_config: {json.dumps(merged_config, indent=4, sort_keys=True)}" - self.log_msg(msg) + msg = f"DEBUG: {self.class_name}.{method_name}: " + msg += f"merged_config: {json.dumps(merged_config, indent=4, sort_keys=True)}" + self.log_msg(msg) + merged_configs.append(merged_config) + self.switch_configs = copy.copy(merged_configs) - return self.merge_dicts(default_config, switch_config) def _validate_switch_configs(self) -> None: """ Verify parameters for each switch - fail_json if any parameters are not valid + - fail_json if any mandatory parameters are missing Callers: - self.get_want """ method_name = inspect.stack()[0][3] - validator = ParamsValidator(self.module) - validator.params_spec = self._build_params_spec_for_merged_state() - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"params_spec[options][epld][module][choices]: " - msg += f"{validator.params_spec['options']['epld']['module']['choices']}" - self.log_msg(msg) - + validator = ParamsValidate(self.module) + if self.module.params.get("state") == "merged": + validator.params_spec = self._build_params_spec_for_merged_state() + if self.module.params.get("state") == "deleted": + validator.params_spec = self._build_params_spec_for_merged_state() + if self.module.params.get("state") == "query": + validator.params_spec = self._build_params_spec_for_query_state() for switch in self.switch_configs: validator.parameters = switch diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py index 6057d4705..6ff097e03 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py @@ -367,493 +367,522 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_mgmt_upgrade_task_00040(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config + - _merge_defaults_to_switch_configs Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - values missing that have defaults defined - (see ImageUpgradeTask._init_defaults) + - instance.switch_configs list is initialized with one switch + config containing mandatory keys only, such that all optional + (default) keys are populated by _merge_defaults_to_switch_configs + (see ImageUpgradeTask._init_defaults) for the default values. Test - - merged_config contains expected default values + - instance.switch_configs contains expected default values """ instance = image_upgrade_task - config = {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + switch_configs = [{"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False}] + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00041(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config + - _merge_defaults_to_switch_configs Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except upgrade.nxos. + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (upgrade.nxos) which is set to a non-default value. Test - - merged_config contains expected default values - - merged_config contains expected non-default values + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - Description - Force code coverage of the upgrade.epld is None path """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "upgrade": {"nxos": False}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is False - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is False + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00042(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config + - _merge_defaults_to_switch_configs Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except upgrade.epld. + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (upgrade.epld) which is set to a non-default value. Test - - merged_config contains expected default values - - merged_config contains expected non-default values + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - Description - Force code coverage of the upgrade.nxos is None path """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "upgrade": {"epld": True}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is True - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is True + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00043(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config + - _merge_defaults_to_switch_configs Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options, which is empty. + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options) which is expected to contain sub-options, but which + is empty. Test - - merged_config contains expected default values - - merged_config contains expected non-default values + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values Description When options is empty, the default values for all sub-options are added """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00044(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config + - _merge_defaults_to_switch_configs Setup - - _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.nxos.mode. + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options.nxos.mode) which is set to a non-default value. Test - Default value for options.nxos.bios_force is added - - merged_config contains expected default values - - merged_config contains expected non-default values + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values + Description + When options.nxos.mode is the only key present in options.nxos, + options.nxos.bios_force sub-option should be added with default value. """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {"nxos": {"mode": "non_disruptive"}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "non_disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "non_disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00045(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config - - Test - - Default value of options.nxos.mode is added + - _merge_defaults_to_switch_configs - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.nxos.bios_force. + Setup + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options.nxos.bios_force) which is set to a non-default value. - Expected results: + Test + - Default value for options.nxos.mode is added + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - 1. merged_config contains the expected default values - 2. merged_config contains the expected non-default values + Description + When options.nxos.bios_force is the only key present in options.nxos, + options.nxos.mode sub-option should be added with default value. """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {"nxos": {"bios_force": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is True - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is True + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00046(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config - - Test - - Default value of options.epld.golden is added + - _merge_defaults_to_switch_configs - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.epld.module. + Setup + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options.epld.module) which is set to a non-default value. - Expected results: + Test + - Default value for options.epld.golden is added + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - 1. merged_config contains the expected default values - 2. merged_config contains the expected non-default values + Description + When options.epld.module is the only key present in options.epld, + options.epld.golden sub-option should be added with default value. """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {"epld": {"module": 27}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == 27 - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == 27 + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00047(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config - - Test - - Default value for options.epld.module is added + - _merge_defaults_to_switch_configs - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.epld.golden. + Setup + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options.epld.golden) which is set to a non-default value. - Expected results: + Test + - Default value for options.epld.module is added + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - 1. options.epld.module is set to ALL - 2. merged_config contains the expected default values - 3. merged_config contains the expected non-default values + Description + When options.epld.golden is the only key present in options.epld, + options.epld.module sub-option should be added with default value. """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {"epld": {"golden": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is True - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is True + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00048(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config + - _merge_defaults_to_switch_configs - Test - - Default value for options.reboot.write_erase is added - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.reboot.config_reload. + Setup + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options.reboot.config_reload) which is set to a non-default + value. - Expected results: + Test + - Default value for options.reboot.write_erase is added + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - 1. options.reboot.write_erase is set to False - 2. merged_config contains the expected default values - 3. merged_config contains the expected non-default values + Description + When options.reboot.config_reload is the only key present in options.reboot, + options.reboot.write_erase sub-option should be added with default value. """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {"reboot": {"config_reload": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is True - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is True + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00049(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config - - Test - - Default value for options.reboot.config_reload is added + - _merge_defaults_to_switch_configs - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.reboot.write_erase. + Setup + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options.reboot.write_erase) which is set to a non-default + value. - Expected results: + Test + - Default value for options.reboot.config_reload is added + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - 1. options.reboot.config_reload is set to False - 2. merged_config contains the expected default values - 3. merged_config contains the expected non-default values + Description + When options.reboot.write_erase is the only key present in options.reboot, + options.reboot.config_reload sub-option should be added with default value. """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {"reboot": {"write_erase": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is True - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is True + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00050(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config + - _merge_defaults_to_switch_configs - Test - - Default value for options.package.uninstall is added - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.package.install. + Setup + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options.package.install) which is set to a non-default + value. - Expected results: + Test + - Default value for options.package.uninstall is added + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - 1. options.package.uninstall is set to False - 2. merged_config contains expected default values - 3. merged_config contains expected non-default values + Description + When options.package.install is the only key present in options.package, + options.package.uninstall sub-option should be added with default value. """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {"package": {"install": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is True - assert merged_config["options"]["package"]["uninstall"] is False + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is True + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False def test_image_mgmt_upgrade_task_00051(image_upgrade_task) -> None: """ Function - - _merge_defaults_to_switch_config + - _merge_defaults_to_switch_configs - Test - - Default value for options.package.install is added - - Setup: - 1. _merge_defaults_to_switch_config is passed a dictionary with all - default values missing except options.package.uninstall. + Setup + - instance.switch_configs list is initialized with one switch + config containing all mandatory keys, and one optional/default + key (options.package.uninstall) which is set to a non-default + value. - Expected results: + Test + - Default value for options.package.install is added + - instance.switch_configs contains expected default values + - instance.switch_configs contains expected non-default values - 1. options.package.install is set to False - 2. merged_config contains the expected default values - 3. merged_config contains the expected non-default values + Description + When options.package.uninstall is the only key present in options.package, + options.package.install sub-option should be added with default value. """ instance = image_upgrade_task - config = { + switch_configs = [{ "policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False, "options": {"package": {"uninstall": True}}, - } - - merged_config = instance._merge_defaults_to_switch_config(config) - assert merged_config["reboot"] is False - assert merged_config["stage"] is True - assert merged_config["validate"] is True - assert merged_config["upgrade"]["nxos"] is True - assert merged_config["upgrade"]["epld"] is False - assert merged_config["options"]["nxos"]["mode"] == "disruptive" - assert merged_config["options"]["nxos"]["bios_force"] is False - assert merged_config["options"]["epld"]["module"] == "ALL" - assert merged_config["options"]["epld"]["golden"] is False - assert merged_config["options"]["reboot"]["config_reload"] is False - assert merged_config["options"]["reboot"]["write_erase"] is False - assert merged_config["options"]["package"]["install"] is False - assert merged_config["options"]["package"]["uninstall"] is True + }] + + instance.switch_configs = switch_configs + instance._merge_defaults_to_switch_configs() + assert instance.switch_configs[0]["reboot"] is False + assert instance.switch_configs[0]["stage"] is True + assert instance.switch_configs[0]["validate"] is True + assert instance.switch_configs[0]["upgrade"]["nxos"] is True + assert instance.switch_configs[0]["upgrade"]["epld"] is False + assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" + assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False + assert instance.switch_configs[0]["options"]["epld"]["module"] == "ALL" + assert instance.switch_configs[0]["options"]["epld"]["golden"] is False + assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False + assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False + assert instance.switch_configs[0]["options"]["package"]["install"] is False + assert instance.switch_configs[0]["options"]["package"]["uninstall"] is True From 6f9587562587401ff7bebc64ccbf90213f2ba034 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 13 Dec 2023 14:56:09 -1000 Subject: [PATCH 133/300] Add integer range validation --- .../image_mgmt/params_validate.py | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/image_mgmt/params_validate.py b/plugins/module_utils/image_mgmt/params_validate.py index fc667a111..6b477f424 100644 --- a/plugins/module_utils/image_mgmt/params_validate.py +++ b/plugins/module_utils/image_mgmt/params_validate.py @@ -22,7 +22,7 @@ class ParamsValidate(ImageUpgradeCommon): 1. parameters: fully-merged dictionary of parameters 2. params_spec: Dictionary that describes each parameter in parameters - + Usage (where module is an instance of AnsibleModule): Assume the following params_spec describing parameters @@ -94,19 +94,32 @@ def validate_parameters(self, spec, parameters): # We shouldn't hit this since defaults are merged for all # missing parameters, but just in case... if ( - parameters.get(param, None) is None and - spec[param].get("required", False) is True + parameters.get(param, None) is None + and spec[param].get("required", False) is True ): msg = f"{self.class_name}.{method_name}: " msg += f"Playbook is missing mandatory parameter: {param}." self.module.fail_json(msg) self.verify_type(spec[param]["type"], parameters[param], param) - self.verify_choices(spec[param].get("choices", None), parameters[param], param) + self.verify_choices( + spec[param].get("choices", None), parameters[param], param + ) + if ( + spec[param].get("type", None) == "int" + and spec[param].get("range_min", None) is not None + and spec[param].get("range_max", None) is not None + ): + self.verify_integer_range( + spec[param].get("range_min", None), + spec[param].get("range_max", None), + parameters[param], + param, + ) def verify_choices(self, choices: List[Any], value: Any, param: str) -> None: """ - Verify that the value is one of the choices + Verify that value is one of the choices """ method_name = inspect.stack()[0][3] if choices is None: @@ -119,9 +132,23 @@ def verify_choices(self, choices: List[Any], value: Any, param: str) -> None: msg += f"Got {value}" self.module.fail_json(msg) + def verify_integer_range( + self, range_min: int, range_max: int, value: int, param: str + ) -> None: + """ + Verify that value is within the range range_min to range_max + """ + method_name = inspect.stack()[0][3] + if value < range_min or value > range_max: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid value for parameter '{param}'. " + msg += f"Expected value between {range_min} and {range_max}. " + msg += f"Got {value}" + self.module.fail_json(msg) + def verify_type(self, expected_type: str, value: Any, param: str) -> None: """ - Verify that the type of value matches the expected type + Verify that value's type matches the expected type """ method_name = inspect.stack()[0][3] if isinstance(expected_type, list): @@ -180,9 +207,11 @@ def verify_type(self, expected_type: str, value: Any, param: str) -> None: msg += f"Got '{value}'." self.module.fail_json(msg) - def verify_multitype(self, expected_types: List[str], value: Any, param: str) -> None: + def verify_multitype( + self, expected_types: List[str], value: Any, param: str + ) -> None: """ - Verify that the type of value matches one of the expected types + Verify that value's type matches one of the expected types """ method_name = inspect.stack()[0][3] invalid = True From 805a453e47c0c241c1ffb5df1f639131b82d2e79 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 13 Dec 2023 16:29:28 -1000 Subject: [PATCH 134/300] Use copy.copy() when assigning need to self.need --- plugins/modules/dcnm_image_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 11516b44a..664cce039 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -718,7 +718,7 @@ def get_need_merged(self) -> None: if True not in test_idempotence: continue need.append(self.idempotent_want) - self.need = need + self.need = copy.copy(need) def get_need_deleted(self) -> None: """ From d2b1dab6e02ac3a33ae7792519757e8124d699d8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 13 Dec 2023 16:38:07 -1000 Subject: [PATCH 135/300] sentence should be on a single line Addressing mikeweibe review comment --- plugins/modules/dcnm_image_upgrade.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 664cce039..1f95124e1 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -60,8 +60,7 @@ default: True validate: description: - - Validate (True) or do not validate (False) the image - - after staging + - Validate (True) or do not validate (False) the image after staging type: bool required: false default: True From f3aef226d39609f63c918e4c4644340f7e8606e8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 13 Dec 2023 16:41:16 -1000 Subject: [PATCH 136/300] Expanded on meaning of validate Addressing mikewiebe review comment. --- plugins/modules/dcnm_image_upgrade.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 1f95124e1..77b2c73cb 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -60,7 +60,8 @@ default: True validate: description: - - Validate (True) or do not validate (False) the image after staging + - Validate (True) or do not validate (False) the image after staging. + - If True, triggers NX-OS to validate that the image is compatible with the switch platform hardware. type: bool required: false default: True From 17b443cd5fd96b1c5d2f6d6cd45dede5f3a0f148 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 13 Dec 2023 16:44:03 -1000 Subject: [PATCH 137/300] Fix copywrite Addressing mikewiebe review comment --- plugins/modules/dcnm_image_upgrade.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 77b2c73cb..631fdda7b 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2023 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. @@ -16,7 +16,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__copyright__ = "Copyright (c) 2023 Cisco and/or its affiliates." __author__ = "Allen Robel" __email__ = "arobel@cisco.com" From 91eea87b31fbdfecc0e6d1dfbdab1e05f2df9d8a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 13 Dec 2023 16:49:31 -1000 Subject: [PATCH 138/300] Align epld description with current playbook structure Addressing mikewiebe review comments. --- plugins/modules/dcnm_image_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 631fdda7b..cad69bd07 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -86,7 +86,7 @@ description: - Enable (True) or disable (False) EPLD upgrade - If upgrade.nxos is false, epld and packages cannot both be true - - If epld is true, nxos_option must be disruptive + - If epld is true, options.nxos.mode must be set to disruptive type: bool required: false default: False From 3117251e7cb2ea4b9e5cdb5fcb7047942d950271 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 13 Dec 2023 16:52:41 -1000 Subject: [PATCH 139/300] Fix nxos options in two places in example playbook Addressing mikewiebe review comments. --- plugins/modules/dcnm_image_upgrade.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index cad69bd07..31c9c346f 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -345,7 +345,7 @@ epld: false options: nxos: - type: disruptive + mode: disruptive epld: module: ALL golden: false @@ -366,7 +366,7 @@ epld: true options: nxos: - type: disruptive + mode: disruptive epld: module: ALL golden: false From 76dcfa5c26f64ad780df01089ef0181fcfc21c05 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 14 Dec 2023 13:24:03 -1000 Subject: [PATCH 140/300] Leverage ansible.module_utils.common validation --- .../image_mgmt/params_validate.py | 136 ++++++++++++------ 1 file changed, 96 insertions(+), 40 deletions(-) diff --git a/plugins/module_utils/image_mgmt/params_validate.py b/plugins/module_utils/image_mgmt/params_validate.py index 6b477f424..c94304016 100644 --- a/plugins/module_utils/image_mgmt/params_validate.py +++ b/plugins/module_utils/image_mgmt/params_validate.py @@ -10,6 +10,7 @@ from collections.abc import MutableMapping as Map from typing import Any, List +from ansible.module_utils.common import validation from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ ImageUpgradeCommon @@ -61,14 +62,17 @@ class ParamsValidate(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) self.class_name = __class__.__name__ + self.validation = validation self.properties = {} self.properties["parameters"] = None self.properties["params_spec"] = None self.reserved_params = set() + self.reserved_params.add("choices") + self.reserved_params.add("default") + self.reserved_params.add("range_max") + self.reserved_params.add("range_min") self.reserved_params.add("required") self.reserved_params.add("type") - self.reserved_params.add("default") - self.reserved_params.add("choices") def validate(self) -> None: """ @@ -156,120 +160,172 @@ def verify_type(self, expected_type: str, value: Any, param: str) -> None: return invalid = False if expected_type == "str": - if not isinstance(value, str): + try: + value = self.validation.check_type_str(value) + except TypeError as error: + invalid = True + elif expected_type == "bool": + try: + value = self.validation.check_type_bool(value) + except TypeError as error: invalid = True - if expected_type == "bool": - if not isinstance(value, bool): + elif expected_type == "int": + try: + value = self.validation.check_type_int(value) + except TypeError as error: invalid = True - if expected_type == "int": - if not isinstance(value, int): + if expected_type == "float": + try: + value = self.validation.check_type_float(value) + except TypeError as error: invalid = True - if expected_type == "dict": - if not isinstance(value, dict): + elif expected_type == "dict": + # check_type_dict() converts strings with format "k1=v1, k2=v2" + # to dict. + try: + value = self.validation.check_type_dict(value) + except TypeError as error: invalid = True - if expected_type == "list": - if not isinstance(value, list): + elif expected_type == "list": + # check_type_list() converts int, str, float to a single-element + # list. It also converts comma-separated strings to lists. + try: + value = self.validation.check_type_list(value) + except TypeError as error: invalid = True - if expected_type == "set": + elif expected_type == "set": + # validate does not have a check_type_set() method if not isinstance(value, set): + error = f"Expected type set. Got type {type(value)} for " + error += f"param {param} with value {value}." invalid = True if expected_type == "tuple": + # validate does not have a check_type_tuple() method if not isinstance(value, tuple): - invalid = True - if expected_type == "float": - if not isinstance(value, float): + error = f"Expected type tuple. Got type {type(value)} for " + error += f"param {param} with value {value}." invalid = True if expected_type == "ipv4": try: ipaddress.IPv4Address(value) - except ipaddress.AddressValueError: + except ipaddress.AddressValueError as error: invalid = True if expected_type == "ipv6": try: ipaddress.IPv6Address(value) - except ipaddress.AddressValueError: + except ipaddress.AddressValueError as error: invalid = True if expected_type == "ipv4_subnet": try: ipaddress.IPv4Network(value) - except ValueError: + except ValueError as error: invalid = True if expected_type == "ipv6_subnet": try: ipaddress.IPv6Network(value) - except ValueError: + except ValueError as error: invalid = True if invalid is True: msg = f"{self.class_name}.{method_name}: " msg += f"Invalid type for parameter '{param}'. " msg += f"Expected {expected_type}. " - msg += f"Got '{value}'." + msg += f"Got '{value}'. " + msg += f"More info: {error}" self.module.fail_json(msg) def verify_multitype( self, expected_types: List[str], value: Any, param: str ) -> None: """ - Verify that value's type matches one of the expected types + Verify that value's type matches one of the types in expected_types """ method_name = inspect.stack()[0][3] invalid = True for expected_type in expected_types: if expected_type == "str": - if isinstance(value, str): + try: + value = self.validation.check_type_str(value) invalid = False - if expected_type == "bool": - if isinstance(value, bool): + except TypeError as error: + pass + elif expected_type == "bool": + try: + value = self.validation.check_type_bool(value) invalid = False - if expected_type == "int": - if isinstance(value, int): + except TypeError as error: + pass + elif expected_type == "int": + try: + value = self.validation.check_type_int(value) invalid = False - if expected_type == "dict": - if isinstance(value, dict): + except TypeError as error: + pass + elif expected_type == "dict": + try: + value = self.validation.check_type_dict(value) invalid = False - if expected_type == "list": - if isinstance(value, list): + except TypeError as error: + pass + elif expected_type == "list": + try: + value = self.validation.check_type_list(value) invalid = False - if expected_type == "set": + except TypeError as error: + pass + elif expected_type == "set": + # validate does not have a check_type_set() method if isinstance(value, set): invalid = False - if expected_type == "tuple": + error = f"Expected type set. Got type {type(value)} for " + error += f"param {param} with value {value}." + elif expected_type == "tuple": + # validate does not have a check_type_tuple() method if isinstance(value, tuple): invalid = False - if expected_type == "float": - if isinstance(value, float): + error = f"Expected type tuple. Got type {type(value)} for " + error += f"param {param} with value {value}." + elif expected_type == "float": + try: + value = self.validation.check_type_float(value) invalid = False - if expected_type == "ipv4": + except TypeError as error: + pass + elif expected_type == "ipv4": try: ipaddress.IPv4Address(value) invalid = False - except ipaddress.AddressValueError: + except ipaddress.AddressValueError as error: pass - if expected_type == "ipv6": + elif expected_type == "ipv6": try: ipaddress.IPv6Address(value) invalid = False except ipaddress.AddressValueError: pass - if expected_type == "ipv4_subnet": + elif expected_type == "ipv4_subnet": try: ipaddress.IPv4Network(value) invalid = False except ValueError: pass - if expected_type == "ipv6_subnet": + elif expected_type == "ipv6_subnet": try: ipaddress.IPv6Network(value) invalid = False except ValueError: pass + else: + error = f"Unknown type {expected_type} for param {param} " + error += f"with value {value}." + invalid = True if invalid is True: msg = f"{self.class_name}.{method_name}: " msg += f"Invalid type for parameter '{param}'. " msg += f"Expected one of {expected_types}. " - msg += f"Got '{value}'." + msg += f"Got '{value}'. " + msg += f"More info: {error}" self.module.fail_json(msg) @property From cf1acbcbbfb71681b5edeb8a56cfb86762a3ff67 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 14 Dec 2023 15:41:34 -1000 Subject: [PATCH 141/300] Decouple ParamsValidate from ImageUpgradeCommon For now, copy log_msg() into ParamsValidate, along with self.debug and self.file_handle. Eventually, we should create a common logging infra that contains log_msg(), a var to enable/disable logging, and a common destination log file. --- .../{image_mgmt => common}/params_validate.py | 56 +++++++++++++------ plugins/modules/dcnm_image_upgrade.py | 40 +++++++------ 2 files changed, 62 insertions(+), 34 deletions(-) rename plugins/module_utils/{image_mgmt => common}/params_validate.py (88%) diff --git a/plugins/module_utils/image_mgmt/params_validate.py b/plugins/module_utils/common/params_validate.py similarity index 88% rename from plugins/module_utils/image_mgmt/params_validate.py rename to plugins/module_utils/common/params_validate.py index c94304016..8c288d66b 100644 --- a/plugins/module_utils/image_mgmt/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -11,11 +11,9 @@ from typing import Any, List from ansible.module_utils.common import validation -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ - ImageUpgradeCommon -class ParamsValidate(ImageUpgradeCommon): +class ParamsValidate: """ Validate playbook parameters. @@ -24,7 +22,7 @@ class ParamsValidate(ImageUpgradeCommon): 2. params_spec: Dictionary that describes each parameter in parameters - Usage (where module is an instance of AnsibleModule): + Usage (where ansible_module is an instance of AnsibleModule): Assume the following params_spec describing parameters ip_address and foo. @@ -54,15 +52,18 @@ class ParamsValidate(ImageUpgradeCommon): foo: bar: bingo - validator = ParamsValidator(module) - validator.parameters = module.params + validator = ParamsValidator(ansible_module) + validator.parameters = ansible_module.params validator.params_spec = params_spec """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): self.class_name = __class__.__name__ + self.ansible_module = ansible_module self.validation = validation + self.debug = False + self.file_handle = None + self.logfile = "/tmp/ansible_dcnm.log" self.properties = {} self.properties["parameters"] = None self.properties["params_spec"] = None @@ -74,6 +75,29 @@ def __init__(self, module): self.reserved_params.add("required") self.reserved_params.add("type") + def log_msg(self, msg): + """ + used for debugging. disable this when committing to main + by setting self.debug to False in __init__() + """ + if self.debug is False: + return + if self.file_handle is None: + try: + # since we need self.file_handle open throughout this class + # we are disabling pylint R1732 + self.file_handle = open( # pylint: disable=consider-using-with + f"{self.logfile}", "a+", encoding="UTF-8" + ) + except IOError as err: + msg = f"error opening logfile {self.logfile}. " + msg += f"detail: {err}" + self.ansible_module.fail_json(msg) + + self.file_handle.write(msg) + self.file_handle.write("\n") + self.file_handle.flush() + def validate(self) -> None: """ Verify that parameters in self.parameters conform to self.params_spec @@ -91,7 +115,6 @@ def validate_parameters(self, spec, parameters): if param in self.reserved_params: continue - self.log_msg(f"DEBUG: {self.class_name}.{method_name}: param: {param}") if isinstance(spec[param], Map): self.validate_parameters(spec[param], parameters.get(param, {})) @@ -103,7 +126,7 @@ def validate_parameters(self, spec, parameters): ): msg = f"{self.class_name}.{method_name}: " msg += f"Playbook is missing mandatory parameter: {param}." - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) self.verify_type(spec[param]["type"], parameters[param], param) self.verify_choices( @@ -134,7 +157,7 @@ def verify_choices(self, choices: List[Any], value: Any, param: str) -> None: msg += f"Invalid value for parameter '{param}'. " msg += f"Expected one of {choices}. " msg += f"Got {value}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) def verify_integer_range( self, range_min: int, range_max: int, value: int, param: str @@ -148,7 +171,7 @@ def verify_integer_range( msg += f"Invalid value for parameter '{param}'. " msg += f"Expected value between {range_min} and {range_max}. " msg += f"Got {value}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) def verify_type(self, expected_type: str, value: Any, param: str) -> None: """ @@ -162,7 +185,7 @@ def verify_type(self, expected_type: str, value: Any, param: str) -> None: if expected_type == "str": try: value = self.validation.check_type_str(value) - except TypeError as error: + except TypeError as error: # pylint: disable=unused-variable invalid = True elif expected_type == "bool": try: @@ -232,7 +255,7 @@ def verify_type(self, expected_type: str, value: Any, param: str) -> None: msg += f"Expected {expected_type}. " msg += f"Got '{value}'. " msg += f"More info: {error}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) def verify_multitype( self, expected_types: List[str], value: Any, param: str @@ -242,12 +265,13 @@ def verify_multitype( """ method_name = inspect.stack()[0][3] invalid = True + error = "" for expected_type in expected_types: if expected_type == "str": try: value = self.validation.check_type_str(value) invalid = False - except TypeError as error: + except TypeError as error: # pylint: disable=unused-variable pass elif expected_type == "bool": try: @@ -326,7 +350,7 @@ def verify_multitype( msg += f"Expected one of {expected_types}. " msg += f"Got '{value}'. " msg += f"More info: {error}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) @property def parameters(self): diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 31c9c346f..0e4214319 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -15,7 +15,7 @@ # limitations under the License. from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __copyright__ = "Copyright (c) 2023 Cisco and/or its affiliates." __author__ = "Allen Robel" __email__ = "arobel@cisco.com" @@ -406,9 +406,9 @@ import json from typing import Any, Dict, List -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ - dcnm_send from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ + ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ @@ -425,12 +425,12 @@ ImageValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ ImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.params_validate import \ - ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ SwitchDetails from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsByIpAddress +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send class ImageUpgradeTask(ImageUpgradeCommon): @@ -810,8 +810,10 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec[section][sub_section]["mode"]["type"] = "str" params_spec[section][sub_section]["mode"]["default"] = "disruptive" params_spec[section][sub_section]["mode"]["choices"] = [ - "disruptive", "non_disruptive", "force_non_disruptive"] - + "disruptive", + "non_disruptive", + "force_non_disruptive", + ] params_spec[section][sub_section]["bios_force"] = {} params_spec[section][sub_section]["bios_force"]["required"] = False @@ -829,8 +831,11 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec[section][sub_section]["module"]["type"] = ["str", "int"] params_spec[section][sub_section]["module"]["default"] = "ALL" params_spec[section][sub_section]["module"]["choices"] = [ - str(x) for x in range(1, 33)] - params_spec[section][sub_section]["module"]["choices"].extend([x for x in range(1,33)]) + str(x) for x in range(1, 33) + ] + params_spec[section][sub_section]["module"]["choices"].extend( + list(range(1, 33)) + ) params_spec[section][sub_section]["module"]["choices"].append("ALL") params_spec[section][sub_section]["golden"] = {} @@ -872,7 +877,6 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: return copy.deepcopy(params_spec) - @staticmethod def _build_params_spec_for_query_state() -> Dict[str, Any]: """ @@ -889,7 +893,6 @@ def _build_params_spec_for_query_state() -> Dict[str, Any]: return copy.deepcopy(params_spec) - def _merge_global_and_switch_configs(self, config) -> None: """ Merge the global config with each switch config and @@ -939,7 +942,6 @@ def _merge_global_and_switch_configs(self, config) -> None: merged_configs.append(switch_config) self.switch_configs = copy.copy(merged_configs) - def _merge_defaults_to_switch_configs(self) -> None: """ For any items in config which are not set, apply the default @@ -953,20 +955,23 @@ def _merge_defaults_to_switch_configs(self) -> None: # we need to rebuild default_config in this loop # because merge_dicts modifies it in place merged_config = self.merge_dicts( - copy.deepcopy(self.defaults), - copy.deepcopy(switch_config)) + copy.deepcopy(self.defaults), copy.deepcopy(switch_config) + ) msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"switch_config: {json.dumps(switch_config, indent=4, sort_keys=True)}" + msg += ( + f"switch_config: {json.dumps(switch_config, indent=4, sort_keys=True)}" + ) self.log_msg(msg) msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"merged_config: {json.dumps(merged_config, indent=4, sort_keys=True)}" + msg += ( + f"merged_config: {json.dumps(merged_config, indent=4, sort_keys=True)}" + ) self.log_msg(msg) merged_configs.append(merged_config) self.switch_configs = copy.copy(merged_configs) - def _validate_switch_configs(self) -> None: """ Verify parameters for each switch @@ -989,7 +994,6 @@ def _validate_switch_configs(self) -> None: validator.parameters = switch validator.validate() - def _build_policy_attach_payload(self) -> None: """ Build the payload for the policy attach request From d661077691cc734f6d9b1f7f39f559e8debc1fa2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 14 Dec 2023 15:42:29 -1000 Subject: [PATCH 142/300] Run thru black/isort --- plugins/module_utils/common/controller_version.py | 13 +++++++------ plugins/module_utils/image_mgmt/image_policies.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 621bb0f28..cbfff2ddb 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -1,12 +1,13 @@ -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( - dcnm_send, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ + ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send class ControllerVersion(ImageUpgradeCommon): diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 08cc73304..46beab06f 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -4,7 +4,7 @@ """ from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name import inspect From 53ee8dae4a0e4ab28bb726d6f29dbfd8a673c8bf Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 15 Dec 2023 07:05:44 -1000 Subject: [PATCH 143/300] isolate self.need from need using copy.copy() --- plugins/modules/dcnm_image_upgrade.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 0e4214319..ea8b91752 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -742,7 +742,7 @@ def get_need_deleted(self) -> None: if self.have.policy != want["policy"]: continue need.append(want) - self.need = need + self.need = copy.copy(need) def get_need_query(self) -> None: """ @@ -758,7 +758,7 @@ def get_need_query(self) -> None: need = [] for want in self.want: need.append(want) - self.need = need + self.need = copy.copy(need) @staticmethod def _build_params_spec_for_merged_state() -> Dict[str, Any]: From 314906fc38618d16d36065a94218f720fb621788 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 15 Dec 2023 07:18:04 -1000 Subject: [PATCH 144/300] rename test file (remove extraneous "image_upgrade") --- ...e_upgrade_task.py => test_image_upgrade_image_upgrade_task.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{test_image_upgrade_image_upgrade_image_upgrade_task.py => test_image_upgrade_image_upgrade_task.py} (100%) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py similarity index 100% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_image_upgrade_task.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py From 280f1b33c9ceb010057832d28d76267bb64d0705 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 15 Dec 2023 10:38:46 -1000 Subject: [PATCH 145/300] Initial set of unit tests for ParamsValidate, more... Also, hardening of ParamsValidate class: ParamsValidate.params_spec - Add basic verifications ParamsValidate.parameters - Add basic verifications ParamsValidate.verify_type() - fix unbound local variable error ParamsValidate.verify_multitype() - no need for var 'error', removed it --- .../module_utils/common/params_validate.py | 80 ++-- .../dcnm_image_upgrade/image_upgrade_utils.py | 10 + .../test_image_upgrade_params_validate.py | 440 ++++++++++++++++++ 3 files changed, 504 insertions(+), 26 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 8c288d66b..70f8a5a7e 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -39,11 +39,9 @@ class ParamsValidate: params_spec["foo"] = {} params_spec["foo"]["required"] = False params_spec["foo"]["type"] = "dict" - params_spec["foo"]["default"] = {} params_spec["foo"]["bar"] = {} params_spec["foo"]["bar"]["required"] = False params_spec["foo"]["bar"]["type"] = "str" - params_spec["foo"]["bar"]["default"] = "bingo" params_spec["foo"]["bar"]["choices"] = ["bingo", "bango", "bongo"] Which describes the following YAML: @@ -74,6 +72,10 @@ def __init__(self, ansible_module): self.reserved_params.add("range_min") self.reserved_params.add("required") self.reserved_params.add("type") + self.mandatory_param_spec_keys = set() + self.mandatory_param_spec_keys.add("required") + self.mandatory_param_spec_keys.add("type") + def log_msg(self, msg): """ @@ -111,6 +113,7 @@ def validate_parameters(self, spec, parameters): """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + for param in spec: if param in self.reserved_params: continue @@ -182,72 +185,83 @@ def verify_type(self, expected_type: str, value: Any, param: str) -> None: self.verify_multitype(expected_type, value, param) return invalid = False + error = "" if expected_type == "str": try: value = self.validation.check_type_str(value) - except TypeError as error: # pylint: disable=unused-variable + except TypeError as err: # pylint: disable=unused-variable invalid = True + error = err elif expected_type == "bool": try: value = self.validation.check_type_bool(value) - except TypeError as error: + except TypeError as err: invalid = True + error = err elif expected_type == "int": try: value = self.validation.check_type_int(value) - except TypeError as error: + except TypeError as err: invalid = True + error = err if expected_type == "float": try: value = self.validation.check_type_float(value) - except TypeError as error: + except TypeError as err: invalid = True + error = err elif expected_type == "dict": # check_type_dict() converts strings with format "k1=v1, k2=v2" # to dict. try: value = self.validation.check_type_dict(value) - except TypeError as error: + except TypeError as err: invalid = True + error = err elif expected_type == "list": # check_type_list() converts int, str, float to a single-element # list. It also converts comma-separated strings to lists. try: value = self.validation.check_type_list(value) - except TypeError as error: + except TypeError as err: invalid = True + error = err elif expected_type == "set": # validate does not have a check_type_set() method if not isinstance(value, set): + invalid = True error = f"Expected type set. Got type {type(value)} for " error += f"param {param} with value {value}." - invalid = True if expected_type == "tuple": # validate does not have a check_type_tuple() method if not isinstance(value, tuple): + invalid = True error = f"Expected type tuple. Got type {type(value)} for " error += f"param {param} with value {value}." - invalid = True if expected_type == "ipv4": try: ipaddress.IPv4Address(value) - except ipaddress.AddressValueError as error: + except ipaddress.AddressValueError as err: invalid = True + error = err if expected_type == "ipv6": try: ipaddress.IPv6Address(value) - except ipaddress.AddressValueError as error: + except ipaddress.AddressValueError as err: invalid = True + error = err if expected_type == "ipv4_subnet": try: ipaddress.IPv4Network(value) - except ValueError as error: + except ValueError as err: invalid = True + error = err if expected_type == "ipv6_subnet": try: ipaddress.IPv6Network(value) - except ValueError as error: + except ValueError as err: invalid = True + error = err if invalid is True: msg = f"{self.class_name}.{method_name}: " @@ -265,61 +279,56 @@ def verify_multitype( """ method_name = inspect.stack()[0][3] invalid = True - error = "" for expected_type in expected_types: if expected_type == "str": try: value = self.validation.check_type_str(value) invalid = False - except TypeError as error: # pylint: disable=unused-variable + except TypeError: pass elif expected_type == "bool": try: value = self.validation.check_type_bool(value) invalid = False - except TypeError as error: + except TypeError: pass elif expected_type == "int": try: value = self.validation.check_type_int(value) invalid = False - except TypeError as error: + except TypeError: pass elif expected_type == "dict": try: value = self.validation.check_type_dict(value) invalid = False - except TypeError as error: + except TypeError: pass elif expected_type == "list": try: value = self.validation.check_type_list(value) invalid = False - except TypeError as error: + except TypeError: pass elif expected_type == "set": # validate does not have a check_type_set() method if isinstance(value, set): invalid = False - error = f"Expected type set. Got type {type(value)} for " - error += f"param {param} with value {value}." elif expected_type == "tuple": # validate does not have a check_type_tuple() method if isinstance(value, tuple): invalid = False - error = f"Expected type tuple. Got type {type(value)} for " - error += f"param {param} with value {value}." elif expected_type == "float": try: value = self.validation.check_type_float(value) invalid = False - except TypeError as error: + except TypeError: pass elif expected_type == "ipv4": try: ipaddress.IPv4Address(value) invalid = False - except ipaddress.AddressValueError as error: + except ipaddress.AddressValueError: pass elif expected_type == "ipv6": try: @@ -362,6 +371,12 @@ def parameters(self): @parameters.setter def parameters(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid parameters. Expected type dict. " + msg += f"Got type {type(value)}." + self.ansible_module.fail_json(msg) self.properties["parameters"] = value @property @@ -373,4 +388,17 @@ def params_spec(self): @params_spec.setter def params_spec(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid params_spec. Expected type dict. " + msg += f"Got type {type(value)}." + self.ansible_module.fail_json(msg) + for param in value: + for key in self.mandatory_param_spec_keys: + if key not in value[param]: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid params_spec. Missing key '{key}' for " + msg += f"param '{param}'." + self.ansible_module.fail_json(msg) self.properties["params_spec"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index c2e83df96..244b1f00a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -25,6 +25,8 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ ControllerVersion +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ + ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ ImagePolicies from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ @@ -151,6 +153,14 @@ def image_validate_fixture(): return ImageValidate(MockAnsibleModule) +@pytest.fixture(name="params_validate") +def params_validate_fixture(): + """ + mock ParamsValidate + """ + return ParamsValidate(MockAnsibleModule) + + @pytest.fixture(name="issu_details_by_device_name") def issu_details_by_device_name_fixture(): """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py new file mode 100644 index 000000000..61bfd27d7 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -0,0 +1,440 @@ +# Copyright (c) 2020-2024 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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + + +""" +ParamsValidate - unit tests +""" + +from __future__ import absolute_import, division, print_function + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ + ParamsValidate + +from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, + image_upgrade_fixture, + issu_details_by_ip_address_fixture, + params_validate_fixture, + payloads_image_upgrade, + responses_image_install_options, + responses_image_upgrade, + responses_switch_issu_details) + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." +PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." + +DCNM_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.dcnm_send" +DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" + + +def test_params_validate_00001(params_validate) -> None: + """ + Function + - __init__ + + Test + - Class attributes are initialized to expected values + """ + with does_not_raise(): + instance = params_validate + assert isinstance(instance, ParamsValidate) + assert isinstance(instance.properties, dict) + assert isinstance(instance.reserved_params, set) + assert instance.reserved_params == { + "choices", + "default", + "range_max", + "range_min", + "required", + "type", + } + assert instance.class_name == "ParamsValidate" + assert instance.debug is False + assert instance.logfile == "/tmp/ansible_dcnm.log" + assert instance.file_handle is None + assert instance.properties.get("parameters", "foo") is None + assert instance.properties.get("params_spec", "foo") is None + + +def test_params_validate_00020(params_validate) -> None: + """ + Function + - params_spec + + Test + - params_spec accepts a valid minimum specification + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + + +def test_params_validate_00021(params_validate) -> None: + """ + Function + - params_spec + + Test + - prams_spec calls fail_json when passed a value that is not a dict + """ + match = "ParamsValidate.params_spec: " + match += "Invalid params_spec. Expected type dict. Got type " + match += r"\\." + + with pytest.raises(AnsibleFailJson, match=match): + instance = params_validate + instance.params_spec = "foo" + + +@pytest.mark.parametrize( + "present_key, present_key_value, missing_key", + [ + ("required", True, "type"), + ("type", "int", "required"), + ], +) +def test_params_validate_00022( + params_validate, present_key, present_key_value, missing_key +) -> None: + """ + Function + - params_spec + + Test + - params_spec calls fail_json when specification is missing a required key + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"][f"{present_key}"] = present_key_value + + match = "ParamsValidate.params_spec: " + match += "Invalid params_spec. " + match += f"Missing key '{missing_key}' for param 'foo'." + + with pytest.raises(AnsibleFailJson, match=match): + instance = params_validate + instance.params_spec = params_spec + + +def test_params_validate_00030(params_validate) -> None: + """ + Function + - parameters + + Test + - parameters accepts a valid dict + """ + with does_not_raise(): + instance = params_validate + instance.parameters = {"foo": "bar"} + + +def test_params_validate_00031(params_validate) -> None: + """ + Function + - parameters + + Test + - parameters calls fail_json when passed a value that is not a dict + """ + match = "ParamsValidate.parameters: " + match += "Invalid parameters. Expected type dict. Got type " + match += r"\\." + + with pytest.raises(AnsibleFailJson, match=match): + instance = params_validate + instance.parameters = [1, 2, 3] + + +def test_params_validate_00050(params_validate) -> None: + """ + Function + - validate + - validate_parameters + - verify_choices + + Test + - happy path for params_spec and parameters + """ + with does_not_raise(): + instance = params_validate + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["choices"] = ["bar", "baz"] + + parameters = {} + parameters["foo"] = "bar" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + instance.validate() + + +def test_params_validate_00051(params_validate) -> None: + """ + Function + - validate + - validate_parameters + + Test + - parameters is missing a required parameter + """ + with does_not_raise(): + instance = params_validate + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["choices"] = ["bar", "baz"] + + parameters = {} + parameters["bar"] = "baz" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate.validate_parameters: " + match += "Playbook is missing mandatory parameter: foo." + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +def test_params_validate_00052(params_validate) -> None: + """ + Function + - validate + - verify_choices + + Test + - parameter is a valid choice + """ + with does_not_raise(): + instance = params_validate + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["choices"] = ["bar", "baz"] + + parameters = {} + parameters["foo"] = "baz" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + instance.validate() + + +def test_params_validate_00053(params_validate) -> None: + """ + Function + - validate + - verify_choices + + Test + - parameter is not a valid choice + """ + with does_not_raise(): + instance = params_validate + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["choices"] = ["bar", "baz"] + + parameters = {} + parameters["foo"] = "bing" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate.verify_choices: " + match += "Invalid value for parameter 'foo'. " + match += r"Expected one of \['bar', 'baz'\]. Got bing" + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +def test_params_validate_00060(params_validate) -> None: + """ + Function + - validate + - verify_type + + Test + - parameter type is invalid + """ + with does_not_raise(): + instance = params_validate + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "int" + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = "bing" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate.verify_type: " + match += "Invalid type for parameter 'foo'. " + match += "Expected int. Got 'bing'. " + match += r"More info: \ cannot be converted to an int" + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +@pytest.mark.parametrize( + "value, type_to_verify", + [ + (1, "int"), + ("1", "int"), + (1.0, "float"), + ("1.0", "float"), + ("foo", "str"), + (1, "str"), + ([1, 2, "3"], "list"), + (1, "list"), + ((1, 2, 3), "tuple"), + ({"foo": "bar"}, "dict"), + ("foo=1, bar=2", "dict"), + ({"foo", "bar"}, "set"), + ("1.1.1.1", "ipv4"), + ("1.1.1.0/24", "ipv4_subnet"), + ("2001:1:1::fe", "ipv6"), + ("2001:1:1::/64", "ipv6_subnet"), + ], +) +def test_params_validate_00061(params_validate, value, type_to_verify) -> None: + """ + Function + - validate + - verify_type + + Test + - parameter type is valid + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + instance.validate() + + +# tests 00110 - 00112 are taken from test_image_upgrade_common.py +# and have the same numbering. These should be moved to a common +# test module when log_msg (or an equivilent) is moved to a +# common module +def test_params_validate_00110(params_validate) -> None: + """ + Function + - log_msg + + Test + - log_msg returns None when debug is False + """ + instance = params_validate + + error_message = "This is an error message" + instance.debug = False + assert instance.log_msg(error_message) is None + + +def test_params_validate_00111(tmp_path, params_validate) -> None: + """ + Function + - log_msg + + Test + - log_msg writes to the logfile when debug is True + """ + instance = params_validate + + directory = tmp_path / "test_log_msg" + directory.mkdir() + filename = directory / "test_log_msg.txt" + + error_message = "This is an error message" + instance.debug = True + instance.logfile = filename + instance.log_msg(error_message) + + assert filename.read_text(encoding="UTF-8") == error_message + "\n" + assert len(list(tmp_path.iterdir())) == 1 + + +def test_params_validate_00112(tmp_path, params_validate) -> None: + """ + Function + - log_msg + + Test + - log_msg calls fail_json if the logfile cannot be opened + + Description + To ensure an error is generated, we attempt a write to a filename + that is too long for the target OS. + """ + instance = params_validate + + directory = tmp_path / "test_log_msg" + directory.mkdir() + filename = directory / f"test_{'a' * 2000}_log_msg.txt" + + error_message = "This is an error message" + instance.debug = True + instance.logfile = filename + with pytest.raises(AnsibleFailJson, match=r"error opening logfile"): + instance.log_msg(error_message) From cfce0b77c6a91c73c56b160944e0f9fc056d2847 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 15 Dec 2023 10:44:49 -1000 Subject: [PATCH 146/300] add assert for mandatory_param_spec_keys --- .../dcnm_image_upgrade/test_image_upgrade_params_validate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index 61bfd27d7..0c4fc4b38 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -77,6 +77,7 @@ def test_params_validate_00001(params_validate) -> None: "required", "type", } + assert instance.mandatory_param_spec_keys == {"required", "type"} assert instance.class_name == "ParamsValidate" assert instance.debug is False assert instance.logfile == "/tmp/ansible_dcnm.log" From 3b8bab1be980ddd79f831df72eb766933b5e0b5b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 15 Dec 2023 13:24:31 -1000 Subject: [PATCH 147/300] Update or add copyright header, more... Also: dcnm_image_upgrade/fixture.py - change "f" to "file_handle" to appease pylint. All unit tests: - Run thru black / isort / pylint linters --- .../module_utils/image_mgmt/api_endpoints.py | 15 + .../module_utils/image_mgmt/image_policies.py | 15 + .../image_mgmt/image_policy_action.py | 15 + .../module_utils/image_mgmt/image_stage.py | 15 + .../module_utils/image_mgmt/image_upgrade.py | 15 + .../image_mgmt/image_upgrade_common.py | 15 + .../module_utils/image_mgmt/image_validate.py | 18 + .../image_mgmt/install_options.py | 15 + .../module_utils/image_mgmt/switch_details.py | 15 + .../image_mgmt/switch_issu_details.py | 15 + plugins/modules/dcnm_image_upgrade.py | 4 +- .../dcnm/dcnm_image_upgrade/fixture.py | 6 +- .../dcnm_image_upgrade/image_upgrade_utils.py | 2 +- .../test_image_upgrade_api_endpoints.py | 2 +- .../test_image_upgrade_controller_version.py | 2 +- ...est_image_upgrade_image_install_options.py | 2 +- .../test_image_upgrade_image_policies.py | 2 +- .../test_image_upgrade_image_policy_action.py | 2 +- .../test_image_upgrade_image_stage.py | 2 +- .../test_image_upgrade_image_upgrade.py | 357 ++++-------------- ...test_image_upgrade_image_upgrade_common.py | 6 +- .../test_image_upgrade_image_upgrade_task.py | 160 ++++---- .../test_image_upgrade_image_validate.py | 2 +- .../test_image_upgrade_params_validate.py | 2 +- .../test_image_upgrade_switch_details.py | 2 +- ...rade_switch_issu_details_by_device_name.py | 2 +- ...grade_switch_issu_details_by_ip_address.py | 2 +- ...de_switch_issu_details_by_serial_number.py | 2 +- 28 files changed, 338 insertions(+), 374 deletions(-) diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py index 0cd3d9fd1..eb3d04185 100644 --- a/plugins/module_utils/image_mgmt/api_endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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. + """ Endpoints for image management API calls """ diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 46beab06f..af3ef3d50 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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. + """ Retrieve image policy details from the controller and provide property accessors for the policy attributes. diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 80ba2f81f..3b230020b 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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. + """ Perform image policy actions on the controller for one or more switches. """ diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 028f646c5..23d322e43 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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. + """ ImageStage - Methods to stage images to NX-OS switches """ diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index cf56a90c6..1cd372f5f 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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. + """ ImageUpgrade - Methods to upgrade images on NX-OS switches """ diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 43d03b522..9fac8aa76 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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. + """ Base class for the other image upgrade classes """ diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 07421746e..70f87ba47 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -1,3 +1,21 @@ +# +# Copyright (c) 2024 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. + +""" +Validate images +""" from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index dd89343d2..ef126a029 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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 diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index 2e7d19b75..d916f4071 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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 diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 9385e60af..70b6a9e75 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 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 diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index ea8b91752..e3f3e400e 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -# Copyright (c) 2023 Cisco and/or its affiliates. +# Copyright (c) 2024 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. @@ -16,7 +16,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type # pylint: disable=invalid-name -__copyright__ = "Copyright (c) 2023 Cisco and/or its affiliates." +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" __email__ = "arobel@cisco.com" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py index eae173520..25396ebce 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2022 Cisco and/or its affiliates. +# Copyright (c) 2024 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. @@ -34,8 +34,8 @@ def load_fixture(filename): path = os.path.join(fixture_path, f"{filename}.json") try: - with open(path, encoding="utf-8") as f: - data = f.read() + with open(path, encoding="utf-8") as file_handle: + data = file_handle.read() except IOError as exception: msg = f"Exception opening test input file {filename}.json : " msg += f"Exception detail: {exception}" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index 244b1f00a..9c3447307 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py index 7e4031f1c..c0f03a7fa 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py index 8174e3d40..2d247e655 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index bc54ea658..73162996a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index c86e8e5fa..d26a60d20 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index c157faf0e..43d8c379d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 959cdf28a..92a18a2a2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index ae058de47..0720be3b6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. @@ -221,18 +221,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": "FOO", "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": True - }, - "package": { - "install": False, - "uninstall": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": True}, + "package": {"install": False, "uninstall": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -311,22 +302,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": False, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": True - }, - "package": { - "install": True, - "uninstall": False - }, - "epld": { - "module": 27, - "golden": True - }, - "reboot": { - "config_reload": True, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": True}, + "package": {"install": True, "uninstall": False}, + "epld": {"module": 27, "golden": True}, + "reboot": {"config_reload": True, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -401,22 +380,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": True, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": True, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -490,22 +457,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "FOO", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "FOO", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -585,22 +540,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "non_disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "non_disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -678,22 +621,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "force_non_disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "force_non_disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -768,22 +699,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": "FOO" - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": "FOO"}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -858,22 +777,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": True - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": True}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -950,22 +857,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "FOO", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "FOO", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1040,22 +935,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": "FOO" - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": "FOO"}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1129,22 +1012,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1219,22 +1090,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": "FOO", - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": "FOO", "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1309,22 +1168,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": "FOO" - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": "FOO"}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1404,22 +1251,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": "FOO" - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": "FOO"}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1495,22 +1330,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1685,22 +1508,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": True, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": False - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": False}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1770,22 +1581,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": False, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": True - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": True}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", @@ -1855,22 +1654,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "stage": True, "upgrade": {"nxos": False, "epld": True}, "options": { - "nxos": { - "mode": "disruptive", - "bios_force": True - }, - "package": { - "install": False, - "uninstall": False - }, - "epld": { - "module": "ALL", - "golden": False - }, - "reboot": { - "config_reload": False, - "write_erase": False - } + "nxos": {"mode": "disruptive", "bios_force": True}, + "package": {"install": False, "uninstall": False}, + "epld": {"module": "ALL", "golden": False}, + "reboot": {"config_reload": False, "write_erase": False}, }, "validate": True, "ip_address": "172.22.150.102", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 4c508ee6f..fbc323982 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. @@ -242,7 +242,7 @@ def test_image_mgmt_image_upgrade_common_00060(image_upgrade_common) -> None: data = responses_image_upgrade_common("test_image_mgmt_image_upgrade_common_00060a") with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): - instance._handle_response( + instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") ) # pylint: disable=protected-access @@ -283,7 +283,7 @@ def test_image_mgmt_image_upgrade_common_00070( data = responses_image_upgrade_common(key) with does_not_raise(): - result = instance._handle_get_response( + result = instance._handle_get_response( # pylint: disable=protected-access data.get("response") ) # pylint: disable=protected-access diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 6ff097e03..b95ec2873 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. @@ -380,7 +380,9 @@ def test_image_mgmt_upgrade_task_00040(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False}] + switch_configs = [ + {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False @@ -415,12 +417,14 @@ def test_image_mgmt_upgrade_task_00041(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "upgrade": {"nxos": False}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "upgrade": {"nxos": False}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -456,12 +460,14 @@ def test_image_mgmt_upgrade_task_00042(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "upgrade": {"epld": True}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "upgrade": {"epld": True}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -500,12 +506,14 @@ def test_image_mgmt_upgrade_task_00043(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -545,12 +553,14 @@ def test_image_mgmt_upgrade_task_00044(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"nxos": {"mode": "non_disruptive"}}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"nxos": {"mode": "non_disruptive"}}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -590,12 +600,14 @@ def test_image_mgmt_upgrade_task_00045(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"nxos": {"bios_force": True}}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"nxos": {"bios_force": True}}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -635,12 +647,14 @@ def test_image_mgmt_upgrade_task_00046(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"epld": {"module": 27}}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"epld": {"module": 27}}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -680,12 +694,14 @@ def test_image_mgmt_upgrade_task_00047(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"epld": {"golden": True}}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"epld": {"golden": True}}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -726,12 +742,14 @@ def test_image_mgmt_upgrade_task_00048(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"reboot": {"config_reload": True}}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"reboot": {"config_reload": True}}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -772,12 +790,14 @@ def test_image_mgmt_upgrade_task_00049(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"reboot": {"write_erase": True}}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"reboot": {"write_erase": True}}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -818,12 +838,14 @@ def test_image_mgmt_upgrade_task_00050(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"package": {"install": True}}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"package": {"install": True}}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() @@ -864,12 +886,14 @@ def test_image_mgmt_upgrade_task_00051(image_upgrade_task) -> None: """ instance = image_upgrade_task - switch_configs = [{ - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"package": {"uninstall": True}}, - }] + switch_configs = [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": False, + "options": {"package": {"uninstall": True}}, + } + ] instance.switch_configs = switch_configs instance._merge_defaults_to_switch_configs() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index fc0ddebcc..bf8c92130 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index 0c4fc4b38..db824744f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 818de7949..70a145cd8 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index 7e98190a6..11c76a41a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index de2ff2bdf..110a3ac27 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 4449f224d..9e5704e7e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# Copyright (c) 2024 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. From 821c3540d9fa1e2baa99b15736cde26556fdd01c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 15 Dec 2023 15:15:46 -1000 Subject: [PATCH 148/300] Align fail_json messages to suggest similar debugging steps. --- plugins/module_utils/image_mgmt/image_upgrade.py | 8 ++++++-- .../test_image_upgrade_image_upgrade.py | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 1cd372f5f..a685ce9aa 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -578,7 +578,9 @@ def _wait_for_image_upgrade_to_complete(self): msg += f"{device_name}, {serial_number}, {ip_address}, " msg += f"upgrade_percent {upgrade_percent}. " msg += "Check the controller to determine the cause. " - msg += "Operations > Image Management > Devices > View Details." + msg += "Operations > Image Management > Devices > View Details. " + msg += "And/or check the devices " + msg += "(e.g. show install all status)." self.module.fail_json(msg) if upgrade_status == "Success": @@ -588,7 +590,9 @@ def _wait_for_image_upgrade_to_complete(self): msg = f"{self.class_name}.{method_name}: " msg += "The following device(s) did not complete upgrade: " msg += f"{sorted(self.ipv4_todo.difference(self.ipv4_done))}. " - msg += "Check the device(s) to determine the cause " + msg += "Check the controller to determine the cause. " + msg += "Operations > Image Management > Devices > View Details. " + msg += "And/or check the device(s) " msg += "(e.g. show install all status)." self.module.fail_json(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 0720be3b6..5865bd917 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -2223,7 +2223,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match = "ImageUpgrade._wait_for_image_upgrade_to_complete: " match += r"The following device\(s\) did not complete upgrade: " match += r"\['172\.22\.150\.108'\]. " - match += r"Check the device\(s\) to determine the cause " + match += "Check the controller to determine the cause. " + match += "Operations > Image Management > Devices > View Details. " + match += r"And/or check the device\(s\) " match += r"\(e\.g\. show install all status\)\." with pytest.raises(AnsibleFailJson, match=match): instance._wait_for_image_upgrade_to_complete() From b052ca9fef9d7488191fb3b3df14ed16dce7e647 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 15 Dec 2023 19:15:28 -1000 Subject: [PATCH 149/300] Add unit tests for multitype and integer range, more... Run through black/isort/pylint and address some pylint issues. --- .../module_utils/common/params_validate.py | 364 ++++++++++++------ .../test_image_upgrade_params_validate.py | 186 ++++++++- 2 files changed, 435 insertions(+), 115 deletions(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 70f8a5a7e..d31ff6fbe 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -3,7 +3,7 @@ """ from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name import inspect import ipaddress @@ -76,7 +76,6 @@ def __init__(self, ansible_module): self.mandatory_param_spec_keys.add("required") self.mandatory_param_spec_keys.add("type") - def log_msg(self, msg): """ used for debugging. disable this when committing to main @@ -105,21 +104,20 @@ def validate(self) -> None: Verify that parameters in self.parameters conform to self.params_spec """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.validate_parameters(self.params_spec, self.parameters) + self._validate_parameters(self.params_spec, self.parameters) - def validate_parameters(self, spec, parameters): + def _validate_parameters(self, spec, parameters): """ Recursively traverse parameters and verify conformity with spec """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - for param in spec: if param in self.reserved_params: continue if isinstance(spec[param], Map): - self.validate_parameters(spec[param], parameters.get(param, {})) + self._validate_parameters(spec[param], parameters.get(param, {})) # We shouldn't hit this since defaults are merged for all # missing parameters, but just in case... @@ -131,23 +129,40 @@ def validate_parameters(self, spec, parameters): msg += f"Playbook is missing mandatory parameter: {param}." self.ansible_module.fail_json(msg) - self.verify_type(spec[param]["type"], parameters[param], param) - self.verify_choices( + self.log_msg(f"parameters[param] PRE: {parameters[param]}") + parameters[param] = self._verify_type( + spec[param]["type"], parameters[param], param + ) + self.log_msg(f"parameters[param] POST: {parameters[param]}") + + self._verify_choices( spec[param].get("choices", None), parameters[param], param ) + + if spec[param].get("type", None) != "int" and ( + spec[param].get("range_min", None) is not None + or spec[param].get("range_max", None) is not None + ): + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid param_spec for parameter '{param}'. " + msg += "range_min and range_max are only valid for " + msg += "parameters of type int. " + msg += f"Got type {spec[param]['type']} for param {param}." + self.ansible_module.fail_json(msg) + if ( spec[param].get("type", None) == "int" and spec[param].get("range_min", None) is not None and spec[param].get("range_max", None) is not None ): - self.verify_integer_range( + self._verify_integer_range( spec[param].get("range_min", None), spec[param].get("range_max", None), parameters[param], param, ) - def verify_choices(self, choices: List[Any], value: Any, param: str) -> None: + def _verify_choices(self, choices: List[Any], value: Any, param: str) -> None: """ Verify that value is one of the choices """ @@ -162,13 +177,14 @@ def verify_choices(self, choices: List[Any], value: Any, param: str) -> None: msg += f"Got {value}" self.ansible_module.fail_json(msg) - def verify_integer_range( + def _verify_integer_range( self, range_min: int, range_max: int, value: int, param: str ) -> None: """ Verify that value is within the range range_min to range_max """ method_name = inspect.stack()[0][3] + if value < range_min or value > range_max: msg = f"{self.class_name}.{method_name}: " msg += f"Invalid value for parameter '{param}'. " @@ -176,109 +192,235 @@ def verify_integer_range( msg += f"Got {value}" self.ansible_module.fail_json(msg) - def verify_type(self, expected_type: str, value: Any, param: str) -> None: + def _verify_type(self, expected_type: str, value: Any, param: str) -> Any: """ Verify that value's type matches the expected type """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable if isinstance(expected_type, list): - self.verify_multitype(expected_type, value, param) - return - invalid = False - error = "" - if expected_type == "str": - try: - value = self.validation.check_type_str(value) - except TypeError as err: # pylint: disable=unused-variable - invalid = True - error = err - elif expected_type == "bool": - try: - value = self.validation.check_type_bool(value) - except TypeError as err: - invalid = True - error = err - elif expected_type == "int": - try: - value = self.validation.check_type_int(value) - except TypeError as err: - invalid = True - error = err - if expected_type == "float": - try: - value = self.validation.check_type_float(value) - except TypeError as err: - invalid = True - error = err - elif expected_type == "dict": - # check_type_dict() converts strings with format "k1=v1, k2=v2" - # to dict. - try: - value = self.validation.check_type_dict(value) - except TypeError as err: - invalid = True - error = err - elif expected_type == "list": - # check_type_list() converts int, str, float to a single-element - # list. It also converts comma-separated strings to lists. - try: - value = self.validation.check_type_list(value) - except TypeError as err: - invalid = True - error = err - elif expected_type == "set": - # validate does not have a check_type_set() method - if not isinstance(value, set): - invalid = True - error = f"Expected type set. Got type {type(value)} for " - error += f"param {param} with value {value}." - if expected_type == "tuple": - # validate does not have a check_type_tuple() method - if not isinstance(value, tuple): - invalid = True - error = f"Expected type tuple. Got type {type(value)} for " - error += f"param {param} with value {value}." - if expected_type == "ipv4": - try: - ipaddress.IPv4Address(value) - except ipaddress.AddressValueError as err: - invalid = True - error = err - if expected_type == "ipv6": - try: - ipaddress.IPv6Address(value) - except ipaddress.AddressValueError as err: - invalid = True - error = err - if expected_type == "ipv4_subnet": - try: - ipaddress.IPv4Network(value) - except ValueError as err: - invalid = True - error = err - if expected_type == "ipv6_subnet": - try: - ipaddress.IPv6Network(value) - except ValueError as err: - invalid = True - error = err + value = self._verify_multitype(expected_type, value, param) + return value + value = self._verify_str(expected_type, value, param) + value = self._verify_bool(expected_type, value, param) + value = self._verify_int(expected_type, value, param) + value = self._verify_float(expected_type, value, param) + value = self._verify_dict(expected_type, value, param) + value = self._verify_list(expected_type, value, param) + value = self._verify_set(expected_type, value, param) + value = self._verify_tuple(expected_type, value, param) + value = self._verify_ipv4(expected_type, value, param) + value = self._verify_ipv6(expected_type, value, param) + value = self._verify_ipv4_subnet(expected_type, value, param) + value = self._verify_ipv6_subnet(expected_type, value, param) + return value + + def _verify_str(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is a str, or convert to str if possible + If value is not a str, and conversion fails, + call invalid_type() to fail the playbook + """ + if expected_type != "str": + return value + try: + return self.validation.check_type_str(value) + except TypeError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_bool(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is a bool, or convert to bool if possible + If value is not a bool, and conversion fails, + call invalid_type() to fail the playbook + """ + if expected_type != "bool": + return value + try: + return self.validation.check_type_bool(value) + except TypeError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_int(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is an int, or convert to int if possible + If value is not an int, and conversion fails, + call invalid_type() to fail the playbook + """ + if expected_type != "int": + return value + try: + return self.validation.check_type_int(value) + except TypeError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_float(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is a float, or convert to float if possible + If value is not a float, and conversion fails, + call invalid_type() to fail the playbook + """ + if expected_type != "float": + return value + try: + return self.validation.check_type_float(value) + except TypeError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_dict(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is a dict + check_type_dict() also converts strings with format + "k1=v1, k2=v2" to dict. - if invalid is True: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid type for parameter '{param}'. " - msg += f"Expected {expected_type}. " - msg += f"Got '{value}'. " - msg += f"More info: {error}" - self.ansible_module.fail_json(msg) + If value is not a dict, and conversion fails, + call invalid_type() to fail the playbook + """ + if expected_type != "dict": + return value + try: + return self.validation.check_type_dict(value) + except TypeError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_list(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is a list + check_type_list() converts int, str, float to a single-element list. + It also converts comma-separated strings to lists. - def verify_multitype( - self, expected_types: List[str], value: Any, param: str + If value is not a list, and conversion fails, + call invalid_type() to fail the playbook + """ + if expected_type != "list": + return value + try: + return self.validation.check_type_list(value) + except TypeError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_set(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is a set + validate does not have a check_type_set() method so + we use isinstance() instead. + + If value is not a set, call invalid_type() to fail the playbook + """ + if expected_type != "set": + return value + if isinstance(value, set): + return value + error = f"Expected type set. Got type {type(value)} for " + error += f"param {param} with value {value}." + self.invalid_type(expected_type, value, param, error) + return value # to make pylint happy + + def _verify_tuple(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is a tuple + validate does not have a check_type_tuple() method so + we use isinstance() instead. + + If value is not a tuple, call invalid_type() to fail the playbook + """ + if expected_type != "tuple": + return value + if isinstance(value, tuple): + return value + error = f"Expected type tuple. Got type {type(value)} for " + error += f"param {param} with value {value}." + self.invalid_type(expected_type, value, param, error) + return value # to make pylint happy + + def _verify_ipv4(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is an IPv4 address + If value is not an IPv4 address, call invalid_type() + to fail the playbook + """ + if expected_type != "ipv4": + return value + try: + _ = ipaddress.IPv4Address(value) + return value + except ipaddress.AddressValueError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_ipv6(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is an IPv6 address + If value is not an IPv6 address, call invalid_type() + to fail the playbook + """ + if expected_type != "ipv6": + return value + try: + _ = ipaddress.IPv6Address(value) + return value + except ipaddress.AddressValueError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_ipv4_subnet(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is an IPv4 subnet + If value is not an IPv4 subnet, call invalid_type() + to fail the playbook + """ + if expected_type != "ipv4_subnet": + return value + try: + _ = ipaddress.IPv4Network(value) + return value + except ValueError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def _verify_ipv6_subnet(self, expected_type: str, value: Any, param: str) -> Any: + """ + verify that value is an IPv6 subnet + If value is not an IPv6 subnet, call invalid_type() + to fail the playbook + """ + if expected_type != "ipv6_subnet": + return value + try: + _ = ipaddress.IPv6Network(value) + return value + except ValueError as err: + self.invalid_type(expected_type, value, param, err) + return value # to make pylint happy + + def invalid_type( + self, expected_type: str, value: Any, param: str, error: str ) -> None: + """ + Calls fail_json when value's type does not match expected_type + """ + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid type for parameter '{param}'. " + msg += f"Expected {expected_type}. " + msg += f"Got '{value}'. " + msg += f"More info: {error}" + self.ansible_module.fail_json(msg) + + def _verify_multitype( + self, expected_types: List[str], value: Any, param: str + ) -> Any: """ Verify that value's type matches one of the types in expected_types """ method_name = inspect.stack()[0][3] invalid = True + error = "" for expected_type in expected_types: if expected_type == "str": try: @@ -353,13 +495,15 @@ def verify_multitype( error += f"with value {value}." invalid = True - if invalid is True: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid type for parameter '{param}'. " - msg += f"Expected one of {expected_types}. " - msg += f"Got '{value}'. " + if invalid is False: + return value + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid type for parameter '{param}'. " + msg += f"Expected one of {expected_types}. " + msg += f"Got '{value}'." + if error: msg += f"More info: {error}" - self.ansible_module.fail_json(msg) + self.ansible_module.fail_json(msg) @property def parameters(self): @@ -374,7 +518,7 @@ def parameters(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid parameters. Expected type dict. " + msg += "Invalid parameters. Expected type dict. " msg += f"Got type {type(value)}." self.ansible_module.fail_json(msg) self.properties["parameters"] = value @@ -391,7 +535,7 @@ def params_spec(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid params_spec. Expected type dict. " + msg += "Invalid params_spec. Expected type dict. " msg += f"Got type {type(value)}." self.ansible_module.fail_json(msg) for param in value: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index db824744f..0ae08d66d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -232,7 +232,7 @@ def test_params_validate_00051(params_validate) -> None: instance.params_spec = params_spec instance.parameters = parameters - match = "ParamsValidate.validate_parameters: " + match = "ParamsValidate._validate_parameters: " match += "Playbook is missing mandatory parameter: foo." with pytest.raises(AnsibleFailJson, match=match): @@ -289,7 +289,7 @@ def test_params_validate_00053(params_validate) -> None: instance.params_spec = params_spec instance.parameters = parameters - match = "ParamsValidate.verify_choices: " + match = "ParamsValidate._verify_choices: " match += "Invalid value for parameter 'foo'. " match += r"Expected one of \['bar', 'baz'\]. Got bing" @@ -306,8 +306,6 @@ def test_params_validate_00060(params_validate) -> None: Test - parameter type is invalid """ - with does_not_raise(): - instance = params_validate params_spec = {} params_spec["foo"] = {} params_spec["foo"]["type"] = "int" @@ -317,10 +315,11 @@ def test_params_validate_00060(params_validate) -> None: parameters["foo"] = "bing" with does_not_raise(): + instance = params_validate instance.params_spec = params_spec instance.parameters = parameters - match = "ParamsValidate.verify_type: " + match = "ParamsValidate.invalid_type: " match += "Invalid type for parameter 'foo'. " match += "Expected int. Got 'bing'. " match += r"More info: \ cannot be converted to an int" @@ -373,6 +372,183 @@ def test_params_validate_00061(params_validate, value, type_to_verify) -> None: instance.parameters = parameters instance.validate() +@pytest.mark.parametrize( + "value, type_to_verify", + [ + (1, ["int", "str"]), + ("1", ["dict", "ipv4"]), + ], +) +def test_params_validate_00062(params_validate, value, type_to_verify) -> None: + """ + Function + - validate + - verify_type + - verify_multitype + + Test + - parameter type is valid + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + instance.validate() + + +@pytest.mark.parametrize( + "value, type_to_verify", + [ + ("1", ["dict", "ipv4"]), + ], +) +def test_params_validate_00062(params_validate, value, type_to_verify) -> None: + """ + Function + - validate + - verify_type + - verify_multitype + + Test + - parameter type is invalid + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_multitype: " + match += "Invalid type for parameter 'foo'. " + match += r"Expected one of \['dict', 'ipv4'\]. " + match += f"Got '{value}'." + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +@pytest.mark.parametrize( + "value", + [ + (1), + (5), + (10), + ], +) +def test_params_validate_00070(params_validate, value) -> None: + """ + Function + - validate + - verify_integer_range + + Test + - parameter (int) is within range_min and range_max + """ + with does_not_raise(): + instance = params_validate + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "int" + params_spec["foo"]["required"] = True + params_spec["foo"]["range_min"] = 1 + params_spec["foo"]["range_max"] = 10 + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + instance.validate() + + +@pytest.mark.parametrize( + "value", + [ + (-1), + (0), + (11), + ], +) +def test_params_validate_00071(params_validate, value) -> None: + """ + Function + - validate + - verify_choices + + Test + - parameter (int) is outside range_min and range_max + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "int" + params_spec["foo"]["required"] = True + params_spec["foo"]["range_min"] = 1 + params_spec["foo"]["range_max"] = 10 + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_integer_range: " + match += "Invalid value for parameter 'foo'. " + match += f"Expected value between 1 and 10. Got {value}" + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +def test_params_validate_00072(params_validate) -> None: + """ + Function + - validate + - verify_choices + + Test + - Negative: non-int parameter with range_min and range_max specified. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["range_min"] = 1 + params_spec["foo"]["range_max"] = 10 + + parameters = {} + parameters["foo"] = "bar" + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._validate_parameters: " + match += "Invalid param_spec for parameter 'foo'. " + match += "range_min and range_max are only valid for " + match += "parameters of type int. Got type str for param foo." + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + # tests 00110 - 00112 are taken from test_image_upgrade_common.py # and have the same numbering. These should be moved to a common From 60952fe176f576c3d9cab3aac2326988ec4c7ab3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 15 Dec 2023 19:21:45 -1000 Subject: [PATCH 150/300] Fix assert expectation After modifying ParamsValidate to leverage validate's conversions, the assert needed to check for int 1 rather than str "1" --- .../dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index b95ec2873..fa1bc4960 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -278,7 +278,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert switch_2.get("ip_address") == "2.2.2.2" assert switch_2.get("options").get("epld").get("golden") is True - assert switch_2.get("options").get("epld").get("module") == "1" + assert switch_2.get("options").get("epld").get("module") == 1 assert switch_2.get("options").get("nxos").get("bios_force") is True assert switch_2.get("options").get("nxos").get("mode") == "non_disruptive" assert switch_2.get("options").get("package").get("install") is True From a8d366b16c7972de4cc9fa4ba0b8bdb6a6d7458c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 17 Dec 2023 09:59:42 -1000 Subject: [PATCH 151/300] Add required 'preferred_type' param for multi-type, more... 1. params_validate.py - If 'type' param is a list of types (i.e. multi-type), require that the caller has set 'preferred_type' to indicate what type they prefer param to be. We then do a best-effort attempt to conver param to that type. If this conversion fails, we call _verify_multitype() so that param will be converted to whatever type succeeds first. If no types succeed, then we fail_json. 2. params_validate.py - Added function ipaddress_guard() to guard against the ipaddress module doing undesired conversions of int and bool types. 3. dcnm_image_upgrade.py - added 'preferred_type' to params_spec, per addtions in item 1 above. 4. modified some unit-tests and fixtures, per additions in item 1 above. --- .../module_utils/common/params_validate.py | 128 ++++++++++++++---- plugins/modules/dcnm_image_upgrade.py | 1 + .../image_upgrade_playbook_configs.json | 2 +- .../test_image_upgrade_image_upgrade_task.py | 2 +- .../test_image_upgrade_params_validate.py | 112 ++++++++++++--- 5 files changed, 192 insertions(+), 53 deletions(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index d31ff6fbe..545a84c76 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -72,9 +72,20 @@ def __init__(self, ansible_module): self.reserved_params.add("range_min") self.reserved_params.add("required") self.reserved_params.add("type") + self.reserved_params.add("preferred_type") self.mandatory_param_spec_keys = set() self.mandatory_param_spec_keys.add("required") self.mandatory_param_spec_keys.add("type") + self.types = dict() + self.types["str"] = str + self.types['int'] = int + self.types['float'] = float + self.types['bool'] = bool + self.types['dict'] = dict + self.types['list'] = list + self.types['set'] = set + self.types['tuple'] = tuple + def log_msg(self, msg): """ @@ -129,11 +140,10 @@ def _validate_parameters(self, spec, parameters): msg += f"Playbook is missing mandatory parameter: {param}." self.ansible_module.fail_json(msg) - self.log_msg(f"parameters[param] PRE: {parameters[param]}") - parameters[param] = self._verify_type( - spec[param]["type"], parameters[param], param - ) - self.log_msg(f"parameters[param] POST: {parameters[param]}") + if isinstance(spec[param]["type"], list): + parameters[param] = self._verify_multitype(spec[param], parameters, param) + else: + parameters[param] = self._verify_type(spec[param]["type"], parameters, param) self._verify_choices( spec[param].get("choices", None), parameters[param], param @@ -192,14 +202,13 @@ def _verify_integer_range( msg += f"Got {value}" self.ansible_module.fail_json(msg) - def _verify_type(self, expected_type: str, value: Any, param: str) -> Any: + def _verify_type(self, expected_type: str, params: Any, param: str) -> Any: """ Verify that value's type matches the expected type """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if isinstance(expected_type, list): - value = self._verify_multitype(expected_type, value, param) - return value + + value = params[param] value = self._verify_str(expected_type, value, param) value = self._verify_bool(expected_type, value, param) value = self._verify_int(expected_type, value, param) @@ -226,7 +235,7 @@ def _verify_str(self, expected_type: str, value: Any, param: str) -> Any: return self.validation.check_type_str(value) except TypeError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_bool(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -240,7 +249,7 @@ def _verify_bool(self, expected_type: str, value: Any, param: str) -> Any: return self.validation.check_type_bool(value) except TypeError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_int(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -254,7 +263,7 @@ def _verify_int(self, expected_type: str, value: Any, param: str) -> Any: return self.validation.check_type_int(value) except TypeError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_float(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -268,7 +277,7 @@ def _verify_float(self, expected_type: str, value: Any, param: str) -> Any: return self.validation.check_type_float(value) except TypeError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_dict(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -285,7 +294,7 @@ def _verify_dict(self, expected_type: str, value: Any, param: str) -> Any: return self.validation.check_type_dict(value) except TypeError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_list(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -302,7 +311,7 @@ def _verify_list(self, expected_type: str, value: Any, param: str) -> Any: return self.validation.check_type_list(value) except TypeError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_set(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -319,7 +328,7 @@ def _verify_set(self, expected_type: str, value: Any, param: str) -> Any: error = f"Expected type set. Got type {type(value)} for " error += f"param {param} with value {value}." self.invalid_type(expected_type, value, param, error) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_tuple(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -336,7 +345,7 @@ def _verify_tuple(self, expected_type: str, value: Any, param: str) -> Any: error = f"Expected type tuple. Got type {type(value)} for " error += f"param {param} with value {value}." self.invalid_type(expected_type, value, param, error) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_ipv4(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -346,12 +355,13 @@ def _verify_ipv4(self, expected_type: str, value: Any, param: str) -> Any: """ if expected_type != "ipv4": return value + self.ipaddress_guard(expected_type, value, param) try: _ = ipaddress.IPv4Address(value) return value except ipaddress.AddressValueError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_ipv6(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -361,12 +371,13 @@ def _verify_ipv6(self, expected_type: str, value: Any, param: str) -> Any: """ if expected_type != "ipv6": return value + self.ipaddress_guard(expected_type, value, param) try: _ = ipaddress.IPv6Address(value) return value except ipaddress.AddressValueError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_ipv4_subnet(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -376,12 +387,13 @@ def _verify_ipv4_subnet(self, expected_type: str, value: Any, param: str) -> Any """ if expected_type != "ipv4_subnet": return value + self.ipaddress_guard(expected_type, value, param) try: _ = ipaddress.IPv4Network(value) return value except ValueError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy def _verify_ipv6_subnet(self, expected_type: str, value: Any, param: str) -> Any: """ @@ -391,12 +403,33 @@ def _verify_ipv6_subnet(self, expected_type: str, value: Any, param: str) -> Any """ if expected_type != "ipv6_subnet": return value + self.ipaddress_guard(expected_type, value, param) try: _ = ipaddress.IPv6Network(value) return value except ValueError as err: self.invalid_type(expected_type, value, param, err) - return value # to make pylint happy + return value # never reached, but it makes pylint happy + + def ipaddress_guard(self, expected_type, value: Any, param: str) -> None: + """ + Guard against int and bool types for ipv4, ipv6, ipv4_subnet, + and ipv6_subnet type. + + The ipaddress module accepts int and bool types and converts + them to IP addresses or networks. E.g. True becomes 0.0.0.1, + False becomes 0.0.0.0, 1 becomse 0.0.0.1, etc. Because of + this, we need to fail int and bool values if expected_type is + one of ipv4, ipv6, ipv4_subnet, or ipv6_subnet. + """ + if isinstance(value, int): + error = f"Expected type ipv4. Got type {type(value)} for " + error += f"param {param} with value {value}." + self.invalid_type(expected_type, value, param, error) + if isinstance(value, bool): + error = f"Expected type ipv4. Got type {type(value)} for " + error += f"param {param} with value {value}." + self.invalid_type(expected_type, value, param, error) def invalid_type( self, expected_type: str, value: Any, param: str, error: str @@ -413,14 +446,35 @@ def invalid_type( self.ansible_module.fail_json(msg) def _verify_multitype( - self, expected_types: List[str], value: Any, param: str + self, spec: Any, params: Any, param: str ) -> Any: """ Verify that value's type matches one of the types in expected_types """ method_name = inspect.stack()[0][3] + + # preferred_type is mandatory for multitype + if spec.get("preferred_type", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid param_spec for parameter '{param}'. " + msg += "If type is a list, preferred_type must be specified." + self.ansible_module.fail_json(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += f"spec: {spec}" + self.log_msg(msg) + # try to convert to preferred_type + preferred_type = spec["preferred_type"] + self.log_msg(f"{self.class_name}.{method_name}: preferred_type: {preferred_type}") + value = self._verify_type(preferred_type, params, param) + if isinstance(value, self.types[preferred_type]): + return value + invalid = True error = "" + value = params[param] + expected_types = spec.get("type", []) + for expected_type in expected_types: if expected_type == "str": try: @@ -505,6 +559,28 @@ def _verify_multitype( msg += f"More info: {error}" self.ansible_module.fail_json(msg) + def verify_mandatory_param_spec_keys(self, params_spec: dict) -> None: + """ + Recurse over params_spec dictionary and verify that the + specification for each param contains the mandatory keys + defined in self.mandatory_param_spec_keys + """ + method_name = inspect.stack()[0][3] + for param in params_spec: + if not isinstance(params_spec[param], Map): + continue + if param in self.reserved_params: + continue + self.verify_mandatory_param_spec_keys(params_spec[param]) + for key in self.mandatory_param_spec_keys: + if key in params_spec[param]: + continue + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid params_spec. Missing mandatory key " + msg += f"'{key}' for param '{param}'." + self.ansible_module.fail_json(msg) + + @property def parameters(self): """ @@ -538,11 +614,5 @@ def params_spec(self, value): msg += "Invalid params_spec. Expected type dict. " msg += f"Got type {type(value)}." self.ansible_module.fail_json(msg) - for param in value: - for key in self.mandatory_param_spec_keys: - if key not in value[param]: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid params_spec. Missing key '{key}' for " - msg += f"param '{param}'." - self.ansible_module.fail_json(msg) + self.verify_mandatory_param_spec_keys(value) self.properties["params_spec"] = value diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index e3f3e400e..60ad1a182 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -829,6 +829,7 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec[section][sub_section]["module"] = {} params_spec[section][sub_section]["module"]["required"] = False params_spec[section][sub_section]["module"]["type"] = ["str", "int"] + params_spec[section][sub_section]["module"]["preferred_type"] = "str" params_spec[section][sub_section]["module"]["default"] = "ALL" params_spec[section][sub_section]["module"]["choices"] = [ str(x) for x in range(1, 33) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json index 430b54a91..7bebf20f3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json @@ -62,7 +62,7 @@ "ip_address": "2.2.2.2", "options": { "epld": { - "module": "1", + "module": 1, "golden": true }, "nxos": { diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index fa1bc4960..b95ec2873 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -278,7 +278,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert switch_2.get("ip_address") == "2.2.2.2" assert switch_2.get("options").get("epld").get("golden") is True - assert switch_2.get("options").get("epld").get("module") == 1 + assert switch_2.get("options").get("epld").get("module") == "1" assert switch_2.get("options").get("nxos").get("bios_force") is True assert switch_2.get("options").get("nxos").get("mode") == "non_disruptive" assert switch_2.get("options").get("package").get("install") is True diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index 0ae08d66d..e6ee18069 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -72,6 +72,7 @@ def test_params_validate_00001(params_validate) -> None: assert instance.reserved_params == { "choices", "default", + "preferred_type", "range_max", "range_min", "required", @@ -142,9 +143,9 @@ def test_params_validate_00022( params_spec["foo"] = {} params_spec["foo"][f"{present_key}"] = present_key_value - match = "ParamsValidate.params_spec: " + match = "ParamsValidate.verify_mandatory_param_spec_keys: " match += "Invalid params_spec. " - match += f"Missing key '{missing_key}' for param 'foo'." + match += f"Missing mandatory key '{missing_key}' for param 'foo'." with pytest.raises(AnsibleFailJson, match=match): instance = params_validate @@ -297,7 +298,15 @@ def test_params_validate_00053(params_validate) -> None: instance.validate() -def test_params_validate_00060(params_validate) -> None: +@pytest.mark.parametrize( + "value, type_to_verify", + [ + ("bing", "int"), + ("1", "ipv4"), + (False, "ipv4"), + ] +) +def test_params_validate_00060(params_validate, value, type_to_verify) -> None: """ Function - validate @@ -308,11 +317,11 @@ def test_params_validate_00060(params_validate) -> None: """ params_spec = {} params_spec["foo"] = {} - params_spec["foo"]["type"] = "int" + params_spec["foo"]["type"] = type_to_verify params_spec["foo"]["required"] = True parameters = {} - parameters["foo"] = "bing" + parameters["foo"] = value with does_not_raise(): instance = params_validate @@ -321,8 +330,8 @@ def test_params_validate_00060(params_validate) -> None: match = "ParamsValidate.invalid_type: " match += "Invalid type for parameter 'foo'. " - match += "Expected int. Got 'bing'. " - match += r"More info: \ cannot be converted to an int" + match += f"Expected {type_to_verify}. Got '{value}'. " + # match += r"More info: \ cannot be converted to an int" with pytest.raises(AnsibleFailJson, match=match): instance.validate() @@ -349,7 +358,7 @@ def test_params_validate_00060(params_validate) -> None: ("2001:1:1::/64", "ipv6_subnet"), ], ) -def test_params_validate_00061(params_validate, value, type_to_verify) -> None: +def test_params_validate_00070(params_validate, value, type_to_verify) -> None: """ Function - validate @@ -372,26 +381,31 @@ def test_params_validate_00061(params_validate, value, type_to_verify) -> None: instance.parameters = parameters instance.validate() + @pytest.mark.parametrize( - "value, type_to_verify", + "value, type_to_verify, preferred_type", [ - (1, ["int", "str"]), - ("1", ["dict", "ipv4"]), + (1, ["int", "str"], "int"), + ("1", ["int", "str"], "int"), + (1, ["int", "str"], "str"), + ("1", ["int", "str"], "str"), + (1, ["int", "str", "list"], "list"), + ("1", ["int", "str", "list"], "list"), ], ) -def test_params_validate_00062(params_validate, value, type_to_verify) -> None: +def test_params_validate_00071(params_validate, value, type_to_verify, preferred_type) -> None: """ Function - validate - - verify_type - verify_multitype Test - - parameter type is valid + - Convert parameter to preferred_type """ params_spec = {} params_spec["foo"] = {} params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["preferred_type"] = preferred_type params_spec["foo"]["required"] = True parameters = {} @@ -402,15 +416,19 @@ def test_params_validate_00062(params_validate, value, type_to_verify) -> None: instance.params_spec = params_spec instance.parameters = parameters instance.validate() + assert isinstance(instance.parameters["foo"], instance.types[preferred_type]) + + @pytest.mark.parametrize( - "value, type_to_verify", + "value, type_to_verify, preferred_type", [ - ("1", ["dict", "ipv4"]), + ("1", ["dict", "ipv4"], "dict"), + ("1", ["dict", "ipv4"], "ipv4"), ], ) -def test_params_validate_00062(params_validate, value, type_to_verify) -> None: +def test_params_validate_00072(params_validate, value, type_to_verify, preferred_type) -> None: """ Function - validate @@ -423,6 +441,7 @@ def test_params_validate_00062(params_validate, value, type_to_verify) -> None: params_spec = {} params_spec["foo"] = {} params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["preferred_type"] = preferred_type params_spec["foo"]["required"] = True parameters = {} @@ -433,9 +452,58 @@ def test_params_validate_00062(params_validate, value, type_to_verify) -> None: instance.params_spec = params_spec instance.parameters = parameters - match = "ParamsValidate._verify_multitype: " + match = "ParamsValidate.invalid_type: " match += "Invalid type for parameter 'foo'. " - match += r"Expected one of \['dict', 'ipv4'\]. " + match += f"Expected {preferred_type}. " + match += f"Got '{value}'." + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +@pytest.mark.parametrize( + "value, type_to_verify, preferred_type", + [ + ("1", ["dict", "ipv4"], "dict"), + ("1", ["dict", "ipv4"], "ipv4"), + ], +) +def test_params_validate_00073(params_validate, value, type_to_verify, preferred_type) -> None: + """ + Function + - validate + - verify_type + - verify_multitype + + Test + - parameter type is invalid in multi-level parameters + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = ["int", "str"] + params_spec["foo"]["preferred_type"] = "int" + params_spec["foo"]["required"] = True + params_spec["bar"] = {} + params_spec["bar"]["type"] = "dict" + params_spec["bar"]["required"] = False + params_spec["bar"]["baz"] = {} + params_spec["bar"]["baz"]["type"] = type_to_verify + params_spec["bar"]["baz"]["preferred_type"] = preferred_type + params_spec["bar"]["baz"]["required"] = True + + parameters = {} + parameters["foo"] = 1 + parameters["bar"] = {} + parameters["bar"]["baz"] = value + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate.invalid_type: " + match += "Invalid type for parameter 'baz'. " + match += f"Expected {preferred_type}. " match += f"Got '{value}'." with pytest.raises(AnsibleFailJson, match=match): @@ -450,7 +518,7 @@ def test_params_validate_00062(params_validate, value, type_to_verify) -> None: (10), ], ) -def test_params_validate_00070(params_validate, value) -> None: +def test_params_validate_00080(params_validate, value) -> None: """ Function - validate @@ -485,7 +553,7 @@ def test_params_validate_00070(params_validate, value) -> None: (11), ], ) -def test_params_validate_00071(params_validate, value) -> None: +def test_params_validate_00090(params_validate, value) -> None: """ Function - validate @@ -517,7 +585,7 @@ def test_params_validate_00071(params_validate, value) -> None: instance.validate() -def test_params_validate_00072(params_validate) -> None: +def test_params_validate_00091(params_validate) -> None: """ Function - validate From 66c8cbfb40476e377604553561c7bc71e9f12d37 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 18 Dec 2023 15:34:55 -1000 Subject: [PATCH 152/300] Harden ParamsValidate and add more unit tests, more... This should be the last modification to ParamsValidate. It turns out that, while Github Co-Pilot generated a "correct" version of self._verify_multitype(), it was anything but elegant. This has been rewritten to (hopefully) be more understandable and maintainable. Same goes for self._verify_type() --- .../module_utils/common/params_validate.py | 521 ++++++++---------- .../test_image_upgrade_params_validate.py | 253 +++++++-- 2 files changed, 428 insertions(+), 346 deletions(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 545a84c76..bfd94ec00 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -76,16 +76,36 @@ def __init__(self, ansible_module): self.mandatory_param_spec_keys = set() self.mandatory_param_spec_keys.add("required") self.mandatory_param_spec_keys.add("type") - self.types = dict() - self.types["str"] = str - self.types['int'] = int - self.types['float'] = float - self.types['bool'] = bool - self.types['dict'] = dict - self.types['list'] = list - self.types['set'] = set - self.types['tuple'] = tuple - + # Standard python types + self._types = {} + self._types["bool"] = bool + self._types["dict"] = dict + self._types["float"] = float + self._types["int"] = int + self._types["list"] = list + self._types["set"] = set + self._types["str"] = str + self._types["tuple"] = tuple + self._ipaddress_types = set() + self._ipaddress_types.add("ipv4") + self._ipaddress_types.add("ipv6") + self._ipaddress_types.add("ipv4_subnet") + self._ipaddress_types.add("ipv6_subnet") + self.valid_expected_types = set(self._types.keys()).union(self._ipaddress_types) + + self.validations = {} + self.validations["bool"] = validation.check_type_bool + self.validations["dict"] = validation.check_type_dict + self.validations["float"] = validation.check_type_float + self.validations["int"] = validation.check_type_int + self.validations["list"] = validation.check_type_list + self.validations["set"] = self._validate_set + self.validations["str"] = validation.check_type_str + self.validations["tuple"] = self._validate_tuple + self.validations["ipv4"] = self._validate_ipv4_address + self.validations["ipv6"] = self._validate_ipv6_address + self.validations["ipv4_subnet"] = self._validate_ipv4_subnet + self.validations["ipv6_subnet"] = self._validate_ipv6_subnet def log_msg(self, msg): """ @@ -141,9 +161,13 @@ def _validate_parameters(self, spec, parameters): self.ansible_module.fail_json(msg) if isinstance(spec[param]["type"], list): - parameters[param] = self._verify_multitype(spec[param], parameters, param) + parameters[param] = self._verify_multitype( + spec[param], parameters, param + ) else: - parameters[param] = self._verify_type(spec[param]["type"], parameters, param) + parameters[param] = self._verify_type( + spec[param]["type"], parameters, param + ) self._verify_choices( spec[param].get("choices", None), parameters[param], param @@ -207,357 +231,229 @@ def _verify_type(self, expected_type: str, params: Any, param: str) -> Any: Verify that value's type matches the expected type """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self._verify_expected_type(expected_type, param) value = params[param] - value = self._verify_str(expected_type, value, param) - value = self._verify_bool(expected_type, value, param) - value = self._verify_int(expected_type, value, param) - value = self._verify_float(expected_type, value, param) - value = self._verify_dict(expected_type, value, param) - value = self._verify_list(expected_type, value, param) - value = self._verify_set(expected_type, value, param) - value = self._verify_tuple(expected_type, value, param) - value = self._verify_ipv4(expected_type, value, param) - value = self._verify_ipv6(expected_type, value, param) - value = self._verify_ipv4_subnet(expected_type, value, param) - value = self._verify_ipv6_subnet(expected_type, value, param) - return value + if expected_type in self._ipaddress_types: + try: + self._ipaddress_guard(expected_type, value, param) + except TypeError as err: + self.invalid_type(expected_type, value, param, err) + return value - def _verify_str(self, expected_type: str, value: Any, param: str) -> Any: - """ - verify that value is a str, or convert to str if possible - If value is not a str, and conversion fails, - call invalid_type() to fail the playbook - """ - if expected_type != "str": - return value try: - return self.validation.check_type_str(value) - except TypeError as err: + return self.validations[expected_type](value) + except (ValueError, TypeError) as err: self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy - - def _verify_bool(self, expected_type: str, value: Any, param: str) -> Any: - """ - verify that value is a bool, or convert to bool if possible - If value is not a bool, and conversion fails, - call invalid_type() to fail the playbook - """ - if expected_type != "bool": return value - try: - return self.validation.check_type_bool(value) - except TypeError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy - def _verify_int(self, expected_type: str, value: Any, param: str) -> Any: + def _ipaddress_guard(self, expected_type, value: Any, param: str) -> None: """ - verify that value is an int, or convert to int if possible - If value is not an int, and conversion fails, - call invalid_type() to fail the playbook + Guard against int and bool types for ipv4, ipv6, ipv4_subnet, + and ipv6_subnet type. + + Raise TypeError if value's type is int or bool and + expected_type is one of self._ipaddress_types. + + The ipaddress module accepts int and bool types and converts + them to IP addresses or networks. E.g. True becomes 0.0.0.1, + False becomes 0.0.0.0, 1 becomse 0.0.0.1, etc. Because of + this, we need to fail int and bool values if expected_type is + one of ipv4, ipv6, ipv4_subnet, or ipv6_subnet. """ - if expected_type != "int": - return value - try: - return self.validation.check_type_int(value) - except TypeError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy + method_name = inspect.stack()[0][3] + if type(value) not in [int, bool]: + return + if expected_type not in self._ipaddress_types: + return + + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected type {expected_type}. " + msg += f"Got type {type(value)} for " + msg += f"param {param} with value {value}." + raise TypeError(f"{msg}") - def _verify_float(self, expected_type: str, value: Any, param: str) -> Any: + def invalid_type( + self, expected_type: str, value: Any, param: str, error: str = "" + ) -> None: """ - verify that value is a float, or convert to float if possible - If value is not a float, and conversion fails, - call invalid_type() to fail the playbook + Calls fail_json when value's type does not match expected_type """ - if expected_type != "float": - return value - try: - return self.validation.check_type_float(value) - except TypeError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid type for parameter '{param}'. " + msg += f"Expected {expected_type}. " + msg += f"Got '{value}'. " + msg += f"More info: {error}" + self.ansible_module.fail_json(msg) - def _verify_dict(self, expected_type: str, value: Any, param: str) -> Any: + def _verify_multitype( # pylint: disable=inconsistent-return-statements + self, spec: Any, params: Any, param: str + ) -> Any: """ - verify that value is a dict - check_type_dict() also converts strings with format - "k1=v1, k2=v2" to dict. + Verify that value's type matches one of the types in expected_types - If value is not a dict, and conversion fails, - call invalid_type() to fail the playbook + NOTES: + 1. We've disabled inconsistent-return-statements. We're pretty + sure this method is correct. """ - if expected_type != "dict": - return value - try: - return self.validation.check_type_dict(value) - except TypeError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy + method_name = inspect.stack()[0][3] - def _verify_list(self, expected_type: str, value: Any, param: str) -> Any: - """ - verify that value is a list - check_type_list() converts int, str, float to a single-element list. - It also converts comma-separated strings to lists. + # preferred_type is mandatory for multitype + self._verify_preferred_type(spec, param) - If value is not a list, and conversion fails, - call invalid_type() to fail the playbook - """ - if expected_type != "list": + # try to convert value to the preferred_type + preferred_type = spec["preferred_type"] + + (result, value) = self._verify_preferred_type_for_standard_types( + preferred_type, params[param] + ) + if result is True: return value - try: - return self.validation.check_type_list(value) - except TypeError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy - def _verify_set(self, expected_type: str, value: Any, param: str) -> Any: - """ - verify that value is a set - validate does not have a check_type_set() method so - we use isinstance() instead. + (result, value) = self._verify_preferred_type_for_ipaddress_types( + preferred_type, params[param] + ) + if result is True: + return value + + # Couldn't convert value to the preferred_type. Try the other types. + value = params[param] + + expected_types = spec.get("type", []) + + if preferred_type in expected_types: + # We've already tried preferred_type, so remove it + expected_types.remove(preferred_type) + + for expected_type in expected_types: + if expected_type in self._ipaddress_types and type(value) in [int, bool]: + # These are invalid, so skip them + continue - If value is not a set, call invalid_type() to fail the playbook + try: + value = self.validations[expected_type](value) + return value + except (ValueError, TypeError): + pass + + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid type for parameter '{param}'. " + msg += f"Expected one of {expected_types}. " + msg += f"Got '{value}'." + self.ansible_module.fail_json(msg) + + def _verify_preferred_type(self, spec: Any, param: str) -> None: """ - if expected_type != "set": - return value - if isinstance(value, set): - return value - error = f"Expected type set. Got type {type(value)} for " - error += f"param {param} with value {value}." - self.invalid_type(expected_type, value, param, error) - return value # never reached, but it makes pylint happy + verify that spec contains the key 'preferred_type' + """ + method_name = inspect.stack()[0][3] + if spec.get("preferred_type", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid param_spec for parameter '{param}'. " + msg += "If type is a list, preferred_type must be specified." + self.ansible_module.fail_json(msg) - def _verify_tuple(self, expected_type: str, value: Any, param: str) -> Any: + def _verify_preferred_type_for_standard_types( + self, preferred_type: str, value: Any + ) -> (bool, Any): """ - verify that value is a tuple - validate does not have a check_type_tuple() method so - we use isinstance() instead. + If preferred_type is one of the standard python types + we use isinstance() to check if we are able to convert + the value to preferred_type + """ + standard_type_success = True + if preferred_type not in self._types: + return (False, value) + try: + value = self.validations[preferred_type](value) + except (ValueError, TypeError): + standard_type_success = False + + if standard_type_success is True: + if isinstance(value, self._types[preferred_type]): + return (True, value) + return (False, value) - If value is not a tuple, call invalid_type() to fail the playbook + def _verify_preferred_type_for_ipaddress_types( + self, preferred_type: str, value: Any + ) -> (bool, Any): """ - if expected_type != "tuple": - return value - if isinstance(value, tuple): - return value - error = f"Expected type tuple. Got type {type(value)} for " - error += f"param {param} with value {value}." - self.invalid_type(expected_type, value, param, error) - return value # never reached, but it makes pylint happy + We can't use isinstance() to verify ipaddress types. + Hence, we check these types separately. + """ + ip_type_success = True + if preferred_type not in self._ipaddress_types: + return (False, value) + try: + value = self.validations[preferred_type](value) + except (ValueError, TypeError): + ip_type_success = False + if ip_type_success is True: + return (True, value) + return (False, value) - def _verify_ipv4(self, expected_type: str, value: Any, param: str) -> Any: + @staticmethod + def _validate_ipv4_address(value: Any) -> Any: """ verify that value is an IPv4 address - If value is not an IPv4 address, call invalid_type() - to fail the playbook """ - if expected_type != "ipv4": - return value - self.ipaddress_guard(expected_type, value, param) try: _ = ipaddress.IPv4Address(value) return value except ipaddress.AddressValueError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy + raise ValueError(f"invalid IPv4 address: {err}") from err - def _verify_ipv6(self, expected_type: str, value: Any, param: str) -> Any: + @staticmethod + def _validate_ipv4_subnet(value: Any) -> Any: """ - verify that value is an IPv6 address - If value is not an IPv6 address, call invalid_type() - to fail the playbook + verify that value is an IPv4 network """ - if expected_type != "ipv6": - return value - self.ipaddress_guard(expected_type, value, param) try: - _ = ipaddress.IPv6Address(value) + _ = ipaddress.IPv4Network(value) return value except ipaddress.AddressValueError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy + raise ValueError(f"invalid IPv4 network: {err}") from err - def _verify_ipv4_subnet(self, expected_type: str, value: Any, param: str) -> Any: + @staticmethod + def _validate_ipv6_address(value: Any) -> Any: """ - verify that value is an IPv4 subnet - If value is not an IPv4 subnet, call invalid_type() - to fail the playbook + verify that value is an IPv6 address """ - if expected_type != "ipv4_subnet": - return value - self.ipaddress_guard(expected_type, value, param) try: - _ = ipaddress.IPv4Network(value) + _ = ipaddress.IPv6Address(value) return value - except ValueError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy + except ipaddress.AddressValueError as err: + raise ValueError(f"invalid IPv6 address: {err}") from err - def _verify_ipv6_subnet(self, expected_type: str, value: Any, param: str) -> Any: + @staticmethod + def _validate_ipv6_subnet(value: Any) -> Any: """ - verify that value is an IPv6 subnet - If value is not an IPv6 subnet, call invalid_type() - to fail the playbook + verify that value is an IPv6 network """ - if expected_type != "ipv6_subnet": - return value - self.ipaddress_guard(expected_type, value, param) try: _ = ipaddress.IPv6Network(value) return value - except ValueError as err: - self.invalid_type(expected_type, value, param, err) - return value # never reached, but it makes pylint happy - - def ipaddress_guard(self, expected_type, value: Any, param: str) -> None: - """ - Guard against int and bool types for ipv4, ipv6, ipv4_subnet, - and ipv6_subnet type. - - The ipaddress module accepts int and bool types and converts - them to IP addresses or networks. E.g. True becomes 0.0.0.1, - False becomes 0.0.0.0, 1 becomse 0.0.0.1, etc. Because of - this, we need to fail int and bool values if expected_type is - one of ipv4, ipv6, ipv4_subnet, or ipv6_subnet. - """ - if isinstance(value, int): - error = f"Expected type ipv4. Got type {type(value)} for " - error += f"param {param} with value {value}." - self.invalid_type(expected_type, value, param, error) - if isinstance(value, bool): - error = f"Expected type ipv4. Got type {type(value)} for " - error += f"param {param} with value {value}." - self.invalid_type(expected_type, value, param, error) + except ipaddress.AddressValueError as err: + raise ValueError(f"invalid IPv6 network: {err}") from err - def invalid_type( - self, expected_type: str, value: Any, param: str, error: str - ) -> None: + @staticmethod + def _validate_set(value: Any) -> Any: """ - Calls fail_json when value's type does not match expected_type + verify that value is a set """ - method_name = inspect.stack()[0][3] - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid type for parameter '{param}'. " - msg += f"Expected {expected_type}. " - msg += f"Got '{value}'. " - msg += f"More info: {error}" - self.ansible_module.fail_json(msg) + if not isinstance(value, set): + raise TypeError(f"expected set, got {type(value)}") + return value - def _verify_multitype( - self, spec: Any, params: Any, param: str - ) -> Any: + @staticmethod + def _validate_tuple(value: Any) -> Any: """ - Verify that value's type matches one of the types in expected_types + verify that value is a tuple """ - method_name = inspect.stack()[0][3] - - # preferred_type is mandatory for multitype - if spec.get("preferred_type", None) is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid param_spec for parameter '{param}'. " - msg += "If type is a list, preferred_type must be specified." - self.ansible_module.fail_json(msg) - - msg = f"{self.class_name}.{method_name}: " - msg += f"spec: {spec}" - self.log_msg(msg) - # try to convert to preferred_type - preferred_type = spec["preferred_type"] - self.log_msg(f"{self.class_name}.{method_name}: preferred_type: {preferred_type}") - value = self._verify_type(preferred_type, params, param) - if isinstance(value, self.types[preferred_type]): - return value - - invalid = True - error = "" - value = params[param] - expected_types = spec.get("type", []) - - for expected_type in expected_types: - if expected_type == "str": - try: - value = self.validation.check_type_str(value) - invalid = False - except TypeError: - pass - elif expected_type == "bool": - try: - value = self.validation.check_type_bool(value) - invalid = False - except TypeError: - pass - elif expected_type == "int": - try: - value = self.validation.check_type_int(value) - invalid = False - except TypeError: - pass - elif expected_type == "dict": - try: - value = self.validation.check_type_dict(value) - invalid = False - except TypeError: - pass - elif expected_type == "list": - try: - value = self.validation.check_type_list(value) - invalid = False - except TypeError: - pass - elif expected_type == "set": - # validate does not have a check_type_set() method - if isinstance(value, set): - invalid = False - elif expected_type == "tuple": - # validate does not have a check_type_tuple() method - if isinstance(value, tuple): - invalid = False - elif expected_type == "float": - try: - value = self.validation.check_type_float(value) - invalid = False - except TypeError: - pass - elif expected_type == "ipv4": - try: - ipaddress.IPv4Address(value) - invalid = False - except ipaddress.AddressValueError: - pass - elif expected_type == "ipv6": - try: - ipaddress.IPv6Address(value) - invalid = False - except ipaddress.AddressValueError: - pass - elif expected_type == "ipv4_subnet": - try: - ipaddress.IPv4Network(value) - invalid = False - except ValueError: - pass - elif expected_type == "ipv6_subnet": - try: - ipaddress.IPv6Network(value) - invalid = False - except ValueError: - pass - else: - error = f"Unknown type {expected_type} for param {param} " - error += f"with value {value}." - invalid = True - - if invalid is False: - return value - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid type for parameter '{param}'. " - msg += f"Expected one of {expected_types}. " - msg += f"Got '{value}'." - if error: - msg += f"More info: {error}" - self.ansible_module.fail_json(msg) + if not isinstance(value, tuple): + raise TypeError(f"expected tuple, got {type(value)}") + return value def verify_mandatory_param_spec_keys(self, params_spec: dict) -> None: """ @@ -580,6 +476,19 @@ def verify_mandatory_param_spec_keys(self, params_spec: dict) -> None: msg += f"'{key}' for param '{param}'." self.ansible_module.fail_json(msg) + def _verify_expected_type(self, expected_type: str, param: str) -> None: + """ + Verify that expected_type is valid + """ + method_name = inspect.stack()[0][3] + if expected_type in self.valid_expected_types: + return + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid 'type' in params_spec for parameter '{param}'. " + msg += "Expected one of " + msg += f"'{','.join(sorted(self.valid_expected_types))}'. " + msg += f"Got '{expected_type}'." + self.ansible_module.fail_json(msg) @property def parameters(self): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index e6ee18069..286f482f2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -299,25 +299,28 @@ def test_params_validate_00053(params_validate) -> None: @pytest.mark.parametrize( - "value, type_to_verify", - [ - ("bing", "int"), - ("1", "ipv4"), - (False, "ipv4"), - ] + "value, expected_type", + [("bing", "int"), ("1", "ipv4"), (False, "set"), (True, "tuple"), ("bar", "bool")], ) -def test_params_validate_00060(params_validate, value, type_to_verify) -> None: +def test_params_validate_00060(params_validate, value, expected_type) -> None: """ Function - validate - verify_type Test - - parameter type is invalid + - parameter value's type is not convertable to expected_type + + NOTES: + 1. value == bool and type in [ipv4, ipv6, ipv4_subnet, ipv6_subnet] + is tested separately (see ipaddress_guard test) + 2. If expected_type is "str" ANY value (dict, tuple, float, int, etc) + will succeed. Hence, for expected_type == "str" there are no invalid + values. """ params_spec = {} params_spec["foo"] = {} - params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["type"] = expected_type params_spec["foo"]["required"] = True parameters = {} @@ -330,15 +333,14 @@ def test_params_validate_00060(params_validate, value, type_to_verify) -> None: match = "ParamsValidate.invalid_type: " match += "Invalid type for parameter 'foo'. " - match += f"Expected {type_to_verify}. Got '{value}'. " - # match += r"More info: \ cannot be converted to an int" + match += f"Expected {expected_type}. Got '{value}'. " with pytest.raises(AnsibleFailJson, match=match): instance.validate() @pytest.mark.parametrize( - "value, type_to_verify", + "value, expected_type", [ (1, "int"), ("1", "int"), @@ -358,7 +360,7 @@ def test_params_validate_00060(params_validate, value, type_to_verify) -> None: ("2001:1:1::/64", "ipv6_subnet"), ], ) -def test_params_validate_00070(params_validate, value, type_to_verify) -> None: +def test_params_validate_00061(params_validate, value, expected_type) -> None: """ Function - validate @@ -369,7 +371,7 @@ def test_params_validate_00070(params_validate, value, type_to_verify) -> None: """ params_spec = {} params_spec["foo"] = {} - params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["type"] = expected_type params_spec["foo"]["required"] = True parameters = {} @@ -382,29 +384,81 @@ def test_params_validate_00070(params_validate, value, type_to_verify) -> None: instance.validate() +def test_params_validate_00062(params_validate) -> None: + """ + Function + - validate + - verify_type + + Test + - verify_type calls fail_json if type is not a valid type + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["required"] = True + params_spec["foo"]["type"] = "bad_type" + + parameters = {} + parameters["foo"] = "bar" + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_expected_type: " + match += "Invalid 'type' in params_spec for parameter 'foo'. " + match += "Expected one of " + match += f"'{','.join(sorted(instance.valid_expected_types))}'. " + match += "Got 'bad_type'." + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + @pytest.mark.parametrize( - "value, type_to_verify, preferred_type", + "value, expected_type, preferred_type", [ - (1, ["int", "str"], "int"), + # preferred type != value's "native" type ("1", ["int", "str"], "int"), - (1, ["int", "str"], "str"), - ("1", ["int", "str"], "str"), (1, ["int", "str", "list"], "list"), ("1", ["int", "str", "list"], "list"), + (1.145, ["int", "list", "float"], "list"), + # preferred_type == value's "native" type + ("1", ["int", "str"], "str"), + (1, ["int", "str"], "int"), + ([1, 2, 3], ["int", "str", "list"], "list"), + (1.456, ["int", "str", "float"], "float"), + (False, ["int", "str", "bool"], "bool"), + ("1.1.1.1", ["int", "str", "ipv4"], "ipv4"), + # any type is convertable to str + (1, ["int", "str"], "str"), + ([1, 2, 3], ["int", "str", "list"], "str"), + ((1, 2, 3), ["int", "str", "list"], "str"), + ({1, 2, 3}, ["int", "str", "list"], "str"), + ({"foo": "bar"}, ["int", "str", "dict"], "str"), + (False, ["int", "str", "bool"], "str"), + (1.456, ["int", "str", "float"], "str"), ], ) -def test_params_validate_00071(params_validate, value, type_to_verify, preferred_type) -> None: +def test_params_validate_00071( + params_validate, value, expected_type, preferred_type +) -> None: """ Function - validate - - verify_multitype + - _verify_multitype Test - - Convert parameter to preferred_type + - Convert parameter value to preferred_type + + NOTES: + 1. ansible.module_utils.common.validation can/will convert + any type to type str. """ params_spec = {} params_spec["foo"] = {} - params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["type"] = expected_type params_spec["foo"]["preferred_type"] = preferred_type params_spec["foo"]["required"] = True @@ -416,9 +470,12 @@ def test_params_validate_00071(params_validate, value, type_to_verify, preferred instance.params_spec = params_spec instance.parameters = parameters instance.validate() - assert isinstance(instance.parameters["foo"], instance.types[preferred_type]) - - + if preferred_type in instance._ipaddress_types: # pylint: disable=protected-access + assert isinstance(instance.parameters["foo"], str) + else: + assert isinstance( + instance.parameters["foo"], instance._types[preferred_type] + ) # pylint: disable=protected-access @pytest.mark.parametrize( @@ -428,12 +485,14 @@ def test_params_validate_00071(params_validate, value, type_to_verify, preferred ("1", ["dict", "ipv4"], "ipv4"), ], ) -def test_params_validate_00072(params_validate, value, type_to_verify, preferred_type) -> None: +def test_params_validate_00072( + params_validate, value, type_to_verify, preferred_type +) -> None: """ Function - validate - verify_type - - verify_multitype + - _verify_multitype Test - parameter type is invalid @@ -452,11 +511,11 @@ def test_params_validate_00072(params_validate, value, type_to_verify, preferred instance.params_spec = params_spec instance.parameters = parameters - match = "ParamsValidate.invalid_type: " + match = "ParamsValidate._verify_multitype: " match += "Invalid type for parameter 'foo'. " - match += f"Expected {preferred_type}. " + match += r"Expected one of .*?. " match += f"Got '{value}'." - + with pytest.raises(AnsibleFailJson, match=match): instance.validate() @@ -468,12 +527,14 @@ def test_params_validate_00072(params_validate, value, type_to_verify, preferred ("1", ["dict", "ipv4"], "ipv4"), ], ) -def test_params_validate_00073(params_validate, value, type_to_verify, preferred_type) -> None: +def test_params_validate_00073( + params_validate, value, type_to_verify, preferred_type +) -> None: """ Function - validate - verify_type - - verify_multitype + - _verify_multitype Test - parameter type is invalid in multi-level parameters @@ -501,11 +562,123 @@ def test_params_validate_00073(params_validate, value, type_to_verify, preferred instance.params_spec = params_spec instance.parameters = parameters - match = "ParamsValidate.invalid_type: " + match = "ParamsValidate._verify_multitype: " match += "Invalid type for parameter 'baz'. " - match += f"Expected {preferred_type}. " + match += r"Expected one of .*?. " match += f"Got '{value}'." - + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +@pytest.mark.parametrize( + "value, type_to_verify, preferred_type", + [ + ("1", ["dict", "tuple", "list"], "dict"), + ], +) +def test_params_validate_00074( + params_validate, value, type_to_verify, preferred_type +) -> None: + """ + Function + - validate + - verify_type + - _verify_multitype + + Test + - Cannot convert parameter value to preferred_type, but can convert + to another type in _verify_multitype + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["preferred_type"] = preferred_type + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + instance.validate() + + +def test_params_validate_00075(params_validate) -> None: + """ + Function + - validate + - _verify_multitype + - _verify_preferred_type + + Test + - preferred_type key is missing from spec when spec.type + is a list of types. + + NOTES: + 1. preferred_type is mandatory when spec.type is a list of types. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = ["int", "str"] + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = 1 + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + match = "ParamsValidate._verify_preferred_type: " + match += "Invalid param_spec for parameter 'foo'. " + match += "If type is a list, preferred_type must be specified." + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +@pytest.mark.parametrize( + "value, type_to_verify", + [ + (1, "ipv4"), + (1, "ipv6"), + (1, "ipv4_subnet"), + (1, "ipv6_subnet"), + (True, "ipv4"), + (True, "ipv6"), + (True, "ipv4_subnet"), + (True, "ipv6_subnet"), + ], +) +def test_params_validate_00080(params_validate, value, type_to_verify) -> None: + """ + Function + - validate + - verify_type + - ipaddress_guard + + Test + - fail_json if type is in [ipv4, ipv6, ipv4_subnet, ipv6_subnet] + and value is bool or int. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._ipaddress_guard: " + match += f"Expected type {type_to_verify}. " + match += f"Got type {type(value)} for param foo with value {value}." with pytest.raises(AnsibleFailJson, match=match): instance.validate() @@ -518,7 +691,7 @@ def test_params_validate_00073(params_validate, value, type_to_verify, preferred (10), ], ) -def test_params_validate_00080(params_validate, value) -> None: +def test_params_validate_00090(params_validate, value) -> None: """ Function - validate @@ -553,7 +726,7 @@ def test_params_validate_00080(params_validate, value) -> None: (11), ], ) -def test_params_validate_00090(params_validate, value) -> None: +def test_params_validate_00100(params_validate, value) -> None: """ Function - validate @@ -580,12 +753,12 @@ def test_params_validate_00090(params_validate, value) -> None: match = "ParamsValidate._verify_integer_range: " match += "Invalid value for parameter 'foo'. " match += f"Expected value between 1 and 10. Got {value}" - + with pytest.raises(AnsibleFailJson, match=match): instance.validate() -def test_params_validate_00091(params_validate) -> None: +def test_params_validate_00101(params_validate) -> None: """ Function - validate @@ -613,7 +786,7 @@ def test_params_validate_00091(params_validate) -> None: match += "Invalid param_spec for parameter 'foo'. " match += "range_min and range_max are only valid for " match += "parameters of type int. Got type str for param foo." - + with pytest.raises(AnsibleFailJson, match=match): instance.validate() From b961965d29ec895885b36e90b0e18eca9f24c2d5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 18 Dec 2023 15:35:15 -1000 Subject: [PATCH 153/300] Run thru black/isort. --- plugins/modules/dcnm_image_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 60ad1a182..668a9e9fc 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -15,7 +15,7 @@ # limitations under the License. from __future__ import absolute_import, division, print_function -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type # pylint: disable=invalid-name __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" __email__ = "arobel@cisco.com" From 0cfd5fdcb72d71a07bd460d3ab30c5e704711104 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 19 Dec 2023 08:40:56 -1000 Subject: [PATCH 154/300] ParamsValidate hardening, add type check, more... ParamsValidate: - verify that range_min and range_max are integers before using them. - update example usage to include range_min and range_max. - privatize a few formerly-public methods. test_image_upgrade_params_validate.py - Add negative test to verify non-integer handling for range_min and range_max. - Modify match for a few tests to match private method names - Modify a couple mark.parametrize structures to include range_min and range_max --- .../module_utils/common/params_validate.py | 35 ++++++-- .../test_image_upgrade_params_validate.py | 84 ++++++++++++++----- 2 files changed, 89 insertions(+), 30 deletions(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index bfd94ec00..348346eb0 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -43,12 +43,18 @@ class ParamsValidate: params_spec["foo"]["bar"]["required"] = False params_spec["foo"]["bar"]["type"] = "str" params_spec["foo"]["bar"]["choices"] = ["bingo", "bango", "bongo"] + params_spec["foo"]["baz"] = {} + params_spec["foo"]["baz"]["required"] = False + params_spec["foo"]["baz"]["type"] = int + params_spec["foo"]["baz"]["range_min"] = 0 + params_spec["foo"]["baz"]["range_max"] = 10 Which describes the following YAML: ip_address: 1.2.3.4 foo: bar: bingo + baz: 10 validator = ParamsValidator(ansible_module) validator.parameters = ansible_module.params @@ -219,6 +225,15 @@ def _verify_integer_range( """ method_name = inspect.stack()[0][3] + for range_value in [range_min, range_max]: + if not isinstance(range_value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid specification for parameter '{param}'. " + msg += "range_min and range_max must be integers. Got " + msg += f"range_min '{range_min}' type {type(range_min)}, " + msg += f"range_max '{range_max}' type {type(range_max)}." + self.ansible_module.fail_json(msg) + if value < range_min or value > range_max: msg = f"{self.class_name}.{method_name}: " msg += f"Invalid value for parameter '{param}'. " @@ -238,13 +253,13 @@ def _verify_type(self, expected_type: str, params: Any, param: str) -> Any: try: self._ipaddress_guard(expected_type, value, param) except TypeError as err: - self.invalid_type(expected_type, value, param, err) + self._invalid_type(expected_type, value, param, err) return value try: return self.validations[expected_type](value) except (ValueError, TypeError) as err: - self.invalid_type(expected_type, value, param, err) + self._invalid_type(expected_type, value, param, err) return value def _ipaddress_guard(self, expected_type, value: Any, param: str) -> None: @@ -273,7 +288,7 @@ def _ipaddress_guard(self, expected_type, value: Any, param: str) -> None: msg += f"param {param} with value {value}." raise TypeError(f"{msg}") - def invalid_type( + def _invalid_type( self, expected_type: str, value: Any, param: str, error: str = "" ) -> None: """ @@ -287,7 +302,7 @@ def invalid_type( msg += f"More info: {error}" self.ansible_module.fail_json(msg) - def _verify_multitype( # pylint: disable=inconsistent-return-statements + def _verify_multitype( # pylint: disable=inconsistent-return-statements self, spec: Any, params: Any, param: str ) -> Any: """ @@ -300,7 +315,7 @@ def _verify_multitype( # pylint: disable=inconsistent-return-statements method_name = inspect.stack()[0][3] # preferred_type is mandatory for multitype - self._verify_preferred_type(spec, param) + self._verify_preferred_type_param_spec_is_present(spec, param) # try to convert value to the preferred_type preferred_type = spec["preferred_type"] @@ -343,7 +358,9 @@ def _verify_multitype( # pylint: disable=inconsistent-return-statements msg += f"Got '{value}'." self.ansible_module.fail_json(msg) - def _verify_preferred_type(self, spec: Any, param: str) -> None: + def _verify_preferred_type_param_spec_is_present( + self, spec: Any, param: str + ) -> None: """ verify that spec contains the key 'preferred_type' """ @@ -455,7 +472,7 @@ def _validate_tuple(value: Any) -> Any: raise TypeError(f"expected tuple, got {type(value)}") return value - def verify_mandatory_param_spec_keys(self, params_spec: dict) -> None: + def _verify_mandatory_param_spec_keys(self, params_spec: dict) -> None: """ Recurse over params_spec dictionary and verify that the specification for each param contains the mandatory keys @@ -467,7 +484,7 @@ def verify_mandatory_param_spec_keys(self, params_spec: dict) -> None: continue if param in self.reserved_params: continue - self.verify_mandatory_param_spec_keys(params_spec[param]) + self._verify_mandatory_param_spec_keys(params_spec[param]) for key in self.mandatory_param_spec_keys: if key in params_spec[param]: continue @@ -523,5 +540,5 @@ def params_spec(self, value): msg += "Invalid params_spec. Expected type dict. " msg += f"Got type {type(value)}." self.ansible_module.fail_json(msg) - self.verify_mandatory_param_spec_keys(value) + self._verify_mandatory_param_spec_keys(value) self.properties["params_spec"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index 286f482f2..011df06cf 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -143,7 +143,7 @@ def test_params_validate_00022( params_spec["foo"] = {} params_spec["foo"][f"{present_key}"] = present_key_value - match = "ParamsValidate.verify_mandatory_param_spec_keys: " + match = "ParamsValidate._verify_mandatory_param_spec_keys: " match += "Invalid params_spec. " match += f"Missing mandatory key '{missing_key}' for param 'foo'." @@ -331,7 +331,7 @@ def test_params_validate_00060(params_validate, value, expected_type) -> None: instance.params_spec = params_spec instance.parameters = parameters - match = "ParamsValidate.invalid_type: " + match = "ParamsValidate._invalid_type: " match += "Invalid type for parameter 'foo'. " match += f"Expected {expected_type}. Got '{value}'. " @@ -632,7 +632,7 @@ def test_params_validate_00075(params_validate) -> None: instance = params_validate instance.params_spec = params_spec instance.parameters = parameters - match = "ParamsValidate._verify_preferred_type: " + match = "ParamsValidate._verify_preferred_type_param_spec_is_present: " match += "Invalid param_spec for parameter 'foo'. " match += "If type is a list, preferred_type must be specified." with pytest.raises(AnsibleFailJson, match=match): @@ -684,18 +684,18 @@ def test_params_validate_00080(params_validate, value, type_to_verify) -> None: @pytest.mark.parametrize( - "value", + "value, range_min, range_max", [ - (1), - (5), - (10), + (1, 1, 10), + (5, 1, 10), + (10, 1, 10), ], ) -def test_params_validate_00090(params_validate, value) -> None: +def test_params_validate_00090(params_validate, value, range_min, range_max) -> None: """ Function - validate - - verify_integer_range + - _verify_integer_range Test - parameter (int) is within range_min and range_max @@ -706,8 +706,8 @@ def test_params_validate_00090(params_validate, value) -> None: params_spec["foo"] = {} params_spec["foo"]["type"] = "int" params_spec["foo"]["required"] = True - params_spec["foo"]["range_min"] = 1 - params_spec["foo"]["range_max"] = 10 + params_spec["foo"]["range_min"] = range_min + params_spec["foo"]["range_max"] = range_max parameters = {} parameters["foo"] = value @@ -719,18 +719,18 @@ def test_params_validate_00090(params_validate, value) -> None: @pytest.mark.parametrize( - "value", + "value, range_min, range_max", [ - (-1), - (0), - (11), + (-1, 1, 10), + (0, 1, 10), + (11, 1, 10), ], ) -def test_params_validate_00100(params_validate, value) -> None: +def test_params_validate_00091(params_validate, value, range_min, range_max) -> None: """ Function - validate - - verify_choices + - _verify_integer_range Test - parameter (int) is outside range_min and range_max @@ -739,8 +739,8 @@ def test_params_validate_00100(params_validate, value) -> None: params_spec["foo"] = {} params_spec["foo"]["type"] = "int" params_spec["foo"]["required"] = True - params_spec["foo"]["range_min"] = 1 - params_spec["foo"]["range_max"] = 10 + params_spec["foo"]["range_min"] = range_min + params_spec["foo"]["range_max"] = range_max parameters = {} parameters["foo"] = value @@ -758,11 +758,53 @@ def test_params_validate_00100(params_validate, value) -> None: instance.validate() -def test_params_validate_00101(params_validate) -> None: +@pytest.mark.parametrize( + "value, range_min, range_max", + [ + (-1, "foo", 10), + (0, 1, "bar"), + (11, [], {}), + ], +) +def test_params_validate_00092(params_validate, value, range_min, range_max) -> None: """ Function - validate - - verify_choices + - _verify_integer_range + + Test + - Negative. range_min or range_max is not an integer + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "int" + params_spec["foo"]["required"] = True + params_spec["foo"]["range_min"] = range_min + params_spec["foo"]["range_max"] = range_max + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_integer_range: " + match += "Invalid specification for parameter 'foo'. " + match += "range_min and range_max must be integers. Got " + match += rf"range_min '.*?' type {type(range_min)}, " + match += rf"range_max '.*?' type {type(range_max)}." + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +def test_params_validate_00093(params_validate) -> None: + """ + Function + - validate + - _verify_integer_range Test - Negative: non-int parameter with range_min and range_max specified. From f009242ee007a5c33d37cda90587acb5078e80f4 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 19 Dec 2023 16:53:51 -1000 Subject: [PATCH 155/300] Add properties to enable/disable debugging and set the logfile name, more... ParamsValidate: - Add ability to enable/disable logging on the fly - Add more reserved parameters to self.reserved_params (length_max and no_log) - Refactor __init__() test_image_upgrade_params_validate.py - Modify asserts in test_params_validate_00001 to reflect the above changes --- .../module_utils/common/params_validate.py | 138 +++++++++++++++--- .../test_image_upgrade_params_validate.py | 8 +- 2 files changed, 122 insertions(+), 24 deletions(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 348346eb0..0e2641a08 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -65,40 +65,88 @@ def __init__(self, ansible_module): self.class_name = __class__.__name__ self.ansible_module = ansible_module self.validation = validation - self.debug = False self.file_handle = None - self.logfile = "/tmp/ansible_dcnm.log" + + self._build_properties() + self._build_reserved_params() + self._build_mandatory_param_spec_keys() + self._build_standard_types() + self._build_ipaddress_types() + self._build_valid_expected_types() + self._build_validations() + + def _build_properties(self): + """ + Set default values for the properties in this class + """ self.properties = {} self.properties["parameters"] = None self.properties["params_spec"] = None + self.properties["debug"] = False + self.properties["logfile"] = None + + def _build_reserved_params(self): + """ + These are reserved parameter names that are skipped + during validation. + """ self.reserved_params = set() self.reserved_params.add("choices") self.reserved_params.add("default") + self.reserved_params.add("length_max") + self.reserved_params.add("no_log") self.reserved_params.add("range_max") self.reserved_params.add("range_min") self.reserved_params.add("required") self.reserved_params.add("type") self.reserved_params.add("preferred_type") - self.mandatory_param_spec_keys = set() - self.mandatory_param_spec_keys.add("required") - self.mandatory_param_spec_keys.add("type") - # Standard python types - self._types = {} - self._types["bool"] = bool - self._types["dict"] = dict - self._types["float"] = float - self._types["int"] = int - self._types["list"] = list - self._types["set"] = set - self._types["str"] = str - self._types["tuple"] = tuple + + def _build_standard_types(self): + """ + Standard python types. These are used with + isinstance() since isinstance() requires the + actual type and not the string representation. + """ + self._standard_types = {} + self._standard_types["bool"] = bool + self._standard_types["dict"] = dict + self._standard_types["float"] = float + self._standard_types["int"] = int + self._standard_types["list"] = list + self._standard_types["set"] = set + self._standard_types["str"] = str + self._standard_types["tuple"] = tuple + + def _build_ipaddress_types(self): + """ + IP address types require special handling since + they cannot be verified using isinstance(). + """ self._ipaddress_types = set() self._ipaddress_types.add("ipv4") self._ipaddress_types.add("ipv6") self._ipaddress_types.add("ipv4_subnet") self._ipaddress_types.add("ipv6_subnet") - self.valid_expected_types = set(self._types.keys()).union(self._ipaddress_types) + def _build_mandatory_param_spec_keys(self): + """ + Mandatory keys for every parameter in params_spec. + """ + self.mandatory_param_spec_keys = set() + self.mandatory_param_spec_keys.add("required") + self.mandatory_param_spec_keys.add("type") + + def _build_valid_expected_types(self): + """ + Valid values for the 'type' key in params_spec. + """ + self.valid_expected_types = set(self._standard_types.keys()).union(self._ipaddress_types) + + def _build_validations(self): + """ + Map of validation functions keyed by the parameter + type they validate. + """ self.validations = {} self.validations["bool"] = validation.check_type_bool self.validations["dict"] = validation.check_type_dict @@ -115,11 +163,18 @@ def __init__(self, ansible_module): def log_msg(self, msg): """ - used for debugging. disable this when committing to main - by setting self.debug to False in __init__() + Used for debugging. To enable, both debug and logfile properties + must be set e.g.: + + instance = ParamsValidate(ansible_module) + instance.debug = True + instance.logfile = "/tmp/params_validate.log" + etc... """ if self.debug is False: return + if self.logfile is None: + return if self.file_handle is None: try: # since we need self.file_handle open throughout this class @@ -140,7 +195,19 @@ def validate(self) -> None: """ Verify that parameters in self.parameters conform to self.params_spec """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] + if self.parameters is None: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.paramaters needs to be set " + msg += "prior to calling instance.validate()." + self.ansible_module.fail_json(msg) + + if self.params_spec is None: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.params_spec needs to be set " + msg += "prior to calling instance.validate()." + self.ansible_module.fail_json(msg) + self._validate_parameters(self.params_spec, self.parameters) def _validate_parameters(self, spec, parameters): @@ -380,7 +447,7 @@ def _verify_preferred_type_for_standard_types( the value to preferred_type """ standard_type_success = True - if preferred_type not in self._types: + if preferred_type not in self._standard_types: return (False, value) try: value = self.validations[preferred_type](value) @@ -388,7 +455,7 @@ def _verify_preferred_type_for_standard_types( standard_type_success = False if standard_type_success is True: - if isinstance(value, self._types[preferred_type]): + if isinstance(value, self._standard_types[preferred_type]): return (True, value) return (False, value) @@ -507,6 +574,35 @@ def _verify_expected_type(self, expected_type: str, param: str) -> None: msg += f"Got '{expected_type}'." self.ansible_module.fail_json(msg) + @property + def debug(self): + """ + Enable/disable debugging to self.logfile + """ + return self.properties["debug"] + + @debug.setter + def debug(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid type for debug. Expected bool. " + msg += f"Got {type(value)}." + self.ansible_module.fail_json(msg) + self.properties["debug"] = value + + @property + def logfile(self): + """ + Set file to which debug log is written + """ + return self.properties["logfile"] + + @logfile.setter + def logfile(self, value): + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.properties["logfile"] = value + @property def parameters(self): """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index 011df06cf..617acb93a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -72,6 +72,8 @@ def test_params_validate_00001(params_validate) -> None: assert instance.reserved_params == { "choices", "default", + "length_max", + "no_log", "preferred_type", "range_max", "range_min", @@ -80,9 +82,9 @@ def test_params_validate_00001(params_validate) -> None: } assert instance.mandatory_param_spec_keys == {"required", "type"} assert instance.class_name == "ParamsValidate" - assert instance.debug is False - assert instance.logfile == "/tmp/ansible_dcnm.log" assert instance.file_handle is None + assert instance.properties.get("debug", None) is False + assert instance.properties.get("logfile", "foo") is None assert instance.properties.get("parameters", "foo") is None assert instance.properties.get("params_spec", "foo") is None @@ -474,7 +476,7 @@ def test_params_validate_00071( assert isinstance(instance.parameters["foo"], str) else: assert isinstance( - instance.parameters["foo"], instance._types[preferred_type] + instance.parameters["foo"], instance._standard_types[preferred_type] ) # pylint: disable=protected-access From 57f499062fd8b54a88d32b6d9d39ef83e292b9ba Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 19 Dec 2023 16:54:53 -1000 Subject: [PATCH 156/300] Run thru black and isort --- plugins/module_utils/common/params_validate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 0e2641a08..d5f78a72f 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -140,7 +140,9 @@ def _build_valid_expected_types(self): """ Valid values for the 'type' key in params_spec. """ - self.valid_expected_types = set(self._standard_types.keys()).union(self._ipaddress_types) + self.valid_expected_types = set(self._standard_types.keys()).union( + self._ipaddress_types + ) def _build_validations(self): """ @@ -600,7 +602,7 @@ def logfile(self): @logfile.setter def logfile(self, value): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties["logfile"] = value @property From ad906054985efe578ea56f7f7b31dd6223bf1442 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 20 Dec 2023 08:16:03 -1000 Subject: [PATCH 157/300] Appease ansible sanity tests, more... Fix pep8 and missing __metaclass__ Ansible sanity errors. I guess there's a disagreement between Ansible sanity and pylint regarding __metaclass__ = type. To appease Ansible sanity, I'll ignore local pylint errors regarding this and modify my pylint config to ignore the error. Also, remove all module docstrings since these seem not to have a good place to go without causing Ansible sanity errors (per pylint, a module docstring is supposed to go at the very top of the file, but this causes Ansible sanity to complain about boilerplate). --- .../module_utils/common/controller_version.py | 18 ++++++++++++++++++ .../module_utils/common/params_validate.py | 17 ++++++++++++++++- .../module_utils/image_mgmt/api_endpoints.py | 4 +--- .../module_utils/image_mgmt/image_policies.py | 7 ++----- .../image_mgmt/image_policy_action.py | 4 +--- .../module_utils/image_mgmt/image_stage.py | 4 +--- .../module_utils/image_mgmt/image_upgrade.py | 5 +---- .../image_mgmt/image_upgrade_common.py | 8 +++----- .../module_utils/image_mgmt/image_validate.py | 16 ++++++++++------ .../image_mgmt/install_options.py | 1 + .../module_utils/image_mgmt/switch_details.py | 1 + .../image_mgmt/switch_issu_details.py | 1 + plugins/modules/dcnm_image_upgrade.py | 2 +- .../dcnm/dcnm_image_upgrade/fixture.py | 6 +----- .../dcnm_image_upgrade/image_upgrade_utils.py | 6 +++--- .../test_image_upgrade_api_endpoints.py | 9 ++++----- .../test_image_upgrade_controller_version.py | 12 +++++------- ...est_image_upgrade_image_install_options.py | 11 +++++------ .../test_image_upgrade_image_policies.py | 12 +++++------- .../test_image_upgrade_image_policy_action.py | 12 +++++------- .../test_image_upgrade_image_stage.py | 9 +++++---- .../test_image_upgrade_image_upgrade.py | 11 ++++------- ...test_image_upgrade_image_upgrade_common.py | 10 ++++++---- .../test_image_upgrade_image_upgrade_task.py | 11 ++++------- .../test_image_upgrade_image_validate.py | 19 +++++++++++-------- .../test_image_upgrade_params_validate.py | 11 ++++------- .../test_image_upgrade_switch_details.py | 13 +++++-------- ...rade_switch_issu_details_by_device_name.py | 12 +++++------- ...grade_switch_issu_details_by_ip_address.py | 13 +++++-------- ...de_switch_issu_details_by_serial_number.py | 12 +++++------- 30 files changed, 139 insertions(+), 138 deletions(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index cbfff2ddb..f7208dc24 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -1,3 +1,21 @@ +""" +Class to retrieve and return information about an NDFC controller +""" +# +# Copyright (c) 2024 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 diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index d5f78a72f..3562962d0 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -1,9 +1,24 @@ """ Validate that parameters conform to specification params_spec """ + +# Copyright (c) 2024 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 # pylint: disable=invalid-name +__metaclass__ = type import inspect import ipaddress diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py index eb3d04185..713350ae3 100644 --- a/plugins/module_utils/image_mgmt/api_endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -13,12 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Endpoints for image management API calls -""" from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" class ApiEndpoints: diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index af3ef3d50..061841607 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -13,13 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" - Retrieve image policy details from the controller and provide - property accessors for the policy attributes. -""" from __future__ import absolute_import, division, print_function -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type +__author__ = "Allen Robel" import inspect diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 3b230020b..85e398c94 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -13,12 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Perform image policy actions on the controller for one or more switches. -""" from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" import inspect import json diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 23d322e43..57eea0242 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -13,12 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -ImageStage - Methods to stage images to NX-OS switches -""" from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" import copy import inspect diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index a685ce9aa..c01135460 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -13,12 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -ImageUpgrade - Methods to upgrade images on NX-OS switches -""" from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" import copy import inspect @@ -225,7 +223,6 @@ def _build_payload(self, device) -> None: msg += f"device FINAL: {json.dumps(device, indent=4, sort_keys=True)}" self.log_msg(msg) - # TODO:2 Validate ip_address self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 9fac8aa76..e986e5f50 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -13,12 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Base class for the other image upgrade classes -""" from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" import inspect from collections.abc import MutableMapping as Map @@ -146,9 +144,9 @@ def log_msg(self, msg): try: # since we need self.fd open throughout several classes # we are disabling pylint R1732 - self.fd = open( + self.fd = open( # pylint: disable=consider-using-with f"{self.logfile}", "a+", encoding="UTF-8" - ) # pylint: disable=consider-using-with + ) except IOError as err: msg = f"error opening logfile {self.logfile}. " msg += f"detail: {err}" diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 70f87ba47..c673c58b1 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -13,12 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Validate images -""" from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" import copy import inspect @@ -75,6 +73,12 @@ def __init__(self, module): self.method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() + + self.path = self.endpoints.image_validate.get("path") + self.verb = self.endpoints.image_validate.get("verb") + self.payload = {} + self.serial_numbers_done: Set[str] = set() + self._init_properties() self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) @@ -133,6 +137,9 @@ def validate_serial_numbers(self) -> None: self.module.fail_json(msg) def build_payload(self) -> None: + """ + Build the payload for the image validation request + """ self.method_name = inspect.stack()[0][3] self.payload = {} @@ -159,9 +166,6 @@ def commit(self) -> None: self.validate_serial_numbers() self._wait_for_current_actions_to_complete() - self.path = self.endpoints.image_validate.get("path") - self.verb = self.endpoints.image_validate.get("verb") - self.build_payload() self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index ef126a029..6b74d98b1 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" import inspect import json diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index d916f4071..f4816eacc 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" import inspect diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 70b6a9e75..3a2600b6b 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +__author__ = "Allen Robel" import inspect diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 668a9e9fc..8bce216ca 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -15,7 +15,7 @@ # limitations under the License. from __future__ import absolute_import, division, print_function -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" __email__ = "arobel@cisco.com" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py index 25396ebce..bb3730787 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixture.py @@ -11,14 +11,10 @@ # 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. -""" -Function to load unit test inputs -Imported by image_upgrade_utils.py -""" from __future__ import absolute_import, division, print_function -__metaclass__ = type # pylint: disable=invalid-name +__metaclass__ = type import json import os diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index 9c3447307..8e115706f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Utilities for image_upgrade unit tests -""" from __future__ import absolute_import, division, print_function +__metaclass__ = type + + from contextlib import contextmanager from typing import Any, Dict diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py index c0f03a7fa..4ba374315 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py @@ -12,17 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -ApiEndpoints - unit tests -""" from __future__ import absolute_import, division, print_function -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ - ApiEndpoints +__metaclass__ = type __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ + ApiEndpoints + def test_image_mgmt_api_00001() -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py index 2d247e655..cd3c64456 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py @@ -19,11 +19,13 @@ # Some fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-argument -""" -ControllerVersion - unit tests -""" from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -33,10 +35,6 @@ from .image_upgrade_utils import (controller_version_fixture, responses_controller_version) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_COMMON = PATCH_MODULE_UTILS + "common." DCNM_SEND_VERSION = PATCH_COMMON + "controller_version.dcnm_send" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index 73162996a..da06dbc8e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -18,12 +18,14 @@ # pylint: disable=unused-import # Some fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-argument -""" -ImageInstallOptions - unit tests -""" from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -36,9 +38,6 @@ image_install_options_fixture, responses_image_install_options) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index d26a60d20..97db01d70 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -19,12 +19,13 @@ # Some fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-argument -""" -ImagePolicies - unit tests -""" - from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -37,9 +38,6 @@ image_policies_fixture, responses_image_policies) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 43d8c379d..2d151c9bd 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -12,10 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -ImagePolicyAction - unit tests -""" - # See the following regarding *_fixture imports # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html # Due to the above, we also need to disable unused-import @@ -25,6 +21,11 @@ from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -46,9 +47,6 @@ responses_switch_details, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 92a18a2a2..51f9e7ea7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -24,6 +24,11 @@ from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -41,10 +46,6 @@ responses_image_stage, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." PATCH_COMMON = PATCH_MODULE_UTILS + "common." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 5865bd917..1415d0116 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -21,12 +21,12 @@ # Some tests require calling protected methods # pylint: disable=protected-access +from __future__ import absolute_import, division, print_function -""" -ImageUpgrade - unit tests -""" +__metaclass__ = type -from __future__ import absolute_import, division, print_function +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" from typing import Any, Dict @@ -43,9 +43,6 @@ responses_image_upgrade, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index fbc323982..7e65b3b6f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -25,6 +25,11 @@ from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Dict import pytest @@ -34,9 +39,6 @@ from .image_upgrade_utils import (does_not_raise, image_upgrade_common_fixture, responses_image_upgrade_common) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: """ @@ -283,7 +285,7 @@ def test_image_mgmt_image_upgrade_common_00070( data = responses_image_upgrade_common(key) with does_not_raise(): - result = instance._handle_get_response( # pylint: disable=protected-access + result = instance._handle_get_response( # pylint: disable=protected-access data.get("response") ) # pylint: disable=protected-access diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index b95ec2873..653d5e9a4 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -21,12 +21,12 @@ # Some tests require calling protected methods # pylint: disable=protected-access +from __future__ import absolute_import, division, print_function -""" -ImageUpgradeTask - unit tests -""" +__metaclass__ = type -from __future__ import absolute_import, division, print_function +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" from typing import Any, Dict @@ -49,9 +49,6 @@ responses_image_upgrade, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index bf8c92130..6bf34c81d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -21,12 +21,13 @@ # Some tests require calling protected methods # pylint: disable=protected-access -""" -ImageUpgrade - unit tests -""" - from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -41,10 +42,6 @@ issu_details_by_serial_number_fixture, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" @@ -62,6 +59,12 @@ def test_image_mgmt_validate_00001(image_validate) -> None: assert instance.class_name == "ImageValidate" assert isinstance(instance.endpoints, ApiEndpoints) assert isinstance(instance.issu_detail, SwitchIssuDetailsBySerialNumber) + assert isinstance(instance.serial_numbers_done, set) + assert ( + instance.path + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image" + ) + assert instance.verb == "POST" def test_image_mgmt_validate_00002(image_validate) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index 617acb93a..653428ca4 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -21,12 +21,12 @@ # Some tests require calling protected methods # pylint: disable=protected-access +from __future__ import absolute_import, division, print_function -""" -ParamsValidate - unit tests -""" +__metaclass__ = type -from __future__ import absolute_import, division, print_function +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" from typing import Any, Dict @@ -45,9 +45,6 @@ responses_image_upgrade, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 70a145cd8..974d9fafb 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -21,12 +21,13 @@ # Some tests require calling protected methods # pylint: disable=protected-access -""" -SwitchDetails - unit tests -""" - from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -38,10 +39,6 @@ from .image_upgrade_utils import (does_not_raise, responses_switch_details, switch_details_fixture) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.dcnm_send" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index 11c76a41a..82cc482f9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -19,12 +19,13 @@ # Some fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-argument -""" -SwitchIssuDetailsByDeviceName - unit tests -""" - from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -35,9 +36,6 @@ issu_details_by_device_name_fixture, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 110a3ac27..e5ce0bbe1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -19,12 +19,13 @@ # Some fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-argument -""" -SwitchIssuDetailsByIpAddress - unit tests -""" - from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -35,10 +36,6 @@ issu_details_by_ip_address_fixture, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 9e5704e7e..b15728e96 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -19,12 +19,14 @@ # Some fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-argument -""" -SwitchIssuDetailsBySerialNumber - unit tests -""" from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + from typing import Any, Dict import pytest @@ -35,10 +37,6 @@ issu_details_by_serial_number_fixture, responses_switch_issu_details) -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - - PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" From b1dda7d6324c3269d1691ed588a81e5d94f7b935 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 20 Dec 2023 13:40:42 -1000 Subject: [PATCH 158/300] ImageValidate: add unit tests for instance.commit() --- .../module_utils/image_mgmt/image_validate.py | 2 +- ...image_upgrade_responses_ImageValidate.json | 34 ++++ ...e_upgrade_responses_SwitchIssuDetails.json | 180 ++++++++++++++++++ .../dcnm_image_upgrade/image_upgrade_utils.py | 9 + .../test_image_upgrade_image_validate.py | 82 +++++++- 5 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index c673c58b1..951d77e8b 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -92,7 +92,7 @@ def _init_properties(self) -> None: self.properties["result"] = {} self.properties["response"] = {} self.properties["non_disruptive"] = False - self.properties["serial_numbers"] = [] + self.properties["serial_numbers"] = None def prune_serial_numbers(self) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json new file mode 100644 index 000000000..349da227f --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json @@ -0,0 +1,34 @@ +{ + "test_image_mgmt_validate_00020a": { + "TEST_NOTES": [ + "Needed only for the 200 return code" + ], + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } + }, + "test_image_mgmt_validate_00021a": { + "TEST_NOTES": [ + "Needed only for the 200 return code", + "RETURN_CODE == 200", + "MESSAGE == OK" + ], + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 709e94458..e942c1e7e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -1750,6 +1750,186 @@ "message": "" } }, + "test_image_mgmt_validate_00020a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Failed", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_mgmt_validate_00021a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Failed", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, "test_image_mgmt_image_policy_action_00003a": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index 8e115706f..f53c4b787 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -292,6 +292,15 @@ def responses_image_upgrade_common(key: str) -> Dict[str, str]: return {"response": response, "verb": verb} +def responses_image_validate(key: str) -> Dict[str, str]: + """ + Return ImageValidate controller responses + """ + response_file = "image_upgrade_responses_ImageValidate" + response = load_fixture(response_file).get(key) + print(f"responses_image_validate: {key} : {response}") + return response + def responses_switch_details(key: str) -> Dict[str, str]: """ Return SwitchDetails controller responses diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 6bf34c81d..0eeffdddf 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -38,12 +38,14 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber -from .image_upgrade_utils import (image_validate_fixture, +from .image_upgrade_utils import (does_not_raise, image_validate_fixture, issu_details_by_serial_number_fixture, + responses_image_validate, responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +DCNM_SEND_IMAGE_VALIDATE = PATCH_IMAGE_MGMT + "image_validate.dcnm_send" DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" @@ -83,7 +85,7 @@ def test_image_mgmt_validate_00002(image_validate) -> None: assert instance.properties.get("response") == {} assert instance.properties.get("result") == {} assert instance.properties.get("non_disruptive") is False - assert instance.properties.get("serial_numbers") == [] + assert instance.properties.get("serial_numbers") is None def test_image_mgmt_validate_00003( @@ -397,3 +399,79 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert len(instance.serial_numbers_done) == 1 assert "FDO21120U5D" in instance.serial_numbers_done assert "FDO2112189M" not in instance.serial_numbers_done + + +MATCH_00020 = "ImageValidate.commit: call instance.serial_numbers " +MATCH_00020 += "before calling commit." + + +@pytest.mark.parametrize( + "serial_numbers_is_set, expected", + [ + (True, does_not_raise()), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00020)), + ], +) +def test_image_mgmt_validate_00020( + monkeypatch, image_validate, serial_numbers_is_set, expected +) -> None: + """ + Function + commit + + Test + - fail_json is called when serial_numbers is None + - fail_json is not called when serial_numbers is set + """ + + def mock_dcnm_send_image_validate(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_validate_00020a" + return responses_image_validate(key) + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_validate_00020a" + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_VALIDATE, mock_dcnm_send_image_validate) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + instance = image_validate + assert instance.class_name == "ImageValidate" + + if serial_numbers_is_set: + instance.serial_numbers = ["FDO21120U5D"] + with expected: + instance.commit() + + +def test_image_mgmt_validate_00021(monkeypatch, image_validate) -> None: + """ + Function + - commit + + Test + - ImageValidate.verb is set to POST + - ImageValidate.path is set to: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image + """ + + # Needed only for the 200 return code + def mock_dcnm_send_image_validate(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_validate_00021a" + return responses_image_validate(key) + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_validate_00021a" + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_VALIDATE, mock_dcnm_send_image_validate) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" + module_path += "stagingmanagement/validate-image" + + instance = image_validate + instance.serial_numbers = ["FDO21120U5D"] + instance.commit() + assert instance.path == module_path + assert instance.verb == "POST" From 30240086b5d5effdd04268849de13a4a574ad2c5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 20 Dec 2023 15:14:05 -1000 Subject: [PATCH 159/300] ImageValidate: additional unit tests, more... ImageValidate - isinstance(True, int) ends up being True, so we needed to add a check for isinstance(value, bool) to a couple properties. - Added unit tests for commit() and for several properties --- .../module_utils/image_mgmt/image_validate.py | 13 +- ...image_upgrade_responses_ImageValidate.json | 16 ++ ...e_upgrade_responses_SwitchIssuDetails.json | 90 +++++++++ .../test_image_upgrade_image_validate.py | 184 ++++++++++++++++++ 4 files changed, 302 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 951d77e8b..baefb4fa8 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -343,7 +343,13 @@ def check_interval(self): def check_interval(self, value): self.method_name = inspect.stack()[0][3] + result = True + # isinstance(True, int) is True so we need to check for bool first + if isinstance(value, bool): + result = False if not isinstance(value, int): + result = False + if result is False: msg = f"{self.class_name}.{self.method_name}: " msg += "instance.check_interval must be an integer. " msg += f"Got {value}." @@ -362,10 +368,15 @@ def check_timeout(self): def check_timeout(self, value): self.method_name = inspect.stack()[0][3] + result = True + # isinstance(True, int) is True so we need to check for bool first + if isinstance(value, bool): + result = False if not isinstance(value, int): + result = False + if result is False: msg = f"{self.class_name}.{self.method_name}: " msg += "instance.check_timeout must be an integer. " msg += f"Got {value}." self.module.fail_json(msg) - self.properties["check_timeout"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json index 349da227f..c65864e91 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json @@ -30,5 +30,21 @@ ], "message": "" } + }, + "test_image_mgmt_validate_00023a": { + "TEST_NOTES": [ + "RETURN_CODE == 501", + "MESSAGE == INTERNAL SERVER ERROR" + ], + "RETURN_CODE": 501, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", + "MESSAGE": "INTERNAL SERVER ERROR", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index e942c1e7e..02dd3d8ef 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -1930,6 +1930,96 @@ "message": "" } }, + "test_image_mgmt_validate_00023a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Failed", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, "test_image_mgmt_image_policy_action_00003a": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 0eeffdddf..fe66420a5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -475,3 +475,187 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.commit() assert instance.path == module_path assert instance.verb == "POST" + + +def test_image_mgmt_validate_00022(image_validate) -> None: + """ + Function + - commit + + Test + - instance.response is set to {} because dcnm_send was not called + - instance.result is set to {} because dcnm_send was not called + + Description + If instance.serial_numbers is an empty list, instance.commit() returns + without calling dcnm_send. + """ + with does_not_raise(): + instance = image_validate + instance.serial_numbers = [] + instance.commit() + assert instance.response == {} + assert instance.result == {} + + +def test_image_mgmt_validate_00023(monkeypatch, image_validate) -> None: + """ + Function + - commit + + Test + - 501 response from controller endpoint: + POST /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image + """ + + # Needed only for the 501 return code + def mock_dcnm_send_image_validate(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_validate_00023a" + return responses_image_validate(key) + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_validate_00023a" + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_VALIDATE, mock_dcnm_send_image_validate) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + with does_not_raise(): + instance = image_validate + instance.serial_numbers = ["FDO21120U5D"] + with pytest.raises(AnsibleFailJson, match="failed:"): + instance.commit() + assert instance.result["success"] is False + assert instance.result["changed"] is False + assert instance.response["RETURN_CODE"] == 501 + + +MATCH_00030 = "ImageValidate.serial_numbers: " +MATCH_00030 += "instance.serial_numbers must be a python list " +MATCH_00030 += "of switch serial numbers." + + +@pytest.mark.parametrize( + "value, expected", + [ + ([], does_not_raise()), + (True, pytest.raises(AnsibleFailJson, match=MATCH_00030)), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00030)), + (None, pytest.raises(AnsibleFailJson, match=MATCH_00030)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00030)), + (10, pytest.raises(AnsibleFailJson, match=MATCH_00030)), + ({1, 2}, pytest.raises(AnsibleFailJson, match=MATCH_00030)), + ({"a": 1, "b": 2}, pytest.raises(AnsibleFailJson, match=MATCH_00030)), + ], +) +def test_image_mgmt_validate_00030(image_validate, value, expected) -> None: + """ + Function + - serial_numbers.setter + + Test + - fail_json when serial_numbers is not a list + """ + with does_not_raise(): + instance = image_validate + assert instance.class_name == "ImageValidate" + + with expected: + instance.serial_numbers = value + + +MATCH_00040 = "ImageValidate.make_boolean: " +MATCH_00040 += "instance.non_disruptive must be a boolean." + + +@pytest.mark.parametrize( + "value, expected", + [ + (True, does_not_raise()), + (False, does_not_raise()), + (None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00040)), + (10, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ([1, 2], pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ({1, 2}, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ({"a": 1, "b": 2}, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ], +) +def test_image_mgmt_validate_00040(image_validate, value, expected) -> None: + """ + Function + - non_disruptive.setter + + Test + - fail_json when non_disruptive is not a boolean + """ + with does_not_raise(): + instance = image_validate + assert instance.class_name == "ImageValidate" + + with expected: + instance.non_disruptive = value + + +MATCH_00050 = "ImageValidate.check_interval: " +MATCH_00050 += "instance.check_interval must be an integer." + + +@pytest.mark.parametrize( + "value, expected", + [ + (10, does_not_raise()), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00050)), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00050)), + (None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), + ([1, 2], pytest.raises(AnsibleFailJson, match=MATCH_00050)), + ({1, 2}, pytest.raises(AnsibleFailJson, match=MATCH_00050)), + ({"a": 1, "b": 2}, pytest.raises(AnsibleFailJson, match=MATCH_00050)), + ], +) +def test_image_mgmt_validate_00050(image_validate, value, expected) -> None: + """ + Function + - check_interval.setter + + Test + - fail_json when check_interval is not an integer + """ + with does_not_raise(): + instance = image_validate + assert instance.class_name == "ImageValidate" + + with expected: + instance.check_interval = value + + +MATCH_00060 = "ImageValidate.check_timeout: " +MATCH_00060 += "instance.check_timeout must be an integer." + + +@pytest.mark.parametrize( + "value, expected", + [ + (10, does_not_raise()), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060)), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00060)), + (None, pytest.raises(AnsibleFailJson, match=MATCH_00060)), + ([1, 2], pytest.raises(AnsibleFailJson, match=MATCH_00060)), + ({1, 2}, pytest.raises(AnsibleFailJson, match=MATCH_00060)), + ({"a": 1, "b": 2}, pytest.raises(AnsibleFailJson, match=MATCH_00060)), + ], +) +def test_image_mgmt_validate_00060(image_validate, value, expected) -> None: + """ + Function + - check_timeout.setter + + Test + - fail_json when check_timeout is not an integer + """ + with does_not_raise(): + instance = image_validate + assert instance.class_name == "ImageValidate" + + with expected: + instance.check_timeout = value From fc53763ee1ad65b924ad46b84982ede4827097d5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 20 Dec 2023 19:22:35 -1000 Subject: [PATCH 160/300] ImageUpgradeTask: leverage ParamsMergeDefault class 1. A few boilerplate lines were updated. 2. ImageUpgradeTask - Previously, we were creating a defaults dictionary which we used to update the merged config. This was not optimal since we were, in essence, duplicating information about default values for parameters that is already in a properly-written params_spec. Wrote ParamsMergeDefaults class which takes a params_spec, and a playbook config and updates the playbook config to add any missing parameters that have a default value. This required re-writing some unit tests. And, as a result of using this, a errors were found in the params_spec for image_upgrade (the following were not specified: reboot, upgrade.epld, upgrade.nxos). --- .../module_utils/common/controller_version.py | 2 + .../common/params_merge_defaults.py | 232 +++++++++++ .../module_utils/common/params_validate.py | 6 +- plugins/modules/dcnm_image_upgrade.py | 89 ++--- .../image_upgrade_playbook_configs.json | 154 ++++++++ .../test_image_upgrade_image_upgrade_task.py | 370 ++++++------------ 6 files changed, 552 insertions(+), 301 deletions(-) create mode 100644 plugins/module_utils/common/params_merge_defaults.py diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index f7208dc24..8f5a60e2a 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -19,6 +19,8 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py new file mode 100644 index 000000000..8ce32fade --- /dev/null +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -0,0 +1,232 @@ +# Copyright (c) 2024 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 +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect +from collections.abc import MutableMapping as Map +from typing import Any, Dict + +class ParamsMergeDefaults: + """ + Merge default parameters into parameters. + + Given a parameter specification (params_spec) and a playbook config + (parameters) merge key/values from params_spec which have a default + associated with them into parameters (if parameters is missing the + corresponding key/value). + """ + def __init__(self, ansible_module): + self.class_name = self.__class__.__name__ + self.ansible_module = ansible_module + self.file_handle = None + + self._build_properties() + self._build_reserved_params() + self.committed = False + + def _build_properties(self): + """ + Container for the properties of this class. + """ + method_name = inspect.stack()[0][3] + self.properties = {} + self.properties["params_spec"] = None + self.properties["parameters"] = None + self.properties["merged_parameters"] = None + self.properties["debug"] = False + self.properties["logfile"] = None + + def _build_reserved_params(self): + """ + These are reserved parameter names that are skipped + during merge. + """ + self.reserved_params = set() + self.reserved_params.add("choices") + self.reserved_params.add("default") + self.reserved_params.add("length_max") + self.reserved_params.add("no_log") + self.reserved_params.add("range_max") + self.reserved_params.add("range_min") + self.reserved_params.add("required") + self.reserved_params.add("type") + self.reserved_params.add("preferred_type") + + def _merge_default_params( + self, + spec: Dict[str, Any], + params: Dict[str, Any]) -> Dict[str, Any]: + """ + Merge default parameters into parameters. + + Caller: + - commit() + Return: + - A modified copy of params where missing parameters are added if: + 1. they are present in spec + 2. they have a default value defined in spec + """ + method_name = inspect.stack()[0][3] + + for spec_key, spec_value in spec.items(): + + if spec_key in self.reserved_params: + continue + + if spec_key not in params and "default" in spec_value: + params[spec_key] = spec_value["default"] + + if spec_key not in params and "default" not in spec_value: + continue + + if isinstance(spec_value, Map): + params[spec_key] = self._merge_default_params(spec_value, params[spec_key]) + + return copy.deepcopy(params) + + def commit(self) -> None: + """ + Merge default parameters into parameters. + + The merged parameters are stored in self.merged_parameters + """ + method_name = inspect.stack()[0][3] + + if self.params_spec is None: + msg = f"{self.class_name}.{method_name}: " + msg += "Cannot commit. params_spec is None." + self.ansible_module.fail_json(msg) + + if self.parameters is None: + msg = f"{self.class_name}.{method_name}: " + msg += "Cannot commit. parameters is None." + self.ansible_module.fail_json(msg) + + self.properties["merged_parameters"] = self._merge_default_params(self.params_spec, self.parameters) + + + def log_msg(self, msg): + """ + Used for debugging. To enable, both debug and logfile properties + must be set e.g.: + + instance = ParamsValidate(ansible_module) + instance.debug = True + instance.logfile = "/tmp/params_validate.log" + etc... + """ + if self.debug is False: + return + if self.logfile is None: + return + if self.file_handle is None: + try: + # since we need self.file_handle open throughout this class + # we are disabling pylint R1732 + self.file_handle = open( # pylint: disable=consider-using-with + f"{self.logfile}", "a+", encoding="UTF-8" + ) + except IOError as err: + msg = f"error opening logfile {self.logfile}. " + msg += f"detail: {err}" + self.ansible_module.fail_json(msg) + + self.file_handle.write(msg) + self.file_handle.write("\n") + self.file_handle.flush() + + @property + def debug(self): + """ + Enable/disable debugging to self.logfile + """ + return self.properties["debug"] + + @debug.setter + def debug(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid type for debug. Expected bool. " + msg += f"Got {type(value)}." + self.ansible_module.fail_json(msg) + self.properties["debug"] = value + + @property + def logfile(self): + """ + Set file to which debug log is written + """ + return self.properties["logfile"] + + @logfile.setter + def logfile(self, value): + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.properties["logfile"] = value + + @property + def merged_parameters(self): + """ + Getter for the merged parameters. + """ + if self.properties["merged_parameters"] is None: + msg = f"{self.class_name}.merged_parameters: " + msg += "Call instance.commit() before calling merged_parameters." + self.ansible_module.fail_json(msg) + return self.properties["merged_parameters"] + + @property + def parameters(self): + """ + The parameters into which defaults are merged. + + The merge consists of adding any missing parameters + (per a comparison with params_spec) and setting their + value to the default value defined in params_spec. + """ + return self.properties["parameters"] + + @parameters.setter + def parameters(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid parameters. Expected type dict. " + msg += f"Got type {type(value)}." + self.ansible_module.fail_json(msg) + self.properties["parameters"] = value + + @property + def params_spec(self): + """ + The param specification used to validate the parameters + """ + return self.properties["params_spec"] + + @params_spec.setter + def params_spec(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid params_spec. Expected type dict. " + msg += f"Got type {type(value)}." + self.ansible_module.fail_json(msg) + self.properties["params_spec"] = value + diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 3562962d0..47e71b266 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -1,7 +1,3 @@ -""" -Validate that parameters conform to specification params_spec -""" - # Copyright (c) 2024 Cisco and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +15,8 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" import inspect import ipaddress diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 8bce216ca..7497ea6cb 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -18,7 +18,6 @@ __metaclass__ = type __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -__email__ = "arobel@cisco.com" DOCUMENTATION = """ --- @@ -407,6 +406,8 @@ from typing import Any, Dict, List from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ + ParamsMergeDefaults from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -449,7 +450,6 @@ def __init__(self, module): self.class_name = self.__class__.__name__ method_name = inspect.stack()[0][3] - self._init_defaults() self.endpoints = ApiEndpoints() self.have = None self.idempotent_want = None @@ -483,32 +483,6 @@ def __init__(self, module): self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) - def _init_defaults(self): - """ - Default values for playbook parameters. - These are merged with playbook parameters in - self._merge_defaults_to_switch_config() - """ - self.defaults: Dict[str, Any] = {} - self.defaults["options"] = {} - self.defaults["options"]["epld"] = {} - self.defaults["options"]["epld"]["module"] = "ALL" - self.defaults["options"]["epld"]["golden"] = False - self.defaults["options"]["nxos"] = {} - self.defaults["options"]["nxos"]["mode"] = "disruptive" - self.defaults["options"]["nxos"]["bios_force"] = False - self.defaults["options"]["package"] = {} - self.defaults["options"]["package"]["install"] = False - self.defaults["options"]["package"]["uninstall"] = False - self.defaults["options"]["reboot"] = {} - self.defaults["options"]["reboot"]["config_reload"] = False - self.defaults["options"]["reboot"]["write_erase"] = False - self.defaults["reboot"] = False - self.defaults["stage"] = True - self.defaults["upgrade"] = {} - self.defaults["upgrade"]["epld"] = False - self.defaults["upgrade"]["nxos"] = True - self.defaults["validate"] = True def get_have(self) -> None: """ @@ -760,6 +734,19 @@ def get_need_query(self) -> None: need.append(want) self.need = copy.copy(need) + def _build_params_spec(self) -> Dict[str, Any]: + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + if self.module.params["state"] == "merged": + return self._build_params_spec_for_merged_state() + elif self.module.params["state"] == "deleted": + return self._build_params_spec_for_merged_state() + elif self.module.params["state"] == "query": + return self._build_params_spec_for_query_state() + else: + msg = f"{self.class_name}.{method_name}: " + msg += f"Unsupported state: {self.module.params['state']}" + self.module.fail_json(msg) + @staticmethod def _build_params_spec_for_merged_state() -> Dict[str, Any]: """ @@ -778,6 +765,11 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec["policy"]["required"] = False params_spec["policy"]["type"] = "str" + params_spec["reboot"] = {} + params_spec["reboot"]["required"] = False + params_spec["reboot"]["type"] = "bool" + params_spec["reboot"]["default"] = False + params_spec["stage"] = {} params_spec["stage"]["required"] = False params_spec["stage"]["type"] = "bool" @@ -792,6 +784,15 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec["upgrade"]["required"] = False params_spec["upgrade"]["type"] = "dict" params_spec["upgrade"]["default"] = {} + params_spec["upgrade"]["epld"] = {} + params_spec["upgrade"]["epld"]["required"] = False + params_spec["upgrade"]["epld"]["type"] = "bool" + params_spec["upgrade"]["epld"]["default"] = False + params_spec["upgrade"]["nxos"] = {} + params_spec["upgrade"]["nxos"]["required"] = False + params_spec["upgrade"]["nxos"]["type"] = "bool" + params_spec["upgrade"]["nxos"]["default"] = True + section = "options" params_spec[section] = {} @@ -946,31 +947,18 @@ def _merge_global_and_switch_configs(self, config) -> None: def _merge_defaults_to_switch_configs(self) -> None: """ For any items in config which are not set, apply the default - value from self.defaults. + value from params_spec (if a default value exists). """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable configs_to_merge = copy.copy(self.switch_configs) merged_configs = [] + merge = ParamsMergeDefaults(self.module) + merge.params_spec = self._build_params_spec() for switch_config in configs_to_merge: - # we need to rebuild default_config in this loop - # because merge_dicts modifies it in place - merged_config = self.merge_dicts( - copy.deepcopy(self.defaults), copy.deepcopy(switch_config) - ) - - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += ( - f"switch_config: {json.dumps(switch_config, indent=4, sort_keys=True)}" - ) - self.log_msg(msg) - - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += ( - f"merged_config: {json.dumps(merged_config, indent=4, sort_keys=True)}" - ) - self.log_msg(msg) - merged_configs.append(merged_config) + merge.parameters = switch_config + merge.commit() + merged_configs.append(merge.merged_parameters) self.switch_configs = copy.copy(merged_configs) def _validate_switch_configs(self) -> None: @@ -984,12 +972,7 @@ def _validate_switch_configs(self) -> None: """ method_name = inspect.stack()[0][3] validator = ParamsValidate(self.module) - if self.module.params.get("state") == "merged": - validator.params_spec = self._build_params_spec_for_merged_state() - if self.module.params.get("state") == "deleted": - validator.params_spec = self._build_params_spec_for_merged_state() - if self.module.params.get("state") == "query": - validator.params_spec = self._build_params_spec_for_query_state() + validator.params_spec = self._build_params_spec() for switch in self.switch_configs: validator.parameters = switch diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json index 7bebf20f3..854b6b58f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json @@ -157,5 +157,159 @@ "validate": true }, "state": "merged" + }, + "test_image_mgmt_upgrade_task_00040a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false} + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00041a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "upgrade": {"nxos": false} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00042a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "upgrade": {"epld": true} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00043a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "options": {} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00044a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "options": {"nxos": {"mode": "non_disruptive"}} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00045a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "options": {"nxos": {"bios_force": true}} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00046a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "options": {"epld": {"module": 27}} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00047a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "options": {"epld": {"golden": true}} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00048a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "options": {"reboot": {"config_reload": true}} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00049a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "options": {"reboot": {"write_erase": true}} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00050a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": true, + "options": {"package": {"install": true}} + } + ] + }, + "state": "merged" + }, + "test_image_mgmt_upgrade_task_00051a": { + "config": { + "switches": [ + { + "policy": "KR5M", + "ip_address": "172.22.150.102", + "policy_changed": false, + "options": {"package": {"uninstall": true}} + } + ] + }, + "state": "merged" } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 653d5e9a4..1793bb993 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -115,95 +115,6 @@ def test_image_mgmt_upgrade_task_00002(image_upgrade_task_bare) -> None: assert isinstance(instance, ImageUpgradeTask) -# This functionality is now in params_validator.py -# def test_image_mgmt_upgrade_task_00003(image_upgrade_task_bare) -> None: -# """ -# Function -# - __init__ - -# Test -# - fail_json is called because config.switches is not a list -# """ -# key = "test_image_mgmt_upgrade_task_00003a" - -# match = "ImageUpgradeTask.__init__: expected list type for " -# match += r"self.config\['switches'\]. got str" - -# mock_ansible_module = MockAnsibleModule() -# mock_ansible_module.params = load_playbook_config(key) -# with pytest.raises(AnsibleFailJson, match=match): -# instance = image_upgrade_task_bare(mock_ansible_module) -# assert isinstance(instance, ImageUpgradeTask) - -# This functionality is now in params_validator.py -# def test_image_mgmt_upgrade_task_00004(image_upgrade_task_bare) -> None: -# """ -# Function -# - __init__ - -# Test -# - fail_json is called because config.switches is empty -# """ -# key = "test_image_mgmt_upgrade_task_00004a" - -# match = "ImageUpgradeTask.__init__: missing list of switches " -# match += "in playbook config." - -# mock_ansible_module = MockAnsibleModule() -# mock_ansible_module.params = load_playbook_config(key) -# with pytest.raises(AnsibleFailJson, match=match): -# instance = image_upgrade_task_bare(mock_ansible_module) -# assert isinstance(instance, ImageUpgradeTask) - -# This functionality is now in params_validator.py -# def test_image_mgmt_upgrade_task_00005(image_upgrade_task_bare) -> None: -# """ -# Function -# - __init__ - -# Test -# - fail_json is called because mandatory keys are missing in -# one of the switch configs -# """ -# key = "test_image_mgmt_upgrade_task_00005a" - -# match = "ImageUpgradeTask.__init__: missing mandatory " -# match += r"key\(s\) in playbook switch config. expected " -# match += r"\{'ip_address'\}, got dict_keys\(\['foo'\]\)" - -# mock_ansible_module = MockAnsibleModule() -# mock_ansible_module.params = load_playbook_config(key) -# with pytest.raises(AnsibleFailJson, match=match): -# instance = image_upgrade_task_bare(mock_ansible_module) -# assert isinstance(instance, ImageUpgradeTask) - - -def test_image_mgmt_upgrade_task_00006(image_upgrade_task) -> None: - """ - Function - - _init_defaults - - Test - - defaults dictionary is initialized with expected keys, values - """ - instance = image_upgrade_task - instance._init_defaults() - assert isinstance(instance.defaults, dict) - assert instance.defaults["reboot"] is False - assert instance.defaults["stage"] is True - assert instance.defaults["validate"] is True - assert instance.defaults["upgrade"]["nxos"] is True - assert instance.defaults["upgrade"]["epld"] is False - assert instance.defaults["options"]["nxos"]["mode"] == "disruptive" - assert instance.defaults["options"]["nxos"]["bios_force"] is False - assert instance.defaults["options"]["epld"]["module"] == "ALL" - assert instance.defaults["options"]["epld"]["golden"] is False - assert instance.defaults["options"]["reboot"]["config_reload"] is False - assert instance.defaults["options"]["reboot"]["write_erase"] is False - assert instance.defaults["options"]["package"]["install"] is False - assert instance.defaults["options"]["package"]["uninstall"] is False - - def test_image_mgmt_upgrade_task_00020(monkeypatch, image_upgrade_task) -> None: """ Function @@ -325,7 +236,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) instance = image_upgrade_task_bare(mock_ansible_module) + instance.get_want() + switch_1 = instance.want[0] switch_2 = instance.want[1] assert switch_1.get("ip_address") == "1.1.1.1" @@ -361,27 +274,29 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert switch_2.get("validate") is True -def test_image_mgmt_upgrade_task_00040(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00040(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup - - instance.switch_configs list is initialized with one switch - config containing mandatory keys only, such that all optional - (default) keys are populated by _merge_defaults_to_switch_configs - (see ImageUpgradeTask._init_defaults) for the default values. + - playbook is initialized with one switch config containing mandatory + keys only, such that all optional (default) keys are populated by + _merge_defaults_to_switch_configs Test - instance.switch_configs contains expected default values """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00040a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - {"policy": "KR5M", "ip_address": "172.22.150.102", "policy_changed": False} - ] - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -397,9 +312,11 @@ def test_image_mgmt_upgrade_task_00040(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00041(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00041(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -412,19 +329,14 @@ def test_image_mgmt_upgrade_task_00041(image_upgrade_task) -> None: - instance.switch_configs contains expected non-default values """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00041a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "upgrade": {"nxos": False}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -440,9 +352,11 @@ def test_image_mgmt_upgrade_task_00041(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00042(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00042(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -455,19 +369,14 @@ def test_image_mgmt_upgrade_task_00042(image_upgrade_task) -> None: - instance.switch_configs contains expected non-default values """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00042a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "upgrade": {"epld": True}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -483,9 +392,11 @@ def test_image_mgmt_upgrade_task_00042(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00043(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00043(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -501,19 +412,14 @@ def test_image_mgmt_upgrade_task_00043(image_upgrade_task) -> None: Description When options is empty, the default values for all sub-options are added """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00043a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -529,9 +435,11 @@ def test_image_mgmt_upgrade_task_00043(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00044(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00044(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -548,19 +456,14 @@ def test_image_mgmt_upgrade_task_00044(image_upgrade_task) -> None: When options.nxos.mode is the only key present in options.nxos, options.nxos.bios_force sub-option should be added with default value. """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00044a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"nxos": {"mode": "non_disruptive"}}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -576,9 +479,11 @@ def test_image_mgmt_upgrade_task_00044(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00045(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00045(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -595,19 +500,14 @@ def test_image_mgmt_upgrade_task_00045(image_upgrade_task) -> None: When options.nxos.bios_force is the only key present in options.nxos, options.nxos.mode sub-option should be added with default value. """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00045a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"nxos": {"bios_force": True}}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -623,9 +523,11 @@ def test_image_mgmt_upgrade_task_00045(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00046(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00046(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -642,19 +544,14 @@ def test_image_mgmt_upgrade_task_00046(image_upgrade_task) -> None: When options.epld.module is the only key present in options.epld, options.epld.golden sub-option should be added with default value. """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00046a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"epld": {"module": 27}}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -662,7 +559,7 @@ def test_image_mgmt_upgrade_task_00046(image_upgrade_task) -> None: assert instance.switch_configs[0]["upgrade"]["epld"] is False assert instance.switch_configs[0]["options"]["nxos"]["mode"] == "disruptive" assert instance.switch_configs[0]["options"]["nxos"]["bios_force"] is False - assert instance.switch_configs[0]["options"]["epld"]["module"] == 27 + assert instance.switch_configs[0]["options"]["epld"]["module"] == "27" assert instance.switch_configs[0]["options"]["epld"]["golden"] is False assert instance.switch_configs[0]["options"]["reboot"]["config_reload"] is False assert instance.switch_configs[0]["options"]["reboot"]["write_erase"] is False @@ -670,9 +567,11 @@ def test_image_mgmt_upgrade_task_00046(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00047(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00047(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -689,19 +588,14 @@ def test_image_mgmt_upgrade_task_00047(image_upgrade_task) -> None: When options.epld.golden is the only key present in options.epld, options.epld.module sub-option should be added with default value. """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00047a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"epld": {"golden": True}}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -717,9 +611,11 @@ def test_image_mgmt_upgrade_task_00047(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00048(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00048(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -737,19 +633,14 @@ def test_image_mgmt_upgrade_task_00048(image_upgrade_task) -> None: When options.reboot.config_reload is the only key present in options.reboot, options.reboot.write_erase sub-option should be added with default value. """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00048a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"reboot": {"config_reload": True}}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -765,9 +656,11 @@ def test_image_mgmt_upgrade_task_00048(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00049(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00049(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -785,19 +678,14 @@ def test_image_mgmt_upgrade_task_00049(image_upgrade_task) -> None: When options.reboot.write_erase is the only key present in options.reboot, options.reboot.config_reload sub-option should be added with default value. """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00049a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"reboot": {"write_erase": True}}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -813,9 +701,11 @@ def test_image_mgmt_upgrade_task_00049(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00050(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00050(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -833,19 +723,14 @@ def test_image_mgmt_upgrade_task_00050(image_upgrade_task) -> None: When options.package.install is the only key present in options.package, options.package.uninstall sub-option should be added with default value. """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00050a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"package": {"install": True}}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True @@ -861,9 +746,11 @@ def test_image_mgmt_upgrade_task_00050(image_upgrade_task) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00051(image_upgrade_task) -> None: +def test_image_mgmt_upgrade_task_00051(image_upgrade_task_bare) -> None: """ Function + - get_want + - _merge_global_and_switch_configs - _merge_defaults_to_switch_configs Setup @@ -881,19 +768,14 @@ def test_image_mgmt_upgrade_task_00051(image_upgrade_task) -> None: When options.package.uninstall is the only key present in options.package, options.package.install sub-option should be added with default value. """ - instance = image_upgrade_task + key = "test_image_mgmt_upgrade_task_00051a" + + mock_ansible_module = MockAnsibleModule() + mock_ansible_module.params = load_playbook_config(key) + instance = image_upgrade_task_bare(mock_ansible_module) + + instance.get_want() - switch_configs = [ - { - "policy": "KR5M", - "ip_address": "172.22.150.102", - "policy_changed": False, - "options": {"package": {"uninstall": True}}, - } - ] - - instance.switch_configs = switch_configs - instance._merge_defaults_to_switch_configs() assert instance.switch_configs[0]["reboot"] is False assert instance.switch_configs[0]["stage"] is True assert instance.switch_configs[0]["validate"] is True From 2ca3fb40eab79e177857fbd1d7065a73a1750542 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 21 Dec 2023 08:11:58 -1000 Subject: [PATCH 161/300] Copy dcnm_inventory action plugin and modify for dcnm_image_upgrade --- plugins/action/dcnm_image_upgrade.py | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 plugins/action/dcnm_image_upgrade.py diff --git a/plugins/action/dcnm_image_upgrade.py b/plugins/action/dcnm_image_upgrade.py new file mode 100644 index 000000000..812a0d4e2 --- /dev/null +++ b/plugins/action/dcnm_image_upgrade.py @@ -0,0 +1,62 @@ +# Copyright (c) 2024 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 + +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) +from ansible.utils.display import Display + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + + connection = self._connection + persistent_connect_timeout = connection.get_option("persistent_connect_timeout") + persistent_command_timeout = connection.get_option("persistent_command_timeout") + + timeout = 1000 + + if (persistent_command_timeout < timeout or persistent_connect_timeout < timeout): + display.warning( + "PERSISTENT_COMMAND_TIMEOUT is %s" + % str(persistent_command_timeout), + self._play_context.remote_addr, + ) + display.warning( + "PERSISTENT_CONNECT_TIMEOUT is %s" + % str(persistent_connect_timeout), + self._play_context.remote_addr, + ) + msg = ( + "PERSISTENT_COMMAND_TIMEOUT and PERSISTENT_CONNECT_TIMEOUT" + ) + msg += " must be set to {0} seconds or higher when using dcnm_image_upgrade module.".format(timeout) + msg += " Current persistent_command_timeout setting:" + str( + persistent_command_timeout + ) + msg += " Current persistent_connect_timeout setting:" + str( + persistent_connect_timeout + ) + return {"failed": True, "msg": msg} + + if self._task.args.get('state') == 'merged': + display.warning("Upgrading switches can take a while. Please be patient...") + self.result = super(ActionModule, self).run(task_vars=task_vars) + return self.result From f0867359d399558f743ca2eb4c8c72857ba95186 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 21 Dec 2023 11:46:26 -1000 Subject: [PATCH 162/300] Appease pycodestyle pep8 issues in sanity run --- plugins/module_utils/common/params_merge_defaults.py | 11 +++++------ plugins/modules/dcnm_image_upgrade.py | 2 -- .../dcnm/dcnm_image_upgrade/image_upgrade_utils.py | 1 + 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index 8ce32fade..bc08a5337 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -23,6 +23,7 @@ from collections.abc import MutableMapping as Map from typing import Any, Dict + class ParamsMergeDefaults: """ Merge default parameters into parameters. @@ -45,7 +46,7 @@ def _build_properties(self): """ Container for the properties of this class. """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties = {} self.properties["params_spec"] = None self.properties["parameters"] = None @@ -76,14 +77,14 @@ def _merge_default_params( """ Merge default parameters into parameters. - Caller: + Caller: - commit() Return: - A modified copy of params where missing parameters are added if: 1. they are present in spec 2. they have a default value defined in spec """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable for spec_key, spec_value in spec.items(): @@ -121,7 +122,6 @@ def commit(self) -> None: self.properties["merged_parameters"] = self._merge_default_params(self.params_spec, self.parameters) - def log_msg(self, msg): """ Used for debugging. To enable, both debug and logfile properties @@ -196,7 +196,7 @@ def merged_parameters(self): def parameters(self): """ The parameters into which defaults are merged. - + The merge consists of adding any missing parameters (per a comparison with params_spec) and setting their value to the default value defined in params_spec. @@ -229,4 +229,3 @@ def params_spec(self, value): msg += f"Got type {type(value)}." self.ansible_module.fail_json(msg) self.properties["params_spec"] = value - diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 7497ea6cb..21fe364ce 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -483,7 +483,6 @@ def __init__(self, module): self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) - def get_have(self) -> None: """ Caller: main() @@ -793,7 +792,6 @@ def _build_params_spec_for_merged_state() -> Dict[str, Any]: params_spec["upgrade"]["nxos"]["type"] = "bool" params_spec["upgrade"]["nxos"]["default"] = True - section = "options" params_spec[section] = {} params_spec[section]["required"] = False diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index f53c4b787..ff0caf3ec 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -301,6 +301,7 @@ def responses_image_validate(key: str) -> Dict[str, str]: print(f"responses_image_validate: {key} : {response}") return response + def responses_switch_details(key: str) -> Dict[str, str]: """ Return SwitchDetails controller responses From bf09150a24909646638aef35d43a0e2dbfead245 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 21 Dec 2023 13:29:31 -1000 Subject: [PATCH 163/300] Fix pylint disallowed-name error in sanity tests --- .../module_utils/common/params_merge_defaults.py | 15 +++++++++------ plugins/module_utils/common/params_validate.py | 8 ++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index bc08a5337..4941eab50 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -33,6 +33,7 @@ class ParamsMergeDefaults: associated with them into parameters (if parameters is missing the corresponding key/value). """ + def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module @@ -71,9 +72,8 @@ def _build_reserved_params(self): self.reserved_params.add("preferred_type") def _merge_default_params( - self, - spec: Dict[str, Any], - params: Dict[str, Any]) -> Dict[str, Any]: + self, spec: Dict[str, Any], params: Dict[str, Any] + ) -> Dict[str, Any]: """ Merge default parameters into parameters. @@ -87,7 +87,6 @@ def _merge_default_params( method_name = inspect.stack()[0][3] # pylint: disable=unused-variable for spec_key, spec_value in spec.items(): - if spec_key in self.reserved_params: continue @@ -98,7 +97,9 @@ def _merge_default_params( continue if isinstance(spec_value, Map): - params[spec_key] = self._merge_default_params(spec_value, params[spec_key]) + params[spec_key] = self._merge_default_params( + spec_value, params[spec_key] + ) return copy.deepcopy(params) @@ -120,7 +121,9 @@ def commit(self) -> None: msg += "Cannot commit. parameters is None." self.ansible_module.fail_json(msg) - self.properties["merged_parameters"] = self._merge_default_params(self.params_spec, self.parameters) + self.properties["merged_parameters"] = self._merge_default_params( + self.params_spec, self.parameters + ) def log_msg(self, msg): """ diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 47e71b266..f5ef4ad1d 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -498,7 +498,7 @@ def _validate_ipv4_address(value: Any) -> Any: verify that value is an IPv4 address """ try: - _ = ipaddress.IPv4Address(value) + ipaddress.IPv4Address(value) return value except ipaddress.AddressValueError as err: raise ValueError(f"invalid IPv4 address: {err}") from err @@ -509,7 +509,7 @@ def _validate_ipv4_subnet(value: Any) -> Any: verify that value is an IPv4 network """ try: - _ = ipaddress.IPv4Network(value) + ipaddress.IPv4Network(value) return value except ipaddress.AddressValueError as err: raise ValueError(f"invalid IPv4 network: {err}") from err @@ -520,7 +520,7 @@ def _validate_ipv6_address(value: Any) -> Any: verify that value is an IPv6 address """ try: - _ = ipaddress.IPv6Address(value) + ipaddress.IPv6Address(value) return value except ipaddress.AddressValueError as err: raise ValueError(f"invalid IPv6 address: {err}") from err @@ -531,7 +531,7 @@ def _validate_ipv6_subnet(value: Any) -> Any: verify that value is an IPv6 network """ try: - _ = ipaddress.IPv6Network(value) + ipaddress.IPv6Network(value) return value except ipaddress.AddressValueError as err: raise ValueError(f"invalid IPv6 network: {err}") from err From eacfef48f9484609c7613f926a71945cf2f97efd Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 21 Dec 2023 15:53:29 -1000 Subject: [PATCH 164/300] Fixes for Undefined variable '__class__' pylint errors --- plugins/module_utils/common/params_validate.py | 2 +- plugins/module_utils/image_mgmt/image_upgrade_common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index f5ef4ad1d..63cf89c7a 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -75,7 +75,7 @@ class ParamsValidate: """ def __init__(self, ansible_module): - self.class_name = __class__.__name__ + self.class_name = self.__class__.__name__ self.ansible_module = ansible_module self.validation = validation self.file_handle = None diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index e986e5f50..013494149 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -35,7 +35,7 @@ def __init__(self, module): """ def __init__(self, module): - self.class_name = __class__.__name__ + self.class_name = self.__class__.__name__ self.method_name = inspect.stack()[0][3] self.module = module From 331a726fff8f89475cda7d56c3ad831e61e43382 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 21 Dec 2023 18:54:59 -1000 Subject: [PATCH 165/300] Remove pylint disable=consider-using-with which doesn't seem compatible with earlier pylint --- plugins/module_utils/common/params_merge_defaults.py | 4 +--- plugins/module_utils/common/params_validate.py | 4 +--- plugins/module_utils/image_mgmt/image_upgrade_common.py | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index 4941eab50..258e57e34 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -141,9 +141,7 @@ def log_msg(self, msg): return if self.file_handle is None: try: - # since we need self.file_handle open throughout this class - # we are disabling pylint R1732 - self.file_handle = open( # pylint: disable=consider-using-with + self.file_handle = open( f"{self.logfile}", "a+", encoding="UTF-8" ) except IOError as err: diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 63cf89c7a..5d553f04d 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -192,9 +192,7 @@ def log_msg(self, msg): return if self.file_handle is None: try: - # since we need self.file_handle open throughout this class - # we are disabling pylint R1732 - self.file_handle = open( # pylint: disable=consider-using-with + self.file_handle = open( f"{self.logfile}", "a+", encoding="UTF-8" ) except IOError as err: diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 013494149..b74f9627f 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -142,9 +142,7 @@ def log_msg(self, msg): return if self.fd is None: try: - # since we need self.fd open throughout several classes - # we are disabling pylint R1732 - self.fd = open( # pylint: disable=consider-using-with + self.fd = open( f"{self.logfile}", "a+", encoding="UTF-8" ) except IOError as err: From f6f78606e24b9c6eea62ad3f3f8d0c78315479f6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 22 Dec 2023 08:10:49 -1000 Subject: [PATCH 166/300] Add docs for dcnm_image_uupgrade --- README.md | 1 + docs/cisco.dcnm.dcnm_image_upgrade_module.rst | 1053 +++++++++++++++++ 2 files changed, 1054 insertions(+) create mode 100644 docs/cisco.dcnm.dcnm_image_upgrade_module.rst diff --git a/README.md b/README.md index 8f94dcbfd..e5837625f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Name | Description ### Modules Name | Description --- | --- +[cisco.dcnm.dcnm_image_upgrade](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_image_upgrade_module.rst)|Image management for Nexus switches [cisco.dcnm.dcnm_interface](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_interface_module.rst)|DCNM Ansible Module for managing interfaces. [cisco.dcnm.dcnm_inventory](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_inventory_module.rst)|Add and remove Switches from a DCNM managed VXLAN fabric. [cisco.dcnm.dcnm_links](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_links_module.rst)|DCNM ansible module for managing Links. diff --git a/docs/cisco.dcnm.dcnm_image_upgrade_module.rst b/docs/cisco.dcnm.dcnm_image_upgrade_module.rst new file mode 100644 index 000000000..4055f1b60 --- /dev/null +++ b/docs/cisco.dcnm.dcnm_image_upgrade_module.rst @@ -0,0 +1,1053 @@ +.. _cisco.dcnm.dcnm_image_upgrade_module: + + +***************************** +cisco.dcnm.dcnm_image_upgrade +***************************** + +**Image management for Nexus switches** + + +Version added: 0.9.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Stage, validate, upgrade images. +- Attach, detach, image policies. +- Query device issu details. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ dictionary + / required +
+
+ +
A dictionary containing the image policy configuration.
+
+
+ options + +
+ dictionary +
+
+ +
A dictionary containing options for each of the upgrade types
+
+
+ epld + +
+ dictionary +
+
+ +
A dictionary containing epld upgrade options
+
+
+ golden + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Enable (True) or disable (False) reverting to the golden EPLD image
+
+
+ module + +
+ string +
+
+ Default:
"ALL"
+
+
The switch module to upgrade
+
Choose between ALL, or integer values
+
+
+ nxos + +
+ dictionary +
+
+ +
A dictionary containing nxos upgrade options
+
+
+ bios_force + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Force BIOS upgrade
+
+
+ mode + +
+ string +
+
+ Default:
"distruptive"
+
+
nxos upgrade mode
+
Choose between distruptive, non_disruptive, force_non_disruptive
+
+
+ package + +
+ dictionary +
+
+ +
A dictionary containing package upgrade options
+
+
+ install + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Install the package
+
+
+ uninstall + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Uninstall the package
+
+
+ reboot + +
+ dictionary +
+
+ +
A dictionary containing reboot options
+
+
+ config_reload + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Reload the configuration
+
+
+ write_erase + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Erase the startup configuration
+
+
+ policy + +
+ string + / required +
+
+ +
Image policy name
+
+
+ reboot + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Reboot the switch after upgrade
+
+
+ stage + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes ←
  • +
+
+
Stage (True) or unstage (False) an image policy
+
+
+ switches + +
+ list + / elements=dictionary + / required +
+
+ +
A list of devices to attach the image policy to.
+
+
+ ip_address + +
+ string + / required +
+
+ +
The IP address of the device to which the policy will be attached.
+
+
+ options + +
+ dictionary +
+
+ +
A dictionary containing options for each of the upgrade types
+
+
+ epld + +
+ dictionary +
+
+ +
A dictionary containing epld upgrade options
+
+
+ golden + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Enable (True) or disable (False) reverting to the golden EPLD image
+
+
+ module + +
+ string +
+
+ Default:
"ALL"
+
+
The switch module to upgrade
+
Choose between ALL, or integer values
+
+
+ nxos + +
+ dictionary +
+
+ +
A dictionary containing nxos upgrade options
+
+
+ bios_force + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Force BIOS upgrade
+
+
+ mode + +
+ string +
+
+ Default:
"distruptive"
+
+
nxos upgrade mode
+
Choose between distruptive, non_disruptive, force_non_disruptive
+
+
+ package + +
+ dictionary +
+
+ +
A dictionary containing package upgrade options
+
+
+ install + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Install the package
+
+
+ uninstall + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Uninstall the package
+
+
+ reboot + +
+ dictionary +
+
+ +
A dictionary containing reboot options
+
+
+ config_reload + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Reload the configuration
+
+
+ write_erase + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Erase the startup configuration
+
+
+ policy + +
+ string + / required +
+
+ +
Image policy name
+
+
+ reboot + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Reboot the switch after upgrade
+
+
+ stage + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes ←
  • +
+
+
Stage (True) or unstage (False) an image policy
+
+
+ upgrade + +
+ dictionary +
+
+ +
A dictionary containing upgrade toggles for nxos and epld
+
+
+ epld + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Enable (True) or disable (False) EPLD upgrade
+
If upgrade.nxos is false, epld and packages cannot both be true
+
If epld is true, nxos_option must be disruptive
+
+
+ nxos + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes ←
  • +
+
+
Enable (True) or disable (False) image upgrade
+
+
+ validate + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes ←
  • +
+
+
Validate (True) or do not validate (False) the image
+
after staging
+
+
+ upgrade + +
+ dictionary +
+
+ +
A dictionary containing upgrade toggles for nxos and epld
+
+
+ epld + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Enable (True) or disable (False) EPLD upgrade
+
If upgrade.nxos is false, epld and packages cannot both be true
+
If epld is true, nxos_option must be disruptive
+
+
+ nxos + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes ←
  • +
+
+
Enable (True) or disable (False) image upgrade
+
+
+ validate + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes ←
  • +
+
+
Validate (True) or do not validate (False) the image
+
after staging
+
+
+ state + +
+ string +
+
+
    Choices: +
  • merged ←
  • +
  • deleted
  • +
  • query
  • +
+
+
The state of the feature or object after module completion.
+
merged, deleted, and query states are supported.
+
+
+ + + + +Examples +-------- + +.. code-block:: yaml + + # This module supports the following states: + # + # merged: + # Attach image policy to one or more devices. + # Stage image on one or more devices. + # Validate image on one or more devices. + # Upgrade image on one or more devices. + # + # query: + # Return ISSU details for one or more devices. + # + # deleted: + # Delete image policy from one or more devices + # + + # Attach image policy NR3F to two devices + # Stage and validate the image on two devices but do not upgrade + - name: stage/validate images + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + policy: NR3F + stage: true + validate: true + upgrade: + nxos: false + epld: false + switches: + - ip_address: 192.168.1.1 + - ip_address: 192.168.1.2 + + # Attach image policy NR1F to device 192.168.1.1 + # Attach image policy NR2F to device 192.168.1.2 + # Stage the image on device 192.168.1.1, but do not upgrade + # Stage the image and upgrade device 192.168.1.2 + - name: stage/upgrade devices + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + validate: false + stage: false + upgrade: + nxos: false + epld: false + options: + nxos: + type: disruptive + epld: + module: ALL + golden: false + switches: + - ip_address: 192.168.1.1 + policy: NR1F + stage: true + validate: true + upgrade: + nxos: true + epld: false + - ip_address: 192.168.1.2 + policy: NR2F + stage: true + validate: true + upgrade: + nxos: true + epld: true + options: + nxos: + type: disruptive + epld: + module: ALL + golden: false + + # Detach image policy NR3F from two devices + - name: stage/upgrade devices + cisco.dcnm.dcnm_image_upgrade: + state: deleted + config: + policy: NR3F + switches: + - ip_address: 192.168.1.1 + - ip_address: 192.168.1.2 + + # Query ISSU details for three devices + - name: query switch ISSU status + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + policy: KMR5 + switches: + - ip_address: 192.168.1.1 + policy: OR1F + - ip_address: 192.168.1.2 + policy: NR2F + - ip_address: 192.168.1.3 # will query policy KMR5 + register: result + - name: print result + ansible.builtin.debug: + var: result + + + + +Status +------ + + +Authors +~~~~~~~ + +- Allen Robel (@quantumonion) From 89d0809aa8da3d176f79ee231fefdd9fe8f82ca2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 22 Dec 2023 09:38:11 -1000 Subject: [PATCH 167/300] Fix typo in validate() fail_json message, add related unit tests --- .../module_utils/common/params_validate.py | 2 +- .../test_image_upgrade_params_validate.py | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 5d553f04d..bcc6ac9eb 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -211,7 +211,7 @@ def validate(self) -> None: method_name = inspect.stack()[0][3] if self.parameters is None: msg = f"{self.class_name}.{method_name}: " - msg += "instance.paramaters needs to be set " + msg += "instance.parameters needs to be set " msg += "prior to calling instance.validate()." self.ansible_module.fail_json(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index 653428ca4..9072bc832 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -181,6 +181,52 @@ def test_params_validate_00031(params_validate) -> None: instance.parameters = [1, 2, 3] +def test_params_validate_00040(params_validate) -> None: + """ + Function + - validate + + Test + - validate calls fail_json when parameters is None + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + + match = "ParamsValidate.validate: " + match += "instance.parameters needs to be set prior to calling " + match += r"instance.validate\(\)\." + + with does_not_raise(): + instance = params_validate + instance.params_spec = params_spec + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + +def test_params_validate_00041(params_validate) -> None: + """ + Function + - validate + + Test + - validate calls fail_json when params_spec is None + """ + parameters = {} + parameters["foo"] = "bar" + + match = "ParamsValidate.validate: " + match += "instance.params_spec needs to be set prior to calling " + match += r"instance.validate\(\)\." + + with does_not_raise(): + instance = params_validate + instance.parameters = parameters + with pytest.raises(AnsibleFailJson, match=match): + instance.validate() + + def test_params_validate_00050(params_validate) -> None: """ Function From 3ad6a639c3cb42c31d860fa6d072ff305fc0fe8a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 22 Dec 2023 15:04:57 -1000 Subject: [PATCH 168/300] Use a common class for log messages --- plugins/module_utils/common/log.py | 93 +++++++++++++++++++ .../common/params_merge_defaults.py | 45 +++------ .../module_utils/common/params_validate.py | 48 +++------- .../image_mgmt/image_policy_action.py | 4 +- .../module_utils/image_mgmt/image_stage.py | 1 - .../module_utils/image_mgmt/image_upgrade.py | 16 ++-- .../image_mgmt/image_upgrade_common.py | 37 +++----- plugins/modules/dcnm_image_upgrade.py | 58 ++++++------ ...test_image_upgrade_image_upgrade_common.py | 35 ++++--- .../test_image_upgrade_params_validate.py | 17 ++-- 10 files changed, 190 insertions(+), 164 deletions(-) create mode 100644 plugins/module_utils/common/log.py diff --git a/plugins/module_utils/common/log.py b/plugins/module_utils/common/log.py new file mode 100644 index 000000000..90eb167ee --- /dev/null +++ b/plugins/module_utils/common/log.py @@ -0,0 +1,93 @@ +# Copyright (c) 2024 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 +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect + + +class Log: + """ + A logging utility for use with Ansible modules. + + Log messages to a file. + + Usage: + + instance = Log(ansible_module) + instance.debug = True + instance.logfile = "/tmp/params_validate.log" + instance.log_msg("some message") + """ + + def __init__(self, ansible_module): + self.class_name = self.__class__.__name__ + self.ansible_module = ansible_module + + self._build_properties() + + def _build_properties(self) -> None: + self.properties = {} + self.properties["debug"] = False + self.properties["logfile"] = None + + def log_msg(self, msg): + """ + Open the logfile and write the message to it. + + Call fail_json() if there is an error writing to the logfile. + """ + if self.debug is False: + return + if self.logfile is None: + return + try: + with open(f"{self.logfile}", "a+", encoding="UTF-8") as file_handle: + file_handle.write(f"{msg}\n") + except IOError as err: + msg = f"error writing to logfile {self.logfile}. " + msg += f"detail: {err}" + self.ansible_module.fail_json(msg) + + @property + def debug(self): + """ + Enable/disable debugging to self.logfile + """ + return self.properties["debug"] + + @debug.setter + def debug(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid type for debug. Expected bool. " + msg += f"Got {type(value)}." + self.ansible_module.fail_json(msg) + self.properties["debug"] = value + + @property + def logfile(self): + """ + Set file to which messages are written + """ + return self.properties["logfile"] + + @logfile.setter + def logfile(self, value): + self.properties["logfile"] = value diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index 258e57e34..ca1497416 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -23,6 +23,9 @@ from collections.abc import MutableMapping as Map from typing import Any, Dict +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ + Log + class ParamsMergeDefaults: """ @@ -37,7 +40,10 @@ class ParamsMergeDefaults: def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module - self.file_handle = None + + self.log = Log(self.ansible_module) + self.log.debug = False + self.log.logfile = None self._build_properties() self._build_reserved_params() @@ -125,40 +131,12 @@ def commit(self) -> None: self.params_spec, self.parameters ) - def log_msg(self, msg): - """ - Used for debugging. To enable, both debug and logfile properties - must be set e.g.: - - instance = ParamsValidate(ansible_module) - instance.debug = True - instance.logfile = "/tmp/params_validate.log" - etc... - """ - if self.debug is False: - return - if self.logfile is None: - return - if self.file_handle is None: - try: - self.file_handle = open( - f"{self.logfile}", "a+", encoding="UTF-8" - ) - except IOError as err: - msg = f"error opening logfile {self.logfile}. " - msg += f"detail: {err}" - self.ansible_module.fail_json(msg) - - self.file_handle.write(msg) - self.file_handle.write("\n") - self.file_handle.flush() - @property def debug(self): """ Enable/disable debugging to self.logfile """ - return self.properties["debug"] + return self.log.debug @debug.setter def debug(self, value): @@ -168,19 +146,18 @@ def debug(self, value): msg += "Invalid type for debug. Expected bool. " msg += f"Got {type(value)}." self.ansible_module.fail_json(msg) - self.properties["debug"] = value + self.log.debug = value @property def logfile(self): """ Set file to which debug log is written """ - return self.properties["logfile"] + return self.log.logfile @logfile.setter def logfile(self, value): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.properties["logfile"] = value + self.log.logfile = value @property def merged_parameters(self): diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index bcc6ac9eb..661fd42a1 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -24,6 +24,8 @@ from typing import Any, List from ansible.module_utils.common import validation +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ + Log class ParamsValidate: @@ -78,7 +80,10 @@ def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module self.validation = validation - self.file_handle = None + + self.log = Log(self.ansible_module) + self.log.debug = False + self.log.logfile = None self._build_properties() self._build_reserved_params() @@ -95,8 +100,6 @@ def _build_properties(self): self.properties = {} self.properties["parameters"] = None self.properties["params_spec"] = None - self.properties["debug"] = False - self.properties["logfile"] = None def _build_reserved_params(self): """ @@ -176,34 +179,6 @@ def _build_validations(self): self.validations["ipv4_subnet"] = self._validate_ipv4_subnet self.validations["ipv6_subnet"] = self._validate_ipv6_subnet - def log_msg(self, msg): - """ - Used for debugging. To enable, both debug and logfile properties - must be set e.g.: - - instance = ParamsValidate(ansible_module) - instance.debug = True - instance.logfile = "/tmp/params_validate.log" - etc... - """ - if self.debug is False: - return - if self.logfile is None: - return - if self.file_handle is None: - try: - self.file_handle = open( - f"{self.logfile}", "a+", encoding="UTF-8" - ) - except IOError as err: - msg = f"error opening logfile {self.logfile}. " - msg += f"detail: {err}" - self.ansible_module.fail_json(msg) - - self.file_handle.write(msg) - self.file_handle.write("\n") - self.file_handle.flush() - def validate(self) -> None: """ Verify that parameters in self.parameters conform to self.params_spec @@ -590,9 +565,9 @@ def _verify_expected_type(self, expected_type: str, param: str) -> None: @property def debug(self): """ - Enable/disable debugging to self.logfile + Enable/disable debugging to self.log.logfile """ - return self.properties["debug"] + return self.log.debug @debug.setter def debug(self, value): @@ -602,19 +577,18 @@ def debug(self, value): msg += "Invalid type for debug. Expected bool. " msg += f"Got {type(value)}." self.ansible_module.fail_json(msg) - self.properties["debug"] = value + self.log.debug = value @property def logfile(self): """ Set file to which debug log is written """ - return self.properties["logfile"] + return self.log.logfile @logfile.setter def logfile(self, value): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.properties["logfile"] = value + self.log.logfile = value @property def parameters(self): diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 85e398c94..2ec927f99 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -205,7 +205,7 @@ def _attach_policy(self): msg = f"{self.class_name}.{method_name}: " msg += f"response: {json.dumps(response, indent=4)}" - self.log_msg(msg) + self.log.log_msg(msg) if not result["success"]: msg = f"{self.class_name}.{method_name}: " @@ -239,7 +239,7 @@ def _detach_policy(self): msg = f"{self.class_name}.{method_name}: " msg += f"response: {json.dumps(self.response, indent=4)}" - self.log_msg(msg) + self.log.log_msg(msg) if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 57eea0242..12a5907d6 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -110,7 +110,6 @@ def __init__(self, module): self.verb = None self.payload = None self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) - self.log_msg("DEBUG: ImageStage.__init__ DONE") def _init_properties(self): method_name = inspect.stack()[0][3] # pylint: disable=unused-variable diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index c01135460..c93361840 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -145,7 +145,7 @@ def __init__(self, module): self._init_properties() self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) self.install_options = ImageInstallOptions(self.module) - self.log_msg("DEBUG: ImageUpgrade.__init__ DONE") + self.log.log_msg("DEBUG: ImageUpgrade.__init__ DONE") def _init_properties(self) -> None: """ @@ -221,7 +221,7 @@ def _build_payload(self, device) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"device FINAL: {json.dumps(device, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() @@ -258,7 +258,7 @@ def _build_payload(self, device) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) def _build_payload_issu_upgrade(self, device) -> None: """ @@ -456,7 +456,7 @@ def commit(self) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"self.devices: {json.dumps(self.devices, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) if self.devices is None: msg = f"{self.class_name}.{method_name}: " @@ -471,18 +471,18 @@ def commit(self) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"self.verb {self.verb}, self.path: {self.path}" - self.log_msg(msg) + self.log.log_msg(msg) for device in self.devices: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"device: {json.dumps(device, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) self._build_payload(device) msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) @@ -493,7 +493,7 @@ def commit(self) -> None: msg += ( f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" ) - self.log_msg(msg) + self.log.log_msg(msg) if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index b74f9627f..1e94aba72 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -20,6 +20,8 @@ import inspect from collections.abc import MutableMapping as Map +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ + Log class ImageUpgradeCommon: @@ -40,11 +42,16 @@ def __init__(self, module): self.module = module self.params = module.params - self.debug = False - self.fd = None - self.logfile = "/tmp/ansible_dcnm.log" + + self.log = Log(module) + self.log.debug = False + self.log.logfile = "/tmp/dcnm_image_upgrade.log" + + # self.debug = False + # self.fd = None + # self.logfile = "/tmp/ansible_dcnm.log" self.module = module - self.log_msg("ImageUpgradeCommon.__init__ DONE") + self.log.log_msg("ImageUpgradeCommon.__init__ DONE") def _handle_response(self, response, verb): # don't add self.method_name to this method since @@ -133,27 +140,6 @@ def _handle_post_put_delete_response(self, response): result["changed"] = True return result - def log_msg(self, msg): - """ - used for debugging. disable this when committing to main - by setting self.debug to False in __init__() - """ - if self.debug is False: - return - if self.fd is None: - try: - self.fd = open( - f"{self.logfile}", "a+", encoding="UTF-8" - ) - except IOError as err: - msg = f"error opening logfile {self.logfile}. " - msg += f"detail: {err}" - self.module.fail_json(msg) - - self.fd.write(msg) - self.fd.write("\n") - self.fd.flush() - def make_boolean(self, value): """ Return value converted to boolean, if possible. @@ -188,7 +174,6 @@ def merge_dicts(self, dict1, dict2): Keys in dict2 have precedence over keys in dict1. """ for key in dict2: - # self.log_msg(f"DEBUG: {self.class_name}.merge_dicts: key: {key}") if isinstance(dict1.get(key, None), Map) and dict2.get(key, None) is None: # This is to handle a case where the playbook contains an diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 21fe364ce..44ee18f5c 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -451,6 +451,7 @@ def __init__(self, module): method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() + self.have = None self.idempotent_want = None # populated in self._merge_global_and_switch_configs() @@ -505,7 +506,7 @@ def get_want(self) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "Calling _merge_global_and_switch_configs with " msg += f"self.config: {json.dumps(self.config, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) self._merge_global_and_switch_configs(self.config) @@ -514,7 +515,7 @@ def get_want(self) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "Calling _validate_switch_configs with self.switch_configs: " msg += f"{json.dumps(self.switch_configs, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) self._validate_switch_configs() @@ -522,11 +523,11 @@ def get_want(self) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"self.want: {json.dumps(self.want, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) if len(self.want) == 0: self.result["changed"] = False - self.exit_json(**self.result) + self.module.exit_json(**self.result) def _build_idempotent_want(self, want) -> None: """ @@ -570,7 +571,7 @@ def _build_idempotent_want(self, want) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"want: {json.dumps(want, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) self.have.ip_address = want["ip_address"] @@ -629,12 +630,12 @@ def _build_idempotent_want(self, want) -> None: msg += "Calling ImageInstallOptions.response " msg += "instance.response: " msg += f"{json.dumps(instance.response_data, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "self.idempotent_want PRE EPLD CHECK: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) # if InstallOptions indicates that EPLD is already upgraded, # don't upgrade it again. @@ -644,7 +645,7 @@ def _build_idempotent_want(self, want) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "self.idempotent_want POST EPLD CHECK: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) def get_need_merged(self) -> None: """ @@ -660,14 +661,14 @@ def get_need_merged(self) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "self.want: " msg += f"{json.dumps(self.want, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) for want in self.want: self.have.ip_address = want["ip_address"] msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"self.have.serial_number: {self.have.serial_number}" - self.log_msg(msg) + self.log.log_msg(msg) if self.have.serial_number is not None: self._build_idempotent_want(want) @@ -675,7 +676,7 @@ def get_need_merged(self) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "self.idempotent_want: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) test_idempotence = set() test_idempotence.add(self.idempotent_want["policy_changed"]) @@ -737,14 +738,13 @@ def _build_params_spec(self) -> Dict[str, Any]: method_name = inspect.stack()[0][3] # pylint: disable=unused-variable if self.module.params["state"] == "merged": return self._build_params_spec_for_merged_state() - elif self.module.params["state"] == "deleted": + if self.module.params["state"] == "deleted": return self._build_params_spec_for_merged_state() - elif self.module.params["state"] == "query": + if self.module.params["state"] == "query": return self._build_params_spec_for_query_state() - else: - msg = f"{self.class_name}.{method_name}: " - msg += f"Unsupported state: {self.module.params['state']}" - self.module.fail_json(msg) + msg = f"{self.class_name}.{method_name}: " + msg += f"Unsupported state: {self.module.params['state']}" + self.module.fail_json(msg) @staticmethod def _build_params_spec_for_merged_state() -> Dict[str, Any]: @@ -927,17 +927,17 @@ def _merge_global_and_switch_configs(self, config) -> None: msg += ( f"global_config: {json.dumps(global_config, indent=4, sort_keys=True)}" ) - self.log_msg(msg) + self.log.log_msg(msg) msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"switch PRE_MERGE : {json.dumps(switch, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) switch_config = self.merge_dicts(global_config, switch) msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) merged_configs.append(switch_config) self.switch_configs = copy.copy(merged_configs) @@ -968,7 +968,7 @@ def _validate_switch_configs(self) -> None: Callers: - self.get_want """ - method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable validator = ParamsValidate(self.module) validator.params_spec = self._build_params_spec() @@ -1074,7 +1074,7 @@ def _stage_images(self, serial_numbers) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"serial_numbers: {serial_numbers}" - self.log_msg(msg) + self.log.log_msg(msg) instance = ImageStage(self.module) instance.serial_numbers = serial_numbers @@ -1091,7 +1091,7 @@ def _validate_images(self, serial_numbers) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"serial_numbers: {serial_numbers}" - self.log_msg(msg) + self.log.log_msg(msg) instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers @@ -1143,7 +1143,7 @@ def _verify_install_options(self, devices) -> None: for device in verify_devices: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"device: {json.dumps(device, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) self.switch_details.ip_address = device.get("ip_address") install_options.serial_number = self.switch_details.serial_number @@ -1157,7 +1157,7 @@ def _verify_install_options(self, devices) -> None: msg += ( f"{json.dumps(install_options.response_data, indent=4, sort_keys=True)}" ) - self.log_msg(msg) + self.log.log_msg(msg) if ( install_options.status not in ["Success", "Skipped"] @@ -1172,14 +1172,14 @@ def _verify_install_options(self, devices) -> None: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"install_options.epld: {install_options.epld}" - self.log_msg(msg) + self.log.log_msg(msg) msg = f"DEBUG: {self.class_name}.{method_name}: " msg += "install_options.epld_modules: " msg += ( f"{json.dumps(install_options.epld_modules, indent=4, sort_keys=True)}" ) - self.log_msg(msg) + self.log.log_msg(msg) if install_options.epld_modules is None and install_options.epld is True: msg = f"{self.class_name}.{method_name}: " @@ -1221,7 +1221,7 @@ def needs_epld_upgrade(self, epld_modules) -> bool: msg += f"(module: {module.get('moduleType')}), " msg += f"new_version {new_version} > old_version {old_version}, " msg += "returning True" - self.log_msg(msg) + self.log.log_msg(msg) return True return False @@ -1261,7 +1261,7 @@ def handle_merged_state(self) -> None: for switch in self.need: msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"switch: {json.dumps(switch, indent=4, sort_keys=True)}" - self.log_msg(msg) + self.log.log_msg(msg) self.switch_details.ip_address = switch.get("ip_address") device = {} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 7e65b3b6f..553833467 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -57,9 +57,8 @@ def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: with does_not_raise(): instance = image_upgrade_common assert instance.params == test_params - assert instance.debug is False - assert instance.fd is None - assert instance.logfile == "/tmp/ansible_dcnm.log" + assert instance.log.debug is False + assert instance.log.logfile == "/tmp/dcnm_image_upgrade.log" @pytest.mark.parametrize( @@ -403,25 +402,25 @@ def test_image_mgmt_image_upgrade_common_00100( def test_image_mgmt_image_upgrade_common_00110(image_upgrade_common) -> None: """ Function - - log_msg + - log.log_msg Test - - log_msg returns None when debug is False + - log.log_msg returns None when debug is False """ instance = image_upgrade_common error_message = "This is an error message" - instance.debug = False - assert instance.log_msg(error_message) is None + instance.log.debug = False + assert instance.log.log_msg(error_message) is None def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) -> None: """ Function - - log_msg + - log.log_msg Test - - log_msg writes to the logfile when debug is True + - log_msg writes to the log.logfile when log.debug is True """ instance = image_upgrade_common @@ -430,9 +429,9 @@ def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) - filename = directory / "test_log_msg.txt" error_message = "This is an error message" - instance.debug = True - instance.logfile = filename - instance.log_msg(error_message) + instance.log.debug = True + instance.log.logfile = filename + instance.log.log_msg(error_message) assert filename.read_text(encoding="UTF-8") == error_message + "\n" assert len(list(tmp_path.iterdir())) == 1 @@ -441,10 +440,10 @@ def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) - def test_image_mgmt_image_upgrade_common_00112(tmp_path, image_upgrade_common) -> None: """ Function - - log_msg + - log.log_msg Test - - log_msg calls fail_json if the logfile cannot be opened + - log.log_msg calls fail_json if the logfile cannot be opened Description To ensure an error is generated, we attempt a write to a filename @@ -457,7 +456,7 @@ def test_image_mgmt_image_upgrade_common_00112(tmp_path, image_upgrade_common) - filename = directory / f"test_{'a' * 2000}_log_msg.txt" error_message = "This is an error message" - instance.debug = True - instance.logfile = filename - with pytest.raises(AnsibleFailJson, match=r"error opening logfile"): - instance.log_msg(error_message) + instance.log.debug = True + instance.log.logfile = filename + with pytest.raises(AnsibleFailJson, match="error writing to logfile"): + instance.log.log_msg(error_message) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py index 9072bc832..3b59e0a04 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py @@ -79,9 +79,8 @@ def test_params_validate_00001(params_validate) -> None: } assert instance.mandatory_param_spec_keys == {"required", "type"} assert instance.class_name == "ParamsValidate" - assert instance.file_handle is None - assert instance.properties.get("debug", None) is False - assert instance.properties.get("logfile", "foo") is None + assert instance.log.logfile is None + assert instance.log.debug is False assert instance.properties.get("parameters", "foo") is None assert instance.properties.get("params_spec", "foo") is None @@ -885,16 +884,16 @@ def test_params_validate_00093(params_validate) -> None: def test_params_validate_00110(params_validate) -> None: """ Function - - log_msg + - log.log_msg Test - - log_msg returns None when debug is False + - log.log_msg returns None when debug is False """ instance = params_validate error_message = "This is an error message" instance.debug = False - assert instance.log_msg(error_message) is None + assert instance.log.log_msg(error_message) is None def test_params_validate_00111(tmp_path, params_validate) -> None: @@ -914,7 +913,7 @@ def test_params_validate_00111(tmp_path, params_validate) -> None: error_message = "This is an error message" instance.debug = True instance.logfile = filename - instance.log_msg(error_message) + instance.log.log_msg(error_message) assert filename.read_text(encoding="UTF-8") == error_message + "\n" assert len(list(tmp_path.iterdir())) == 1 @@ -941,5 +940,5 @@ def test_params_validate_00112(tmp_path, params_validate) -> None: error_message = "This is an error message" instance.debug = True instance.logfile = filename - with pytest.raises(AnsibleFailJson, match=r"error opening logfile"): - instance.log_msg(error_message) + with pytest.raises(AnsibleFailJson, match="error writing to logfile"): + instance.log.log_msg(error_message) From 0ffad6779d938b043ba41f0e33af2c0ce8fba356 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 22 Dec 2023 16:49:38 -1000 Subject: [PATCH 169/300] Move merge_dicts() to a common class MergeDicts, more... Move merge_dicts() out of ImageUpgradeCommon() and into its own class that can be shared with other modules. ImageUpgradeTask now leverages MergeDicts() Also: - ImageUpgradeTask(): remove unused self.mandatory_global_keys - ImageUpgradeCommon(): remove self.method_name from a couple functions (these should have been non-global assignments and were obscuring the method names of functions that called these). - ImageValidate: Update unit tests to expect the proper method names (per above change) - test_image_mgmt_upgrade_task_00001: remove verifications for items no longer present in __init__. - Cleanup a few comments here and there. --- plugins/module_utils/common/merge_dicts.py | 191 ++++++++++++++++++ .../image_mgmt/image_upgrade_common.py | 63 +----- plugins/modules/dcnm_image_upgrade.py | 10 +- .../test_image_upgrade_image_upgrade_task.py | 2 - .../test_image_upgrade_image_validate.py | 2 +- 5 files changed, 205 insertions(+), 63 deletions(-) create mode 100644 plugins/module_utils/common/merge_dicts.py diff --git a/plugins/module_utils/common/merge_dicts.py b/plugins/module_utils/common/merge_dicts.py new file mode 100644 index 000000000..ec7b12a3b --- /dev/null +++ b/plugins/module_utils/common/merge_dicts.py @@ -0,0 +1,191 @@ +# Copyright (c) 2024 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 +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect +from collections.abc import MutableMapping as Map +from typing import Any, Dict + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log + + +class MergeDicts: + """ + Merge two dictionaries. + + Given two dictionaries, dict1 and dict2, merge them into a + single dictionary, dict_merged, where keys in dict2 have + precedence over (will overwrite) keys in dict1. + + Example: + + module = AnsibleModule(...) + instance = MergeDicts(module) + instance.dict1 = { "foo": 1, "bar": 2 } + instance.dict2 = { "foo": 3, "baz": 4 } + instance.commit() + dict_merged = instance.dict_merged + print(dict_merged) + + Output: + { foo: 3, bar: 2, baz: 4 } + """ + + def __init__(self, ansible_module): + self.class_name = self.__class__.__name__ + self.ansible_module = ansible_module + + self.log = Log(self.ansible_module) + self.log.debug = False + self.log.logfile = None + + self._build_properties() + + def _build_properties(self) -> None: + self.properties = {} + self.properties["dict1"] = None + self.properties["dict2"] = None + self.properties["dict_merged"] = None + + def commit(self) -> None: + """ + Commit the merged dict. + """ + method_name = inspect.stack()[0][3] + if self.dict1 is None or self.dict2 is None: + msg = f"{self.class_name}.{method_name}: " + msg += "dict1 and dict2 must be set before calling commit()" + self.ansible_module.fail_json(msg) + + self.properties["dict_merged"] = self.merge_dicts(self.dict1, self.dict2) + + def merge_dicts( + self, dict1: Dict[Any, Any], dict2: Dict[Any, Any] + ) -> Dict[Any, Any]: + """ + Merge dict2 into dict1 and return dict1. + Keys in dict2 have precedence over keys in dict1. + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + for key in dict2: + if isinstance(dict1.get(key, None), Map) and dict2.get(key, None) is None: + # This is to handle a case where dict1 contains a key that is a + # dict, which is supposed to contain sub-options, but the dict + # is empty. + # + # For example, below, upgrade is specified, but upgrade.nxos + # and upgrade.epld are not. In this case, we copy the entire + # 'upgrade' dict from dict1 to dict2: + # + # - ip_address: 172.22.150.110 + # upgrade: + # options: + # epld: + # module: 27 + # golden: false + dict2[key] = dict1[key] + elif ( + key in dict1 + and isinstance(dict1[key], Map) + and isinstance(dict2[key], Map) + ): + self.merge_dicts(dict1[key], dict2[key]) + else: + dict1[key] = dict2[key] + return copy.deepcopy(dict1) + + @property + def debug(self): + """ + Enable/disable debugging to self.logfile + """ + return self.log.debug + + @debug.setter + def debug(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid type for debug. Expected bool. " + msg += f"Got {type(value)}." + self.ansible_module.fail_json(msg) + self.log.debug = value + + @property + def logfile(self): + """ + Set file to which debug log is written + """ + return self.log.logfile + + @logfile.setter + def logfile(self, value): + self.log.logfile = value + + @property + def dict_merged(self): + """ + Getter for the merged dictionary. + """ + method_name = inspect.stack()[0][3] + if self.properties["dict_merged"] is None: + msg = f"{self.class_name}.{method_name}: " + msg += "Call instance.commit() before calling " + msg += f"instance.{method_name}." + self.ansible_module.fail_json(msg) + return self.properties["dict_merged"] + + @property + def dict1(self): + """ + The dictionary into which dict2 will be merged. + + dict1's keys will be overwritten by dict2's keys. + """ + return self.properties["dict1"] + + @dict1.setter + def dict1(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid parameters. Expected type dict. " + msg += f"Got type {type(value)}." + self.ansible_module.fail_json(msg) + self.properties["dict1"] = copy.deepcopy(value) + + @property + def dict2(self): + """ + The dictionary which will be merged into dict1. + + dict2's keys will overwrite by dict1's keys. + """ + return self.properties["dict2"] + + @dict2.setter + def dict2(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid parameters. Expected type dict. " + msg += f"Got type {type(value)}." + self.ansible_module.fail_json(msg) + self.properties["dict2"] = copy.deepcopy(value) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 1e94aba72..4dbe8ea78 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -19,14 +19,13 @@ __author__ = "Allen Robel" import inspect -from collections.abc import MutableMapping as Map from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ Log class ImageUpgradeCommon: """ - Base class for the other image upgrade classes + Common methods used by the other image upgrade classes Usage (where module is an instance of AnsibleModule): @@ -38,7 +37,6 @@ def __init__(self, module): def __init__(self, module): self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] self.module = module self.params = module.params @@ -47,17 +45,13 @@ def __init__(self, module): self.log.debug = False self.log.logfile = "/tmp/dcnm_image_upgrade.log" - # self.debug = False - # self.fd = None - # self.logfile = "/tmp/ansible_dcnm.log" self.module = module self.log.log_msg("ImageUpgradeCommon.__init__ DONE") def _handle_response(self, response, verb): - # don't add self.method_name to this method since - # it is called by other methods and we want their - # method_names in the log - + """ + Call the appropriate handler for response based on verb + """ if verb == "GET": return self._handle_get_response(response) if verb in {"POST", "PUT", "DELETE"}: @@ -65,9 +59,9 @@ def _handle_response(self, response, verb): return self._handle_unknown_request_verbs(response, verb) def _handle_unknown_request_verbs(self, response, verb): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"Unknown request verb ({verb}) for response {response}." self.module.fail_json(msg) @@ -84,10 +78,6 @@ def _handle_get_response(self, response): - False if RETURN_CODE != 200 or MESSAGE != "OK" - True otherwise """ - # don't add self.method_name to this method since - # it is called by other methods and we want their - # method_names in the log - result = {} success_return_codes = {200, 404} if ( @@ -123,10 +113,6 @@ def _handle_post_put_delete_response(self, response): - False if RETURN_CODE != 200 or MESSAGE != "OK" - True otherwise """ - # don't add self.method_name to this method since - # it is called by other methods and we want their - # method_names in the log - result = {} if response.get("ERROR") is not None: result["success"] = False @@ -145,8 +131,6 @@ def make_boolean(self, value): Return value converted to boolean, if possible. Return value, if value cannot be converted. """ - self.method_name = inspect.stack()[0][3] - if isinstance(value, bool): return value if isinstance(value, str): @@ -162,41 +146,6 @@ def make_none(self, value): representation of a None type Return value otherwise """ - self.method_name = inspect.stack()[0][3] - if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: return None return value - - def merge_dicts(self, dict1, dict2): - """ - Merge dict2 into dict1 and return dict1. - Keys in dict2 have precedence over keys in dict1. - """ - for key in dict2: - - if isinstance(dict1.get(key, None), Map) and dict2.get(key, None) is None: - # This is to handle a case where the playbook contains an - # options dict that is supposed to contain sub-options - # (in the example below, 'upgrade' should contain 'nxos' and - # 'epld'), but the dict is empty in the playbook. - # For example, below, upgrade is specified, but upgrade.nxos - # and upgrade.epld are not: - # - # - ip_address: 172.22.150.110 - # upgrade: - # options: - # epld: - # module: 27 - # golden: false - # In this case, we copy the entire 'upgrade' dict from dict1 to dict2 - dict2[key] = dict1[key] - elif ( - key in dict1 - and isinstance(dict1[key], Map) - and isinstance(dict2[key], Map) - ): - self.merge_dicts(dict1[key], dict2[key]) - else: - dict1[key] = dict2[key] - return dict1 diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 44ee18f5c..be903c678 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -408,6 +408,8 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ ParamsMergeDefaults +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts \ + import MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -479,8 +481,6 @@ def __init__(self, module): self.result = {"changed": False, "diff": [], "response": []} - self.mandatory_global_keys = {"switches"} - self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) @@ -933,7 +933,11 @@ def _merge_global_and_switch_configs(self, config) -> None: msg += f"switch PRE_MERGE : {json.dumps(switch, indent=4, sort_keys=True)}" self.log.log_msg(msg) - switch_config = self.merge_dicts(global_config, switch) + merge_dicts = MergeDicts(self.module) + merge_dicts.dict1 = global_config + merge_dicts.dict2 = switch + merge_dicts.commit() + switch_config = merge_dicts.dict_merged msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 1793bb993..089e13b46 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -91,8 +91,6 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: assert instance.want == [] assert instance.need == [] assert instance.result == {"changed": False, "diff": [], "response": []} - # assert instance.mandatory_global_keys == {"switches"} - # assert instance.mandatory_switch_keys == {"ip_address"} assert isinstance(instance.switch_details, SwitchDetails) assert isinstance(instance.image_policies, ImagePolicies) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index fe66420a5..1774118ab 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -564,7 +564,7 @@ def test_image_mgmt_validate_00030(image_validate, value, expected) -> None: instance.serial_numbers = value -MATCH_00040 = "ImageValidate.make_boolean: " +MATCH_00040 = "ImageValidate.non_disruptive: " MATCH_00040 += "instance.non_disruptive must be a boolean." From db529c763781d3eebe14e2f1420cb59e1b76cd48 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 23 Dec 2023 15:12:42 -1000 Subject: [PATCH 170/300] Move unit tests for common classes out of image_upgrade ControllerVersion and Log were initially written as specific to image_upgrade, and their unit tests lived in: tests/unit/modules/dcnm/dcnm_image_upgrade These classes have moved to plugins/module_utils/common and so we've moved their unit test scripts, fixtures, and other supporting utilities to: tests/unit/module_utils/common --- tests/unit/module_utils/__init__.py | 0 tests/unit/module_utils/common/__init__.py | 0 .../unit/module_utils/common/common_utils.py | 114 +++++ tests/unit/module_utils/common/fixture.py | 50 ++ .../fixtures/responses_ControllerVersion.json | 453 ++++++++++++++++++ .../common/test_controller_version.py} | 4 +- tests/unit/module_utils/common/test_log.py | 146 ++++++ .../common/test_params_validate.py} | 85 +--- ...e_upgrade_responses_ControllerVersion.json | 451 ----------------- 9 files changed, 767 insertions(+), 536 deletions(-) create mode 100644 tests/unit/module_utils/__init__.py create mode 100644 tests/unit/module_utils/common/__init__.py create mode 100644 tests/unit/module_utils/common/common_utils.py create mode 100644 tests/unit/module_utils/common/fixture.py create mode 100644 tests/unit/module_utils/common/fixtures/responses_ControllerVersion.json rename tests/unit/{modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py => module_utils/common/test_controller_version.py} (99%) create mode 100644 tests/unit/module_utils/common/test_log.py rename tests/unit/{modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py => module_utils/common/test_params_validate.py} (89%) diff --git a/tests/unit/module_utils/__init__.py b/tests/unit/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/module_utils/common/__init__.py b/tests/unit/module_utils/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py new file mode 100644 index 000000000..d234b812a --- /dev/null +++ b/tests/unit/module_utils/common/common_utils.py @@ -0,0 +1,114 @@ +# Copyright (c) 2024 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 + + +from contextlib import contextmanager +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ + ControllerVersion +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ + Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ + ParamsValidate + +from .fixture import load_fixture + + +class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + + params = {"config": {"switches": [{"ip_address": "172.22.150.105"}]}} + argument_spec = { + "config": {"required": True, "type": "dict"}, + "state": {"default": "merged", "choices": ["merged", "deleted", "query"]}, + } + supports_check_mode = True + + @staticmethod + def fail_json(msg) -> AnsibleFailJson: + """ + mock the fail_json method + """ + raise AnsibleFailJson(msg) + + def public_method_for_pylint(self) -> Any: + """ + Add one public method to appease pylint + """ + + +# See the following for explanation of why fixtures are explicitely named +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html + + +@pytest.fixture(name="controller_version") +def controller_version_fixture(): + """ + mock ControllerVersion + """ + return ControllerVersion(MockAnsibleModule) + + +@pytest.fixture(name="log") +def log_fixture(): + """ + mock Log + """ + return Log(MockAnsibleModule) + + +@pytest.fixture(name="params_validate") +def params_validate_fixture(): + """ + mock ParamsValidate + """ + return ParamsValidate(MockAnsibleModule) + + +@contextmanager +def does_not_raise(): + """ + A context manager that does not raise an exception. + """ + yield + + +def load_playbook_config(key: str) -> Dict[str, str]: + """ + Return playbook configs for common + """ + playbook_file = "common_playbook_configs" + playbook_config = load_fixture(playbook_file).get(key) + print(f"load_playbook_config: {key} : {playbook_config}") + return playbook_config + + +def responses_controller_version(key: str) -> Dict[str, str]: + """ + Return ControllerVersion controller responses + """ + response_file = "responses_ControllerVersion" + response = load_fixture(response_file).get(key) + print(f"responses_controller_version: {key} : {response}") + return response diff --git a/tests/unit/module_utils/common/fixture.py b/tests/unit/module_utils/common/fixture.py new file mode 100644 index 000000000..bb3730787 --- /dev/null +++ b/tests/unit/module_utils/common/fixture.py @@ -0,0 +1,50 @@ +# Copyright (c) 2024 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 + +import json +import os +import sys + +fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") + + +def load_fixture(filename): + """ + load test inputs from json files + """ + path = os.path.join(fixture_path, f"{filename}.json") + + try: + with open(path, encoding="utf-8") as file_handle: + data = file_handle.read() + except IOError as exception: + msg = f"Exception opening test input file {filename}.json : " + msg += f"Exception detail: {exception}" + print(msg) + sys.exit(1) + + try: + fixture = json.loads(data) + except json.JSONDecodeError as exception: + msg = "Exception reading JSON contents in " + msg += f"test input file {filename}.json : " + msg += f"Exception detail: {exception}" + print(msg) + sys.exit(1) + + return fixture diff --git a/tests/unit/module_utils/common/fixtures/responses_ControllerVersion.json b/tests/unit/module_utils/common/fixtures/responses_ControllerVersion.json new file mode 100644 index 000000000..e14e07e28 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_ControllerVersion.json @@ -0,0 +1,453 @@ +{ + "test_common_version_00009a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00010a": { + "RETURN_CODE": 404, + "METHOD": "GET", + "REQUEST_PATH": "https://foo/noop", + "MESSAGE": "Not Found", + "DATA": {} + }, + "test_common_version_00011a": { + "RETURN_CODE": 500, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "Internal Server Error", + "DATA": {} + }, + "test_common_version_00002a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00002b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "true", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00002c": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00003a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00003b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00004b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00004a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "true", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00004c": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00006b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00006a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "true" + } + }, + "test_common_version_00006c": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "" + } + }, + "test_common_version_00005b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00005a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "true", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00005c": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00007a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00008a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK" + }, + "test_common_version_00012a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00012b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00013a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "foo-uuid", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00013b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00014a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00014b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00015a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00015b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00016a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00016b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00017a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "version": "12.1.3b", + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "install": "EASYFABRIC", + "uuid": "", + "is_upgrade_inprogress": "false" + } + }, + "test_common_version_00017b": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", + "MESSAGE": "OK", + "DATA": { + "mode": "LAN", + "isMediaController": "false", + "dev": "false", + "isHaEnabled": "false", + "uuid": "", + "is_upgrade_inprogress": "false" + } + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py b/tests/unit/module_utils/common/test_controller_version.py similarity index 99% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py rename to tests/unit/module_utils/common/test_controller_version.py index cd3c64456..ff108d6b6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_controller_version.py +++ b/tests/unit/module_utils/common/test_controller_version.py @@ -32,8 +32,8 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from .image_upgrade_utils import (controller_version_fixture, - responses_controller_version) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + controller_version_fixture, responses_controller_version) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_COMMON = PATCH_MODULE_UTILS + "common." diff --git a/tests/unit/module_utils/common/test_log.py b/tests/unit/module_utils/common/test_log.py new file mode 100644 index 000000000..b25ada2c1 --- /dev/null +++ b/tests/unit/module_utils/common/test_log.py @@ -0,0 +1,146 @@ +# Copyright (c) 2024 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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + does_not_raise, log_fixture) + + +def test_log_00010(log) -> None: + """ + Function + - log_msg + + Test + - log_msg returns None when debug is False + """ + with does_not_raise(): + instance = log + + error_message = "This is an error message" + instance.debug = False + assert instance.log_msg(error_message) is None + + +def test_log_00011(tmp_path, log) -> None: + """ + Function + - log_msg + + Test + - log_msg does not write to the logfile when debug is False + """ + directory = tmp_path / "test_log_msg" + directory.mkdir() + filename = directory / "test_log_msg.txt" + + msg = "This is an error message" + + with does_not_raise(): + instance = log + instance.debug = False + instance.logfile = filename + instance.log_msg(msg) + + match = r"\[Errno 2\] " + match += "No such file or directory" + with pytest.raises(FileNotFoundError, match=match): + filename.read_text(encoding="UTF-8") + + +def test_log_00012(tmp_path, log) -> None: + """ + Function + - log_msg + + Test + - log_msg writes to the logfile when debug is True + """ + directory = tmp_path / "test_log_msg" + directory.mkdir() + filename = directory / "test_log_msg.txt" + + msg = "This is an error message" + + with does_not_raise(): + instance = log + instance.debug = True + instance.logfile = filename + instance.log_msg(msg) + + assert filename.read_text(encoding="UTF-8") == msg + "\n" + + +def test_log_00013(tmp_path, log) -> None: + """ + Function + - log_msg + + Test + - log_msg calls fail_json if the logfile cannot be opened + + Description + To ensure an error is generated, we attempt a write to a filename + that is too long for the target OS. + """ + directory = tmp_path / "test_log_msg" + directory.mkdir() + filename = directory / f"test_{'a' * 2000}_log_msg.txt" + + msg = "This is an error message" + + with does_not_raise(): + instance = log + instance.debug = True + instance.logfile = filename + + match = "error writing to logfile" + with pytest.raises(AnsibleFailJson, match=match): + instance.log_msg(msg) + + +def test_log_00020(log) -> None: + """ + Function + - debug + + Test + - log_msg calls fail_json if debug is not a boolean + """ + with does_not_raise(): + instance = log + + match = "Invalid type for debug. Expected bool. " + with pytest.raises(AnsibleFailJson, match=match): + instance.debug = 10 diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py b/tests/unit/module_utils/common/test_params_validate.py similarity index 89% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py rename to tests/unit/module_utils/common/test_params_validate.py index 3b59e0a04..41910d2a0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_params_validate.py +++ b/tests/unit/module_utils/common/test_params_validate.py @@ -35,22 +35,8 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate - -from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, - image_upgrade_fixture, - issu_details_by_ip_address_fixture, - params_validate_fixture, - payloads_image_upgrade, - responses_image_install_options, - responses_image_upgrade, - responses_switch_issu_details) - -PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." - -DCNM_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.dcnm_send" -DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + does_not_raise, params_validate_fixture) def test_params_validate_00001(params_validate) -> None: @@ -875,70 +861,3 @@ def test_params_validate_00093(params_validate) -> None: with pytest.raises(AnsibleFailJson, match=match): instance.validate() - - -# tests 00110 - 00112 are taken from test_image_upgrade_common.py -# and have the same numbering. These should be moved to a common -# test module when log_msg (or an equivilent) is moved to a -# common module -def test_params_validate_00110(params_validate) -> None: - """ - Function - - log.log_msg - - Test - - log.log_msg returns None when debug is False - """ - instance = params_validate - - error_message = "This is an error message" - instance.debug = False - assert instance.log.log_msg(error_message) is None - - -def test_params_validate_00111(tmp_path, params_validate) -> None: - """ - Function - - log_msg - - Test - - log_msg writes to the logfile when debug is True - """ - instance = params_validate - - directory = tmp_path / "test_log_msg" - directory.mkdir() - filename = directory / "test_log_msg.txt" - - error_message = "This is an error message" - instance.debug = True - instance.logfile = filename - instance.log.log_msg(error_message) - - assert filename.read_text(encoding="UTF-8") == error_message + "\n" - assert len(list(tmp_path.iterdir())) == 1 - - -def test_params_validate_00112(tmp_path, params_validate) -> None: - """ - Function - - log_msg - - Test - - log_msg calls fail_json if the logfile cannot be opened - - Description - To ensure an error is generated, we attempt a write to a filename - that is too long for the target OS. - """ - instance = params_validate - - directory = tmp_path / "test_log_msg" - directory.mkdir() - filename = directory / f"test_{'a' * 2000}_log_msg.txt" - - error_message = "This is an error message" - instance.debug = True - instance.logfile = filename - with pytest.raises(AnsibleFailJson, match="error writing to logfile"): - instance.log.log_msg(error_message) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json index 049321107..a4905185c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json @@ -1,455 +1,4 @@ { - "test_common_version_00009a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00010a": { - "RETURN_CODE": 404, - "METHOD": "GET", - "REQUEST_PATH": "https://foo/noop", - "MESSAGE": "Not Found", - "DATA": {} - }, - "test_common_version_00011a": { - "RETURN_CODE": 500, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "Internal Server Error", - "DATA": {} - }, - "test_common_version_00002a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00002b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "true", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00002c": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00003a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00003b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00004b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00004a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "true", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00004c": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00006b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00006a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "true" - } - }, - "test_common_version_00006c": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "" - } - }, - "test_common_version_00005b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00005a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "true", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00005c": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00007a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00008a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK" - }, - "test_common_version_00012a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00012b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00013a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "foo-uuid", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00013b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00014a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00014b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00015a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00015b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00016a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00016b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00017a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "version": "12.1.3b", - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "install": "EASYFABRIC", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, - "test_common_version_00017b": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", - "MESSAGE": "OK", - "DATA": { - "mode": "LAN", - "isMediaController": "false", - "dev": "false", - "isHaEnabled": "false", - "uuid": "", - "is_upgrade_inprogress": "false" - } - }, "test_image_mgmt_stage_00003a": { "RETURN_CODE": 200, "METHOD": "GET", From 4d666d9a5673f967117cc838fa9975a89abdc646 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 23 Dec 2023 15:30:04 -1000 Subject: [PATCH 171/300] Don't abbreviate import --- .../modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index ff0caf3ec..2d1b28f62 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -48,8 +48,8 @@ SwitchIssuDetailsBySerialNumber) from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ ImageUpgradeTask - -from .fixture import load_fixture +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.fixture import \ + load_fixture class MockAnsibleModule: From 9fdae8cca3c093a97adff1536d40e7ecfc08a8e6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 23 Dec 2023 18:11:33 -1000 Subject: [PATCH 172/300] MergeDicts: add unit tests, simplify logic --- plugins/module_utils/common/merge_dicts.py | 22 +- .../unit/module_utils/common/common_utils.py | 31 +- .../common/fixtures/merge_dicts.json | 147 +++++++ .../module_utils/common/test_merge_dicts.py | 372 ++++++++++++++++++ 4 files changed, 542 insertions(+), 30 deletions(-) create mode 100644 tests/unit/module_utils/common/fixtures/merge_dicts.json create mode 100644 tests/unit/module_utils/common/test_merge_dicts.py diff --git a/plugins/module_utils/common/merge_dicts.py b/plugins/module_utils/common/merge_dicts.py index ec7b12a3b..a7821e53f 100644 --- a/plugins/module_utils/common/merge_dicts.py +++ b/plugins/module_utils/common/merge_dicts.py @@ -85,23 +85,7 @@ def merge_dicts( """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable for key in dict2: - if isinstance(dict1.get(key, None), Map) and dict2.get(key, None) is None: - # This is to handle a case where dict1 contains a key that is a - # dict, which is supposed to contain sub-options, but the dict - # is empty. - # - # For example, below, upgrade is specified, but upgrade.nxos - # and upgrade.epld are not. In this case, we copy the entire - # 'upgrade' dict from dict1 to dict2: - # - # - ip_address: 172.22.150.110 - # upgrade: - # options: - # epld: - # module: 27 - # golden: false - dict2[key] = dict1[key] - elif ( + if ( key in dict1 and isinstance(dict1[key], Map) and isinstance(dict2[key], Map) @@ -166,7 +150,7 @@ def dict1(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "Invalid parameters. Expected type dict. " + msg += "Invalid value. Expected type dict. " msg += f"Got type {type(value)}." self.ansible_module.fail_json(msg) self.properties["dict1"] = copy.deepcopy(value) @@ -185,7 +169,7 @@ def dict2(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "Invalid parameters. Expected type dict. " + msg += "Invalid value. Expected type dict. " msg += f"Got type {type(value)}." self.ansible_module.fail_json(msg) self.properties["dict2"] = copy.deepcopy(value) diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index d234b812a..de2cb1ac4 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -25,8 +25,9 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ ControllerVersion -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ - Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ + MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate @@ -65,7 +66,7 @@ def public_method_for_pylint(self) -> Any: @pytest.fixture(name="controller_version") def controller_version_fixture(): """ - mock ControllerVersion + return ControllerVersion with mocked AnsibleModule """ return ControllerVersion(MockAnsibleModule) @@ -73,15 +74,23 @@ def controller_version_fixture(): @pytest.fixture(name="log") def log_fixture(): """ - mock Log + return Log with mocked AnsibleModule """ return Log(MockAnsibleModule) +@pytest.fixture(name="merge_dicts") +def merge_dicts_fixture(): + """ + return MergeDicts with mocked AnsibleModule + """ + return MergeDicts(MockAnsibleModule) + + @pytest.fixture(name="params_validate") def params_validate_fixture(): """ - mock ParamsValidate + return ParamsValidate with mocked AnsibleModule """ return ParamsValidate(MockAnsibleModule) @@ -94,14 +103,14 @@ def does_not_raise(): yield -def load_playbook_config(key: str) -> Dict[str, str]: +def merge_dicts_data(key: str) -> Dict[str, str]: """ - Return playbook configs for common + Return data for merge_dicts unit tests """ - playbook_file = "common_playbook_configs" - playbook_config = load_fixture(playbook_file).get(key) - print(f"load_playbook_config: {key} : {playbook_config}") - return playbook_config + data_file = "merge_dicts" + data = load_fixture(data_file).get(key) + print(f"merge_dicts_data: {key} : {data}") + return data def responses_controller_version(key: str) -> Dict[str, str]: diff --git a/tests/unit/module_utils/common/fixtures/merge_dicts.json b/tests/unit/module_utils/common/fixtures/merge_dicts.json new file mode 100644 index 000000000..41851c145 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/merge_dicts.json @@ -0,0 +1,147 @@ +{ + "test_merge_dicts_00050": { + "TEST_NOTES": [ + "keys from dict1 and dict2 are different", + "keys from dict1 and dict2 are merged unchanged." + ], + "dict1": { + "foo": 1 + }, + "dict2": { + "bar": 3 + }, + "dict_merged": { + "foo": 1, + "bar": 3 + } + }, + "test_merge_dicts_00051": { + "TEST_NOTES": [ + "dict1 and dict2 keys are the same", + "dict2 overwrites dict1" + ], + "dict1": { + "foo": 1 + }, + "dict2": { + "foo": 2 + }, + "dict_merged": { + "foo": 2 + } + }, + "test_merge_dicts_00052": { + "TEST_NOTES": [ + "dict1 and dict2 keys are the same", + "dict2 overwrites dict1, even though dict1 keys value is a dict" + ], + "dict1": { + "foo": { + "bar": 1 + } + }, + "dict2": { + "foo": 2 + }, + "dict_merged": { + "foo": 2 + } + }, + "test_merge_dicts_00053": { + "TEST_NOTES": [ + "dict1 and dict2 contain the same top-level keys", + "these keys both have a value that is a dict", + "dict1 nested-dict keys are the same as dict2 nested-dict keys", + "dict_merged nested-dict keys contain the values from dict2" + ], + "dict1": { + "foo": { + "bar": 1, + "baz": 1 + } + }, + "dict2": { + "foo": { + "bar": 2, + "baz": 2 + } + }, + "dict_merged": { + "foo": { + "bar": 2, + "baz": 2 + } + } + }, + "test_merge_dicts_00054": { + "TEST_NOTES": [ + "dict1 and dict2 contain the same top-level keys", + "these keys both have a value that is a dict", + "dict1 nested-dict keys are different from dict2 nested-dict keys", + "dict_merged contains all keys from dict1 and dict2 with values unchanged" + ], + "dict1": { + "foo": { + "bar": 1 + } + }, + "dict2": { + "foo": { + "baz": 2 + } + }, + "dict_merged": { + "foo": { + "bar": 1, + "baz": 2 + } + } + }, + "test_merge_dicts_00055": { + "TEST_NOTES": [ + "dict1 is empty", + "dict2 overwrites dict1", + "dict_merged == dict2" + ], + "dict1": {}, + "dict2": { + "foo": 3, + "baz": { + "bar": 10, + "key1": "value1", + "key2": "value2" + } + }, + "dict_merged": { + "foo": 3, + "baz": { + "bar": 10, + "key1": "value1", + "key2": "value2" + } + } + }, + "test_merge_dicts_00056": { + "TEST_NOTES": [ + "dict2 is empty", + "dict_merge == dict1" + ], + "dict1": { + "foo": 3, + "baz": { + "bar": 10, + "key1": "value1", + "key2": "value2" + } + }, + "dict2": {}, + "dict_merged": { + "foo": 3, + "baz": { + "bar": 10, + "key1": "value1", + "key2": "value2" + } + } + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_merge_dicts.py b/tests/unit/module_utils/common/test_merge_dicts.py new file mode 100644 index 000000000..478795f36 --- /dev/null +++ b/tests/unit/module_utils/common/test_merge_dicts.py @@ -0,0 +1,372 @@ +# Copyright (c) 2024 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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ + MergeDicts +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + does_not_raise, merge_dicts_data, merge_dicts_fixture) + + +def test_merge_dicts_00001(merge_dicts) -> None: + """ + Function + - __init__ + + Test + - Class attributes are initialized to expected values + """ + with does_not_raise(): + instance = merge_dicts + assert isinstance(instance, MergeDicts) + assert isinstance(instance.properties, dict) + assert instance.class_name == "MergeDicts" + assert instance.log.logfile is None + assert instance.log.debug is False + assert instance.properties.get("dict1", "foo") is None + assert instance.properties.get("dict2", "foo") is None + assert instance.properties.get("dict_merged", "foo") is None + + +MATCH_00020 = "MergeDicts.dict1: Invalid value. Expected type dict. Got type " + + +@pytest.mark.parametrize( + "value, expected", + [ + ({}, does_not_raise()), + ([], pytest.raises(AnsibleFailJson, match=MATCH_00020)), + ((), pytest.raises(AnsibleFailJson, match=MATCH_00020)), + (None, pytest.raises(AnsibleFailJson, match=MATCH_00020)), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00020)), + (1.1, pytest.raises(AnsibleFailJson, match=MATCH_00020)), + ("foo", pytest.raises(AnsibleFailJson, match=MATCH_00020)), + (True, pytest.raises(AnsibleFailJson, match=MATCH_00020)), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00020)), + ], +) +def test_merge_dicts_00020(merge_dicts, value, expected) -> None: + """ + Function + - dict1 + + Test + - dict1 accepts only a dict + """ + with does_not_raise(): + instance = merge_dicts + with expected: + instance.dict1 = value + + +MATCH_00021 = "MergeDicts.dict2: Invalid value. Expected type dict. Got type " + + +@pytest.mark.parametrize( + "value, expected", + [ + ({}, does_not_raise()), + ([], pytest.raises(AnsibleFailJson, match=MATCH_00021)), + ((), pytest.raises(AnsibleFailJson, match=MATCH_00021)), + (None, pytest.raises(AnsibleFailJson, match=MATCH_00021)), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00021)), + (1.1, pytest.raises(AnsibleFailJson, match=MATCH_00021)), + ("foo", pytest.raises(AnsibleFailJson, match=MATCH_00021)), + (True, pytest.raises(AnsibleFailJson, match=MATCH_00021)), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00021)), + ], +) +def test_merge_dicts_00021(merge_dicts, value, expected) -> None: + """ + Function + - dict2 + + Test + - dict2 accepts only a dict + """ + with does_not_raise(): + instance = merge_dicts + with expected: + instance.dict2 = value + + +MATCH_00030 = "MergeDicts.commit: " +MATCH_00030 += "dict1 and dict2 must be set " +MATCH_00030 += r"before calling commit\(\)" + + +@pytest.mark.parametrize( + "dict1, dict2, expected", + [ + ({}, {}, does_not_raise()), + (None, {}, pytest.raises(AnsibleFailJson, match=MATCH_00030)), + ({}, None, pytest.raises(AnsibleFailJson, match=MATCH_00030)), + ], +) +def test_merge_dicts_00030(merge_dicts, dict1, dict2, expected) -> None: + """ + Function + - commit + + Test + - commit calls fail_json when dict1 or dict2 is None + """ + with does_not_raise(): + instance = merge_dicts + if dict1 is not None: + instance.dict1 = dict1 + if dict2 is not None: + instance.dict2 = dict2 + with expected: + instance.commit() + + +MATCH_00040 = "Invalid type for debug. Expected bool. " + + +@pytest.mark.parametrize( + "value, expected", + [ + (True, does_not_raise()), + (False, does_not_raise()), + ([], pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ((), pytest.raises(AnsibleFailJson, match=MATCH_00040)), + (None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + (1.1, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ("foo", pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ], +) +def test_merge_dicts_00040(merge_dicts, value, expected) -> None: + """ + Function + - debug + + Test + - debug accepts boolean values + - debug calls fail_json for non-boolean values + """ + with does_not_raise(): + instance = merge_dicts + + with expected: + instance.debug = value + + +# The remaining tests verify various combinations of dict1 and dict2 +# using the following merge rules: +# 1. non-dict keys in dict1 are overwritten by dict2 +# if they exist in dict2 +# 2. non-dict keys in dict1 are not overwritten by dict2 +# if they do not exist in dict2 +# 3. if a key exists in both dict1 and dict2 and that key's value +# is a dict in both dict1 and dict2, the function recurses into +# the dict and applies the first two rules to the nested dict +# 4. in all other cases, dict2 overwrites dict1. For example, if +# a key exists in both dict1 and dict2 and that key's value +# is a dict in dict1 but not in dict2, the key is overwritten +# by dict2 (similar to rule 1) +def test_merge_dicts_00050(merge_dicts) -> None: + """ + Function + - dict1 + - dict2 + - commit + - dict_merged + + Test + - dict1 contains one top-level key foo with non-dict value + - dict2 contains one top-level key bar with non-dict value + - dict_merged contains both top-level keys with unchanged values + """ + key = "test_merge_dicts_00050" + data = merge_dicts_data(key) + + with does_not_raise(): + instance = merge_dicts + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_00051(merge_dicts) -> None: + """ + Function + - dict1 + - dict2 + - commit + - dict_merged + + Test + - dict1 contains one top-level key foo with non-dict value + - dict2 contains one top-level key foo with non-dict value + - dict_merged contains one top-level key foo with value from dict2 + """ + key = "test_merge_dicts_00051" + data = merge_dicts_data(key) + + with does_not_raise(): + instance = merge_dicts + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_00052(merge_dicts) -> None: + """ + Function + - dict1 + - dict2 + - commit + - dict_merged + + Test + - dict1 contains one top-level key foo with dict value + - dict2 contains one top-level key foo with non-dict value + - dict_merged contains one top-level key foo with value from dict2 + """ + key = "test_merge_dicts_00052" + data = merge_dicts_data(key) + + with does_not_raise(): + instance = merge_dicts + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_00053(merge_dicts) -> None: + """ + Function + - dict1 + - dict2 + - commit + - dict_merged + + Test + - dict1 contains one top-level key foo that is a dict + - dict2 contains one top-level key foo that is a dict + - the keys in both nested dicts are the same + - dict_merged contains one top-level key foo + that is a dict containing key/values from dict2 + """ + key = "test_merge_dicts_00053" + data = merge_dicts_data(key) + + with does_not_raise(): + instance = merge_dicts + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_00054(merge_dicts) -> None: + """ + Function + - dict1 + - dict2 + - commit + - dict_merged + + Test + - dict1 contains one top-level key foo that is a dict + - dict2 contains one top-level key foo that is a dict + - the keys in dict1/dict2 nested dicts are different + - dict_merged contains one top-level key foo + that is a dict containing keys from both dict1 + and dict2, with values unchanged. + """ + key = "test_merge_dicts_00054" + data = merge_dicts_data(key) + + with does_not_raise(): + instance = merge_dicts + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_00055(merge_dicts) -> None: + """ + Function + - dict1 + - dict2 + - commit + - dict_merged + + Test + - dict1 is empty + - dict2 contains several keys with a combination of + dict and non-dict values + - dict_merged contains the contents of dict2 + """ + key = "test_merge_dicts_00055" + data = merge_dicts_data(key) + + with does_not_raise(): + instance = merge_dicts + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_00056(merge_dicts) -> None: + """ + Function + - dict1 + - dict2 + - commit + - dict_merged + + Test + - dict2 is empty + - dict1 contains several keys with a combination of + dict and non-dict values + - dict_merged contains the contents of dict1 + """ + key = "test_merge_dicts_00056" + data = merge_dicts_data(key) + + with does_not_raise(): + instance = merge_dicts + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") From ba9aa28d36bea0d154fd5f5908ee7702bedae13d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 23 Dec 2023 19:13:33 -1000 Subject: [PATCH 173/300] Additional unit test --- .../module_utils/common/test_merge_dicts.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/module_utils/common/test_merge_dicts.py b/tests/unit/module_utils/common/test_merge_dicts.py index 478795f36..51f90ac9d 100644 --- a/tests/unit/module_utils/common/test_merge_dicts.py +++ b/tests/unit/module_utils/common/test_merge_dicts.py @@ -184,6 +184,25 @@ def test_merge_dicts_00040(merge_dicts, value, expected) -> None: instance.debug = value +def test_merge_dicts_00041(merge_dicts) -> None: + """ + Function + - dict_merged + + Test + - dict_merged calls fail_json when called before calling commit + """ + with does_not_raise(): + instance = merge_dicts + + match = "MergeDicts.dict_merged: " + match += r"Call instance\.commit\(\) before " + match += r"calling instance\.dict_merged\." + + with pytest.raises(AnsibleFailJson, match=match): + value = instance.dict_merged # pylint: disable=unused-variable + + # The remaining tests verify various combinations of dict1 and dict2 # using the following merge rules: # 1. non-dict keys in dict1 are overwritten by dict2 From 41c88e917fd84c37a994cd5be8b71008d46337ce Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 24 Dec 2023 09:16:00 -1000 Subject: [PATCH 174/300] ImageValidaate: move refresh() out of for loop ImageValidate.prune_serial_numbers: issu_detail.refresh() should be called only once outside the for loop. --- plugins/module_utils/image_mgmt/image_validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index baefb4fa8..19b937e36 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -101,10 +101,10 @@ def prune_serial_numbers(self) -> None: """ self.method_name = inspect.stack()[0][3] + self.issu_detail.refresh() serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: self.issu_detail.serial_number = serial_number - self.issu_detail.refresh() if self.issu_detail.validated == "Success": self.serial_numbers.remove(self.issu_detail.serial_number) From 221a3e39147e4082b56d7ff59d06b93579fe9743 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 24 Dec 2023 10:09:49 -1000 Subject: [PATCH 175/300] ParamsMergeDefaults: use dict.get() rather than "not in" For reasons I don't understand, using: "if not in " was failing idempotence for keys with null values that have a default value of dict i.e. key["default"] = {}. After changing this to use: "if dict.get(, None) is None" things are working for this specific case. --- plugins/module_utils/common/params_merge_defaults.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index ca1497416..4fe408129 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -96,12 +96,12 @@ def _merge_default_params( if spec_key in self.reserved_params: continue - if spec_key not in params and "default" in spec_value: - params[spec_key] = spec_value["default"] - - if spec_key not in params and "default" not in spec_value: + if params.get(spec_key, None) is None and "default" not in spec_value: continue + if params.get(spec_key, None) is None and "default" in spec_value: + params[spec_key] = spec_value["default"] + if isinstance(spec_value, Map): params[spec_key] = self._merge_default_params( spec_value, params[spec_key] From 305912d98d7da94bff0a6340d759d18d5fa2d3f7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 24 Dec 2023 11:45:25 -1000 Subject: [PATCH 176/300] ImageInstallOptions: Avoid NDFC error NDFC returns an error when all of issu, epld, and package_install are False. Avoid this by returning a mocked NDFC response indicating that nothing needs to be done. This makes the change transparant to anything outside this class. While we're modifying this class, fix a couple pylint errors and inappropriate assignments of self.method_name. --- .../image_mgmt/install_options.py | 51 ++++++++++++++----- ...est_image_upgrade_image_install_options.py | 11 ++-- .../test_image_upgrade_image_upgrade.py | 2 +- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 6b74d98b1..18ecfb5b1 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -141,6 +141,13 @@ def __init__(self, module) -> None: super().__init__(module) self.class_name = self.__class__.__name__ self.endpoints = ApiEndpoints() + + self.path = self.endpoints.install_options.get("path") + self.verb = self.endpoints.install_options.get("verb") + self.payload: Dict[str, Any] = {} + + self.compatibility_status = {} + self._init_properties() def _init_properties(self): @@ -159,25 +166,39 @@ def refresh(self) -> None: """ Refresh self.response_data with current install-options from the controller """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if self.policy_name is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.policy_name must be set before " msg += "calling refresh()" self.module.fail_json(msg) if self.serial_number is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_number must be set before " msg += "calling refresh()" self.module.fail_json(msg) - self.path = self.endpoints.install_options.get("path") - self.verb = self.endpoints.install_options.get("verb") + # At least one of epld, issu, or package_install must be True + # before calling refresh() or the controller will return an error. + # Mock the response such that the caller knows nothing needs to be + # done. + if self.epld is False and self.issu is False and self.package_install is False: + msg = f"{self.class_name}.{method_name}: " + msg += "At least one of epld, issu, or package_install " + msg += "must be True before calling refresh(). Skipping." + self.log.log_msg(msg) + self.compatibility_status = {} + self.properties["response_data"] = { + "compatibilityStatusList": [], + "epldModules": None, + "installPacakges": None, + "errMessage": "", + } + return self._build_payload() - self.method_name = inspect.stack()[0][3] self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) @@ -186,7 +207,7 @@ def refresh(self) -> None: self.properties["result"] = self._handle_response(self.response, self.verb) if self.result["success"] is False: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Bad result when retrieving install-options from " msg += f"the controller. Controller response: {self.response}. " if self.response_data.get("error", None) is None: @@ -220,7 +241,7 @@ def _build_payload(self) -> None: "packageInstall": false } """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.payload: Dict[str, Any] = {} self.payload["devices"] = [] @@ -233,7 +254,7 @@ def _build_payload(self) -> None: self.payload["packageInstall"] = self.package_install def _get(self, item): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable return self.make_boolean(self.make_none(self.response_data.get(item))) # Mandatory properties @@ -246,8 +267,9 @@ def policy_name(self): @policy_name.setter def policy_name(self, value): + method_name = inspect.stack()[0][3] if not isinstance(value, str): - msg = f"{self.class_name}.policy_name.setter: " + msg = f"{self.class_name}.{method_name}: " msg += f"policy_name must be a string. Got {value}." self.module.fail_json(msg) self.properties["policy_name"] = value @@ -277,9 +299,10 @@ def issu(self): @issu.setter def issu(self, value): + method_name = inspect.stack()[0][3] value = self.make_boolean(value) if not isinstance(value, bool): - msg = f"{self.class_name}.issu.setter: " + msg = f"{self.class_name}.{method_name}: " msg += f"issu must be a boolean value. Got {value}." self.module.fail_json(msg) self.properties["issu"] = value @@ -298,9 +321,10 @@ def epld(self): @epld.setter def epld(self, value): + method_name = inspect.stack()[0][3] value = self.make_boolean(value) if not isinstance(value, bool): - msg = f"{self.class_name}.epld.setter: " + msg = f"{self.class_name}.{method_name}: " msg += f"epld must be a boolean value. Got {value}." self.module.fail_json(msg) self.properties["epld"] = value @@ -318,9 +342,10 @@ def package_install(self): @package_install.setter def package_install(self, value): + method_name = inspect.stack()[0][3] value = self.make_boolean(value) if not isinstance(value, bool): - msg = f"{self.class_name}.package_install.setter: " + msg = f"{self.class_name}.{method_name}: " msg += "package_install must be a boolean value. " msg += f"Got {value}." self.module.fail_json(msg) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index da06dbc8e..0b1277fd5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -57,6 +57,11 @@ def test_image_mgmt_install_options_00001(image_install_options) -> None: assert instance.module == MockAnsibleModule assert instance.class_name == "ImageInstallOptions" assert isinstance(instance.endpoints, ApiEndpoints) + path = "/appcenter/cisco/ndfc/api/v1/imagemanagement" + path += "/rest/imageupgrade/install-options" + assert instance.path == path + assert instance.verb == "POST" + assert instance.compatibility_status == {} def test_image_mgmt_install_options_00002(image_install_options) -> None: @@ -452,7 +457,7 @@ def test_image_mgmt_install_options_00022(image_install_options) -> None: Test - fail_json is called if issu is not a boolean. """ - match = "ImageInstallOptions.issu.setter: issu must be a " + match = "ImageInstallOptions.issu: issu must be a " match += "boolean value" instance = image_install_options @@ -468,7 +473,7 @@ def test_image_mgmt_install_options_00023(image_install_options) -> None: Test - fail_json is called if epld is not a boolean. """ - match = "ImageInstallOptions.epld.setter: epld must be a " + match = "ImageInstallOptions.epld: epld must be a " match += "boolean value" instance = image_install_options @@ -484,7 +489,7 @@ def test_image_mgmt_install_options_00024(image_install_options) -> None: Test - fail_json is called if package_install is not a boolean. """ - match = "ImageInstallOptions.package_install.setter: " + match = "ImageInstallOptions.package_install: " match += "package_install must be a boolean value" instance = image_install_options diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 1415d0116..bd2cab7d6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -1419,7 +1419,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - match = "ImageInstallOptions.epld.setter: " + match = "ImageInstallOptions.epld: " match += r"epld must be a boolean value. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): instance.commit() From 11ee773c504c40f95cd533dc63fe7caa414f31d9 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 24 Dec 2023 12:30:26 -1000 Subject: [PATCH 177/300] Add unit test to cover the changes in the last commit --- ...est_image_upgrade_image_install_options.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index 0b1277fd5..ba17fe952 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -401,6 +401,47 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.refresh() +def test_image_mgmt_install_options_00011(image_install_options) -> None: + """ + Function + - refresh + + Setup + - POST REQUEST + - issu is False + - epld is False + - package_install is False + + Test + - ImageInstallOptions returns a mocked response when all of + issu, epld, and package_install are False + - Mocked response contains expected values + + Endpoint + - install-options + + Description: + monkeypatch is not needed here since the class never sends a request + to the controller in this case. + """ + with does_not_raise(): + instance = image_install_options + instance.policy_name = "KRM5" + instance.serial_number = "FDO21120U5D" + instance.epld = False + instance.issu = False + instance.package_install = False + instance.refresh() + + assert isinstance(instance.response_data, dict) + assert instance.response_data.get("compatibilityStatusList") == [] + assert instance.response_data.get("epldModules") is None + # yes, installPackages is intentionally misspelled below since + # this is what the controller returns in a real response + assert instance.response_data.get("installPacakges") is None + assert instance.response_data.get("errMessage") == "" + + def test_image_mgmt_install_options_00020(image_install_options) -> None: """ Function From 3701dbd78cc4cec924b7da0f6ed9ceab8edb7269 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 25 Dec 2023 09:10:26 -1000 Subject: [PATCH 178/300] rename ParamsValidate.validate() to ParamsValidate.commit() For consistency with other classes... --- .../module_utils/common/params_validate.py | 9 +- plugins/modules/dcnm_image_upgrade.py | 2 +- .../common/test_params_validate.py | 88 +++++++++---------- 3 files changed, 50 insertions(+), 49 deletions(-) diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 661fd42a1..0c3f8d0d2 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -71,9 +71,10 @@ class ParamsValidate: bar: bingo baz: 10 - validator = ParamsValidator(ansible_module) + validator = ParamsValidate(ansible_module) validator.parameters = ansible_module.params validator.params_spec = params_spec + validator.commit() """ def __init__(self, ansible_module): @@ -179,7 +180,7 @@ def _build_validations(self): self.validations["ipv4_subnet"] = self._validate_ipv4_subnet self.validations["ipv6_subnet"] = self._validate_ipv6_subnet - def validate(self) -> None: + def commit(self) -> None: """ Verify that parameters in self.parameters conform to self.params_spec """ @@ -187,13 +188,13 @@ def validate(self) -> None: if self.parameters is None: msg = f"{self.class_name}.{method_name}: " msg += "instance.parameters needs to be set " - msg += "prior to calling instance.validate()." + msg += "prior to calling instance.commit()." self.ansible_module.fail_json(msg) if self.params_spec is None: msg = f"{self.class_name}.{method_name}: " msg += "instance.params_spec needs to be set " - msg += "prior to calling instance.validate()." + msg += "prior to calling instance.commit()." self.ansible_module.fail_json(msg) self._validate_parameters(self.params_spec, self.parameters) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index be903c678..6d6b1c7aa 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -978,7 +978,7 @@ def _validate_switch_configs(self) -> None: for switch in self.switch_configs: validator.parameters = switch - validator.validate() + validator.commit() def _build_policy_attach_payload(self) -> None: """ diff --git a/tests/unit/module_utils/common/test_params_validate.py b/tests/unit/module_utils/common/test_params_validate.py index 41910d2a0..1c7cb217a 100644 --- a/tests/unit/module_utils/common/test_params_validate.py +++ b/tests/unit/module_utils/common/test_params_validate.py @@ -169,53 +169,53 @@ def test_params_validate_00031(params_validate) -> None: def test_params_validate_00040(params_validate) -> None: """ Function - - validate + - commit Test - - validate calls fail_json when parameters is None + - commit calls fail_json when parameters is None """ params_spec = {} params_spec["foo"] = {} params_spec["foo"]["type"] = "str" params_spec["foo"]["required"] = True - match = "ParamsValidate.validate: " + match = "ParamsValidate.commit: " match += "instance.parameters needs to be set prior to calling " - match += r"instance.validate\(\)\." + match += r"instance.commit\(\)\." with does_not_raise(): instance = params_validate instance.params_spec = params_spec with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() def test_params_validate_00041(params_validate) -> None: """ Function - - validate + - commit Test - - validate calls fail_json when params_spec is None + - commit calls fail_json when params_spec is None """ parameters = {} parameters["foo"] = "bar" - match = "ParamsValidate.validate: " + match = "ParamsValidate.commit: " match += "instance.params_spec needs to be set prior to calling " - match += r"instance.validate\(\)\." + match += r"instance.commit\(\)\." with does_not_raise(): instance = params_validate instance.parameters = parameters with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() def test_params_validate_00050(params_validate) -> None: """ Function - - validate + - commit - validate_parameters - verify_choices @@ -236,13 +236,13 @@ def test_params_validate_00050(params_validate) -> None: with does_not_raise(): instance.params_spec = params_spec instance.parameters = parameters - instance.validate() + instance.commit() def test_params_validate_00051(params_validate) -> None: """ Function - - validate + - commit - validate_parameters Test @@ -267,13 +267,13 @@ def test_params_validate_00051(params_validate) -> None: match += "Playbook is missing mandatory parameter: foo." with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() def test_params_validate_00052(params_validate) -> None: """ Function - - validate + - commit - verify_choices Test @@ -293,13 +293,13 @@ def test_params_validate_00052(params_validate) -> None: with does_not_raise(): instance.params_spec = params_spec instance.parameters = parameters - instance.validate() + instance.commit() def test_params_validate_00053(params_validate) -> None: """ Function - - validate + - commit - verify_choices Test @@ -325,7 +325,7 @@ def test_params_validate_00053(params_validate) -> None: match += r"Expected one of \['bar', 'baz'\]. Got bing" with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -335,7 +335,7 @@ def test_params_validate_00053(params_validate) -> None: def test_params_validate_00060(params_validate, value, expected_type) -> None: """ Function - - validate + - commit - verify_type Test @@ -366,7 +366,7 @@ def test_params_validate_00060(params_validate, value, expected_type) -> None: match += f"Expected {expected_type}. Got '{value}'. " with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -393,7 +393,7 @@ def test_params_validate_00060(params_validate, value, expected_type) -> None: def test_params_validate_00061(params_validate, value, expected_type) -> None: """ Function - - validate + - commit - verify_type Test @@ -411,13 +411,13 @@ def test_params_validate_00061(params_validate, value, expected_type) -> None: instance = params_validate instance.params_spec = params_spec instance.parameters = parameters - instance.validate() + instance.commit() def test_params_validate_00062(params_validate) -> None: """ Function - - validate + - commit - verify_type Test @@ -443,7 +443,7 @@ def test_params_validate_00062(params_validate) -> None: match += "Got 'bad_type'." with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -476,7 +476,7 @@ def test_params_validate_00071( ) -> None: """ Function - - validate + - commit - _verify_multitype Test @@ -499,7 +499,7 @@ def test_params_validate_00071( instance = params_validate instance.params_spec = params_spec instance.parameters = parameters - instance.validate() + instance.commit() if preferred_type in instance._ipaddress_types: # pylint: disable=protected-access assert isinstance(instance.parameters["foo"], str) else: @@ -520,7 +520,7 @@ def test_params_validate_00072( ) -> None: """ Function - - validate + - commit - verify_type - _verify_multitype @@ -547,7 +547,7 @@ def test_params_validate_00072( match += f"Got '{value}'." with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -562,7 +562,7 @@ def test_params_validate_00073( ) -> None: """ Function - - validate + - commit - verify_type - _verify_multitype @@ -598,7 +598,7 @@ def test_params_validate_00073( match += f"Got '{value}'." with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -612,7 +612,7 @@ def test_params_validate_00074( ) -> None: """ Function - - validate + - commit - verify_type - _verify_multitype @@ -633,13 +633,13 @@ def test_params_validate_00074( instance = params_validate instance.params_spec = params_spec instance.parameters = parameters - instance.validate() + instance.commit() def test_params_validate_00075(params_validate) -> None: """ Function - - validate + - commit - _verify_multitype - _verify_preferred_type @@ -666,7 +666,7 @@ def test_params_validate_00075(params_validate) -> None: match += "Invalid param_spec for parameter 'foo'. " match += "If type is a list, preferred_type must be specified." with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -685,7 +685,7 @@ def test_params_validate_00075(params_validate) -> None: def test_params_validate_00080(params_validate, value, type_to_verify) -> None: """ Function - - validate + - commit - verify_type - ipaddress_guard @@ -710,7 +710,7 @@ def test_params_validate_00080(params_validate, value, type_to_verify) -> None: match += f"Expected type {type_to_verify}. " match += f"Got type {type(value)} for param foo with value {value}." with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -724,7 +724,7 @@ def test_params_validate_00080(params_validate, value, type_to_verify) -> None: def test_params_validate_00090(params_validate, value, range_min, range_max) -> None: """ Function - - validate + - commit - _verify_integer_range Test @@ -745,7 +745,7 @@ def test_params_validate_00090(params_validate, value, range_min, range_max) -> with does_not_raise(): instance.params_spec = params_spec instance.parameters = parameters - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -759,7 +759,7 @@ def test_params_validate_00090(params_validate, value, range_min, range_max) -> def test_params_validate_00091(params_validate, value, range_min, range_max) -> None: """ Function - - validate + - commit - _verify_integer_range Test @@ -785,7 +785,7 @@ def test_params_validate_00091(params_validate, value, range_min, range_max) -> match += f"Expected value between 1 and 10. Got {value}" with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() @pytest.mark.parametrize( @@ -799,7 +799,7 @@ def test_params_validate_00091(params_validate, value, range_min, range_max) -> def test_params_validate_00092(params_validate, value, range_min, range_max) -> None: """ Function - - validate + - commit - _verify_integer_range Test @@ -827,13 +827,13 @@ def test_params_validate_00092(params_validate, value, range_min, range_max) -> match += rf"range_max '.*?' type {type(range_max)}." with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() def test_params_validate_00093(params_validate) -> None: """ Function - - validate + - commit - _verify_integer_range Test @@ -860,4 +860,4 @@ def test_params_validate_00093(params_validate) -> None: match += "parameters of type int. Got type str for param foo." with pytest.raises(AnsibleFailJson, match=match): - instance.validate() + instance.commit() From 55dd467a2a17922f519e7d194dff24e050af49ee Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 26 Dec 2023 14:44:40 -1000 Subject: [PATCH 179/300] Changing behavior of ImagePolicies, more... ImagePolicies() called fail_json() when the caller asked for a policy_name that did not exist on the controller. During initial work on dcnm_image_policy module, we need this to return None instead. Hence, changing the behavior. This does not require any other changes to dcnm_image_upgrade, other than modifying one of the unit tests to expect the new behavior. --- .../module_utils/image_mgmt/image_policies.py | 17 +++++++++---- .../test_image_upgrade_image_policies.py | 24 +++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 061841607..e3bd7c03f 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -119,10 +119,10 @@ def _get(self, item): self.module.fail_json(msg) if self.policy_name not in self.properties["response_data"]: - msg = f"{self.class_name}.{self.method_name}: " - msg += f"policy_name {self.policy_name} is not defined " - msg += "on the controller." - self.module.fail_json(msg) + return None + + if item == "policy": + return self.properties["response_data"][self.policy_name] if item not in self.properties["response_data"][self.policy_name]: msg = f"{self.class_name}.{self.method_name}: " @@ -195,6 +195,15 @@ def policy_name(self): def policy_name(self, value): self.properties["policy_name"] = value + @property + def policy(self): + """ + Return the policy data of the policy matching self.policy_name, + if it exists. + Return None otherwise + """ + return self._get("policy") + @property def policy_type(self): """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index 97db01d70..53aaecadd 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -227,10 +227,11 @@ def test_image_mgmt_image_policies_00024(monkeypatch, image_policies) -> None: """ Function - refresh + - policy_name Test - - fail_json() is called if response does not contain policy_name. - - i.e. image policy with name FOO has not yet been created on the controller. + - instance.policy_name is set to a policy that does not exist on the controller. + - instance.policy returns None Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies @@ -243,16 +244,19 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) - image_policies.refresh() - image_policies.policy_name = "FOO" + with does_not_raise(): + instance = image_policies + instance.refresh() + image_policies.policy_name = "FOO" - match = "ImagePolicies._get: " - match += "policy_name FOO is not defined on the controller." + assert image_policies.policy is None + # match = "ImagePolicies._get: " + # match += "policy_name FOO is not defined on the controller." - instance = image_policies - with pytest.raises(AnsibleFailJson, match=match): - if instance.policy_type == "PLATFORM": - pass + # instance = image_policies + # with pytest.raises(AnsibleFailJson, match=match): + # if instance.policy_type == "PLATFORM": + # pass def test_image_mgmt_image_policies_00025(monkeypatch, image_policies) -> None: From 0d7c00b8d4cc9dbaa6821a598605b70e67914074 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 30 Dec 2023 17:54:52 -1000 Subject: [PATCH 180/300] Add a failed_result to all fail_json calls, more... Add failed_result property to ImageUpgradeCommon. Modify all fail_json() calls to add this result. This is the first phase of adding proper results to the dcnm_image_upgrade module. Phase 2 will add a Result() class and the needed infra to return proper success results. image_policies.py in class ImagePolicies, add property all_policies which return the current image policies on the controller. This was needed for dcnm_image_policy module. --- .../module_utils/image_mgmt/image_policies.py | 29 +++++-- .../image_mgmt/image_policy_action.py | 22 +++--- .../module_utils/image_mgmt/image_stage.py | 18 ++--- .../module_utils/image_mgmt/image_upgrade.py | 75 +++++++++---------- .../image_mgmt/image_upgrade_common.py | 60 +++++++++++++++ .../module_utils/image_mgmt/image_validate.py | 21 +++--- .../image_mgmt/install_options.py | 16 ++-- .../module_utils/image_mgmt/switch_details.py | 8 +- .../image_mgmt/switch_issu_details.py | 24 +++--- 9 files changed, 171 insertions(+), 102 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index e3bd7c03f..ec52a581e 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -19,6 +19,8 @@ __author__ = "Allen Robel" import inspect +import copy +from typing import Dict, Any, AnyStr from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -61,6 +63,7 @@ def __init__(self, module): def _init_properties(self): self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties = {} + self.properties["all_policies"] = None self.properties["policy_name"] = None self.properties["response_data"] = None self.properties["response"] = None @@ -82,7 +85,7 @@ def refresh(self): msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retriving image policy " msg += "information from the controller." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) data = self.response.get("DATA").get("lastOperDataObject") @@ -90,25 +93,27 @@ def refresh(self): msg = f"{self.class_name}.{self.method_name}: " msg += "Bad response when retrieving image policy " msg += "information from the controller." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if len(data) == 0: msg = f"{self.class_name}.{self.method_name}: " msg += "the controller has no defined image policies." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["response_data"] = {} - + self.properties["all_policies"] = {} for policy in data: policy_name = policy.get("policyName") if policy_name is None: msg = f"{self.class_name}.{self.method_name}: " msg += "Cannot parse policy information from the controller." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["response_data"][policy_name] = policy + self.properties["all_policies"] = copy.deepcopy(self.properties["response_data"]) + def _get(self, item): self.method_name = inspect.stack()[0][3] @@ -116,7 +121,7 @@ def _get(self, item): msg = f"{self.class_name}.{self.method_name}: " msg += "instance.policy_name must be set before " msg += f"accessing property {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.policy_name not in self.properties["response_data"]: return None @@ -127,12 +132,22 @@ def _get(self, item): if item not in self.properties["response_data"][self.policy_name]: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.policy_name} does not have a key named {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) return self.make_boolean( self.make_none(self.properties["response_data"][self.policy_name][item]) ) + + @property + def all_policies(self) -> Dict[AnyStr, Any]: + """ + Return dict containing all policies, keyed on policy_name + """ + if self.properties["all_policies"] is None: + return {} + return self.properties["all_policies"] + @property def description(self): """ diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 2ec927f99..529059d4c 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -112,7 +112,7 @@ def build_payload(self): msg += f"{self.switch_issu_details.device_name}. " msg += "Please verify that the switch is managed by " msg += "the controller." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.payloads.append(payload) def validate_request(self): @@ -125,13 +125,13 @@ def validate_request(self): msg = f"{self.class_name}.{method_name}: " msg += "instance.action must be set before " msg += "calling commit()" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.policy_name is None: msg = f"{self.class_name}.{method_name}: " msg += "instance.policy_name must be set before " msg += "calling commit()" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.action == "query": return @@ -140,7 +140,7 @@ def validate_request(self): msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_numbers must be set before " msg += "calling commit()" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.image_policies.refresh() self.switch_issu_details.refresh() @@ -155,7 +155,7 @@ def validate_request(self): msg += f"{self.switch_issu_details.platform}. {self.policy_name} " msg += "supports the following platform(s): " msg += f"{self.image_policies.platform}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) def commit(self): """ @@ -176,7 +176,7 @@ def commit(self): else: msg = f"{self.class_name}.{method_name}: " msg += f"Unknown action {self.action}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) def _attach_policy(self): """ @@ -211,7 +211,7 @@ def _attach_policy(self): msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " msg += f"to switch {payload['ipAddr']}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) responses.append(response) results.append(result) @@ -245,7 +245,7 @@ def _detach_policy(self): msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when detaching policy {self.policy_name} " msg += f"from the following device(s): {','.join(sorted(self.serial_numbers))}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) def _query_policy(self): """ @@ -266,7 +266,7 @@ def _query_policy(self): if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when querying image policy {self.policy_name}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["query_result"] = self.response.get("DATA") @@ -297,7 +297,7 @@ def action(self, value): msg += "instance.action must be one of " msg += f"{','.join(sorted(self.valid_actions))}. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["action"] = value @@ -355,5 +355,5 @@ def serial_numbers(self, value): msg += "instance.serial_numbers must be a " msg += "python list of switch serial numbers. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 12a5907d6..b1ae6f7fd 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -165,7 +165,7 @@ def validate_serial_numbers(self): msg += f"{self.issu_detail.serial_number}. " msg += "Check the switch connectivity to the controller " msg += "and try again." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) def commit(self): """ @@ -178,7 +178,7 @@ def commit(self): msg = f"{self.class_name}.{method_name}: " msg += "call instance.serial_numbers " msg += "before calling commit." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if len(self.serial_numbers) == 0: return @@ -208,7 +208,7 @@ def commit(self): msg = f"{self.class_name}.{method_name}: " msg = f"failed: {self.result}. " msg += f"Controller response: {self.response}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_stage_to_complete() @@ -246,7 +246,7 @@ def _wait_for_current_actions_to_complete(self): msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) def _wait_for_image_stage_to_complete(self): """ @@ -278,7 +278,7 @@ def _wait_for_image_stage_to_complete(self): msg += f"Seconds remaining {timeout}: stage image failed " msg += f"for {device_name}, {serial_number}, {ip_address}. " msg += f"image staged percent: {staged_percent}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if staged_status == "Success": self.serial_numbers_done.add(serial_number) @@ -290,7 +290,7 @@ def _wait_for_image_stage_to_complete(self): msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) @property def serial_numbers(self): @@ -308,7 +308,7 @@ def serial_numbers(self, value): msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_numbers must be a " msg += "python list of switch serial numbers." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value @property @@ -347,7 +347,7 @@ def check_interval(self, value): if not isinstance(value, int): msg = f"{self.__class__.__name__}: instance.check_interval must " msg += "be an integer." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @property @@ -362,5 +362,5 @@ def check_timeout(self, value): if not isinstance(value, int): msg = f"{self.__class__.__name__}: instance.check_timeout must " msg += "be an integer." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index c93361840..6ed828fd9 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -219,10 +219,6 @@ def _build_payload(self, device) -> None: """ method_name = inspect.stack()[0][3] - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"device FINAL: {json.dumps(device, indent=4, sort_keys=True)}" - self.log.log_msg(msg) - self.issu_detail.ip_address = device.get("ip_address") self.issu_detail.refresh() @@ -256,9 +252,6 @@ def _build_payload(self, device) -> None: self._build_payload_reboot_options(device) self._build_payload_package(device) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" - self.log.log_msg(msg) def _build_payload_issu_upgrade(self, device) -> None: """ @@ -272,7 +265,7 @@ def _build_payload_issu_upgrade(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "upgrade.nxos must be a boolean. " msg += f"Got {nxos_upgrade}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.payload["issuUpgrade"] = nxos_upgrade def _build_payload_issu_options_1(self, device) -> None: @@ -295,7 +288,7 @@ def _build_payload_issu_options_1(self, device) -> None: msg += "options.nxos.mode must be one of " msg += f"{sorted(self.valid_nxos_mode)}. " msg += f"Got {nxos_mode}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) verify_nxos_mode_list = [] if nxos_mode == "non_disruptive": @@ -320,7 +313,7 @@ def _build_payload_issu_options_2(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.nxos.bios_force must be a boolean. " msg += f"Got {bios_force}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.payload["issuUpgradeOptions2"] = {} self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force @@ -337,7 +330,7 @@ def _build_payload_epld(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "upgrade.epld must be a boolean. " msg += f"Got {epld_upgrade}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) epld_module = device.get("options").get("epld").get("module") epld_golden = device.get("options").get("epld").get("golden") @@ -347,7 +340,7 @@ def _build_payload_epld(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.epld.golden must be a boolean. " msg += f"Got {epld_golden}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if epld_golden is True and device.get("upgrade").get("nxos") is True: msg = f"{self.class_name}.{method_name}: " @@ -356,7 +349,7 @@ def _build_payload_epld(self, device) -> None: msg += "If options.epld.golden is True " msg += "all other upgrade options, e.g. upgrade.nxos, " msg += "must be False." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if epld_module != "ALL": try: @@ -365,7 +358,7 @@ def _build_payload_epld(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.epld.module must either be 'ALL' " msg += f"or an integer. Got {epld_module}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.payload["epldUpgrade"] = epld_upgrade self.payload["epldOptions"] = {} @@ -384,7 +377,7 @@ def _build_payload_reboot(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "reboot must be a boolean. " msg += f"Got {reboot}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.payload["reboot"] = reboot def _build_payload_reboot_options(self, device) -> None: @@ -401,14 +394,14 @@ def _build_payload_reboot_options(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.config_reload must be a boolean. " msg += f"Got {config_reload}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) write_erase = self.make_boolean(write_erase) if not isinstance(write_erase, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.write_erase must be a boolean. " msg += f"Got {write_erase}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.payload["rebootOptions"] = {} self.payload["rebootOptions"]["configReload"] = config_reload @@ -432,14 +425,14 @@ def _build_payload_package(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.package.install must be a boolean. " msg += f"Got {package_install}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) package_uninstall = self.make_boolean(package_uninstall) if not isinstance(package_uninstall, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.package.uninstall must be a boolean. " msg += f"Got {package_uninstall}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) # Yes, these keys are misspelled. The controller # wants them to be misspelled. Need to keep an @@ -461,7 +454,7 @@ def commit(self) -> None: if self.devices is None: msg = f"{self.class_name}.{method_name}: " msg += "call instance.devices before calling commit." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self._validate_devices() self._wait_for_current_actions_to_complete() @@ -499,7 +492,7 @@ def commit(self) -> None: msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.result}. " msg += f"Controller response: {self.response}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_upgrade_to_complete() @@ -540,7 +533,7 @@ def _wait_for_current_actions_to_complete(self): msg += f"{','.join(sorted(self.ipv4_todo))}. " msg += "check the device(s) to determine the cause " msg += "(e.g. show install all status)." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) def _wait_for_image_upgrade_to_complete(self): """ @@ -578,7 +571,7 @@ def _wait_for_image_upgrade_to_complete(self): msg += "Operations > Image Management > Devices > View Details. " msg += "And/or check the devices " msg += "(e.g. show install all status)." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if upgrade_status == "Success": self.ipv4_done.add(ipv4) @@ -591,7 +584,7 @@ def _wait_for_image_upgrade_to_complete(self): msg += "Operations > Image Management > Devices > View Details. " msg += "And/or check the device(s) " msg += "(e.g. show install all status)." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) # setter properties @property @@ -609,7 +602,7 @@ def bios_force(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.bios_force must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["bios_force"] = value @property @@ -627,7 +620,7 @@ def config_reload(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.config_reload must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["config_reload"] = value @property @@ -652,20 +645,20 @@ def devices(self, value: List[Dict]): msg = f"{self.class_name}.{method_name}: " msg += "instance.devices must be a python list of dict. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) for device in value: if not isinstance(device, dict): msg = f"{self.class_name}.{method_name}: " msg += "instance.devices must be a python list of dict. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if "ip_address" not in device: msg = f"{self.class_name}.{method_name}: " msg += "instance.devices must be a python list of dict, " msg += "where each dict contains the following keys: " msg += "ip_address. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["devices"] = value @property @@ -683,7 +676,7 @@ def disruptive(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.disruptive must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["disruptive"] = value @property @@ -701,7 +694,7 @@ def epld_golden(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.epld_golden must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["epld_golden"] = value @property @@ -719,7 +712,7 @@ def epld_upgrade(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.epld_upgrade must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["epld_upgrade"] = value @property @@ -747,7 +740,7 @@ def epld_module(self, value): if not isinstance(value, int) and value != "ALL": msg = f"{self.class_name}.{method_name}: " msg += "instance.epld_module must be an integer or 'ALL'" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["epld_module"] = value @property @@ -765,7 +758,7 @@ def force_non_disruptive(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.force_non_disruptive must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["force_non_disruptive"] = value @property @@ -783,7 +776,7 @@ def non_disruptive(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.non_disruptive must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["non_disruptive"] = value @property @@ -801,7 +794,7 @@ def package_install(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.package_install must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["package_install"] = value @property @@ -819,7 +812,7 @@ def package_uninstall(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.package_uninstall must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["package_uninstall"] = value @property @@ -837,7 +830,7 @@ def reboot(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.reboot must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["reboot"] = value @property @@ -855,7 +848,7 @@ def write_erase(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.write_erase must be a boolean." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["write_erase"] = value @property @@ -870,7 +863,7 @@ def check_interval(self, value): if not isinstance(value, int): msg = f"{self.__class__.__name__}: instance.check_interval must " msg += "be an integer." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @property @@ -885,7 +878,7 @@ def check_timeout(self, value): if not isinstance(value, int): msg = f"{self.__class__.__name__}: instance.check_timeout must " msg += "be an integer." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value # getter properties diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 4dbe8ea78..7bc832212 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -149,3 +149,63 @@ def make_none(self, value): if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: return None return value + + @property + def failed_result(self): + """ + return a result for a failed task with no changes + """ + result = {} + result["changed"] = False + result["diff"] = [] + return result + + @property + def changed(self): + """ + bool = whether we changed anything + """ + return self.properties["changed"] + + @changed.setter + def changed(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"changed must be a bool. Got {value}" + self.ansible_module.fail_json(msg) + self.properties["changed"] = value + + @property + def diff(self): + """ + List of dicts representing the changes made + """ + return self.properties["diff"] + + @diff.setter + def diff(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += f"diff must be a dict. Got {value}" + self.ansible_module.fail_json(msg) + self.properties["diff"].append(value) + + @property + def failed(self): + """ + bool = whether we failed or not + If True, this means we failed to make a change + If False, this means we succeeded in making a change + """ + return self.properties["failed"] + + @failed.setter + def failed(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"failed must be a bool. Got {value}" + self.ansible_module.fail_json(msg) + self.properties["failed"] = value diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 19b937e36..b8881e336 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -134,7 +134,7 @@ def validate_serial_numbers(self) -> None: msg += f"{self.issu_detail.serial_number}. " msg += "If this persists, check the switch connectivity to " msg += "the controller and try again." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) def build_payload(self) -> None: """ @@ -157,7 +157,7 @@ def commit(self) -> None: msg = f"{self.class_name}.{self.method_name}: " msg += "call instance.serial_numbers before " msg += "calling commit." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if len(self.serial_numbers) == 0: return @@ -176,7 +176,7 @@ def commit(self) -> None: msg = f"{self.class_name}.{self.method_name}: " msg = f"failed: {self.result}. " msg += f"Controller response: {self.response}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_validate_to_complete() @@ -200,6 +200,7 @@ def _wait_for_current_actions_to_complete(self) -> None: for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue + self.issu_detail.serial_number = serial_number self.issu_detail.refresh() @@ -213,7 +214,7 @@ def _wait_for_current_actions_to_complete(self) -> None: msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) def _wait_for_image_validate_to_complete(self) -> None: """ @@ -252,7 +253,7 @@ def _wait_for_image_validate_to_complete(self) -> None: msg += "check Operations > Image Management > " msg += "Devices > View Details > Validate on the " msg += "controller GUI for more details." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if validated_status == "Success": self.serial_numbers_done.add(serial_number) @@ -264,7 +265,7 @@ def _wait_for_image_validate_to_complete(self) -> None: msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) @property def serial_numbers(self) -> List[str]: @@ -284,7 +285,7 @@ def serial_numbers(self, value: List[str]): msg += "instance.serial_numbers must be a " msg += "python list of switch serial numbers. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value @@ -304,7 +305,7 @@ def non_disruptive(self, value): msg = f"{self.class_name}.{self.method_name}: " msg += "instance.non_disruptive must be a boolean. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["non_disruptive"] = value @@ -353,7 +354,7 @@ def check_interval(self, value): msg = f"{self.class_name}.{self.method_name}: " msg += "instance.check_interval must be an integer. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @@ -378,5 +379,5 @@ def check_timeout(self, value): msg = f"{self.class_name}.{self.method_name}: " msg += "instance.check_timeout must be an integer. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 18ecfb5b1..8feb29ac0 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -172,13 +172,13 @@ def refresh(self) -> None: msg = f"{self.class_name}.{method_name}: " msg += "instance.policy_name must be set before " msg += "calling refresh()" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.serial_number is None: msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_number must be set before " msg += "calling refresh()" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) # At least one of epld, issu, or package_install must be True # before calling refresh() or the controller will return an error. @@ -211,14 +211,14 @@ def refresh(self) -> None: msg += "Bad result when retrieving install-options from " msg += f"the controller. Controller response: {self.response}. " if self.response_data.get("error", None) is None: - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if "does not have package to continue" in self.response_data.get( "error", "" ): msg += f"Possible cause: Image policy {self.policy_name} does not have " msg += "a package defined, and package_install is set to " msg += f"True in the playbook for device {self.serial_number}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.response_data.get("compatibilityStatusList") is None: self.compatibility_status = {} @@ -271,7 +271,7 @@ def policy_name(self, value): if not isinstance(value, str): msg = f"{self.class_name}.{method_name}: " msg += f"policy_name must be a string. Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["policy_name"] = value @property @@ -304,7 +304,7 @@ def issu(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"issu must be a boolean value. Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["issu"] = value @property @@ -326,7 +326,7 @@ def epld(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"epld must be a boolean value. Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["epld"] = value @property @@ -348,7 +348,7 @@ def package_install(self, value): msg = f"{self.class_name}.{method_name}: " msg += "package_install must be a boolean value. " msg += f"Got {value}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["package_install"] = value # Retrievable properties diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index f4816eacc..ea28279de 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -84,7 +84,7 @@ def refresh(self): msg = f"{self.class_name}.{self.method_name}: " msg += "Unable to retrieve switch information from the controller. " msg += f"Got response {self.response}" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) data = self.response.get("DATA") self.properties["response_data"] = {} @@ -98,17 +98,17 @@ def _get(self, item): msg = f"{self.class_name}.{self.method_name}: " msg += "set instance.ip_address before accessing " msg += f"property {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.ip_address not in self.properties["response_data"]: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.ip_address} does not exist on the controller." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if item not in self.properties["response_data"][self.ip_address]: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.ip_address} does not have a key named {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) return self.make_boolean( self.make_none(self.properties["response_data"][self.ip_address].get(item)) diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 3a2600b6b..d61e324f8 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -123,19 +123,19 @@ def refresh(self) -> None: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retriving switch " msg += "information from the controller" - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) data = self.response.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.{self.method_name}: " msg += "The controller has no switch ISSU information." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if len(data) == 0: msg = f"{self.class_name}.{self.method_name}: " msg += "The controller has no switch ISSU information." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) self.properties["response_data"] = self.response.get("DATA", {}).get( "lastOperDataObject", [] @@ -705,17 +705,17 @@ def _get(self, item): msg = f"{self.class_name}.{self.method_name}: " msg += "set instance.ip_address before accessing " msg += f"property {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.ip_address) is None: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.ip_address} does not exist on the controller." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.data_subclass[self.ip_address].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.ip_address} unknown property name: {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) return self.make_none( self.make_boolean(self.data_subclass[self.ip_address].get(item)) @@ -793,18 +793,18 @@ def _get(self, item): msg = f"{self.class_name}.{self.method_name}: " msg += "set instance.serial_number before " msg += f"accessing property {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.serial_number) is None: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.serial_number} does not exist " msg += "on the controller." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.data_subclass[self.serial_number].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.serial_number} unknown property name: {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) return self.make_none( self.make_boolean(self.data_subclass[self.serial_number].get(item)) @@ -880,18 +880,18 @@ def _get(self, item): msg = f"{self.class_name}.{self.method_name}: " msg += "set instance.device_name before " msg += f"accessing property {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.device_name) is None: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.device_name} does not exist " msg += "on the controller." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) if self.data_subclass[self.device_name].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.device_name} unknown property name: {item}." - self.module.fail_json(msg) + self.module.fail_json(msg, **self.failed_result) return self.make_none( self.make_boolean(self.data_subclass[self.device_name].get(item)) From f8b803707f5789c74678bd8b33b3afa367422a7a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 30 Dec 2023 18:05:24 -1000 Subject: [PATCH 181/300] Fix mocked fail_json() to add **kwargs required by last commit. --- .../modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index 2d1b28f62..291eb2c5e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -65,11 +65,11 @@ class MockAnsibleModule: supports_check_mode = True @staticmethod - def fail_json(msg) -> AnsibleFailJson: + def fail_json(msg, **kwargs) -> AnsibleFailJson: """ mock the fail_json method """ - raise AnsibleFailJson(msg) + raise AnsibleFailJson(msg, kwargs) def public_method_for_pylint(self) -> Any: """ From 4a535aa295abf6e049ca47c685f32b4f48817ede Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 30 Dec 2023 18:11:56 -1000 Subject: [PATCH 182/300] Fix ansible-sanity complaints --- plugins/module_utils/image_mgmt/image_policies.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index ec52a581e..6761209a0 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -18,9 +18,9 @@ __metaclass__ = type __author__ = "Allen Robel" -import inspect import copy -from typing import Dict, Any, AnyStr +import inspect +from typing import Any, AnyStr, Dict from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -112,7 +112,9 @@ def refresh(self): self.properties["response_data"][policy_name] = policy - self.properties["all_policies"] = copy.deepcopy(self.properties["response_data"]) + self.properties["all_policies"] = copy.deepcopy( + self.properties["response_data"] + ) def _get(self, item): self.method_name = inspect.stack()[0][3] @@ -138,7 +140,6 @@ def _get(self, item): self.make_none(self.properties["response_data"][self.policy_name][item]) ) - @property def all_policies(self) -> Dict[AnyStr, Any]: """ From dc5eec77c362165c4a59774d44b3773aa6d61490 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 30 Dec 2023 18:17:36 -1000 Subject: [PATCH 183/300] Fix ansible-sanity errors --- plugins/module_utils/image_mgmt/image_upgrade.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 6ed828fd9..dec8be6c1 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -252,7 +252,6 @@ def _build_payload(self, device) -> None: self._build_payload_reboot_options(device) self._build_payload_package(device) - def _build_payload_issu_upgrade(self, device) -> None: """ Build the issuUpgrade portion of the payload. From 6390ee44dd4d5ab0b465bbc5c23e2098f9998b03 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 31 Dec 2023 11:17:24 -1000 Subject: [PATCH 184/300] SwitchIssuDetails: change filtering key name, more... In preparation for refactoring, we are changing the key name used to set which device's information we retrieve. Previously, each of the subclasses used a different key name: SwitchIssuDetailsByIpAddress used ip_address SwitchIssuDetailsBySerial_number used serial_number SwitchIssuDetailsByDeviceName used device_name We have changed the above such that all of them now use "retrieval_key" to set the filter. This will allow us to refactor now only this class, but also we'll be able to refactor _wait_for_current_actions_to_complete() from ImageStage(), ImageUpgrade() and ImageValidate() into a single function in ImageUpgradeCommon() --- .../image_mgmt/image_policy_action.py | 4 +- .../module_utils/image_mgmt/image_stage.py | 8 +- .../module_utils/image_mgmt/image_upgrade.py | 8 +- .../module_utils/image_mgmt/image_validate.py | 8 +- .../image_mgmt/switch_issu_details.py | 100 +++++++++--------- plugins/modules/dcnm_image_upgrade.py | 10 +- .../test_image_upgrade_image_upgrade_task.py | 4 +- ...rade_switch_issu_details_by_device_name.py | 8 +- ...grade_switch_issu_details_by_ip_address.py | 8 +- ...de_switch_issu_details_by_serial_number.py | 8 +- 10 files changed, 83 insertions(+), 83 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 529059d4c..99ac09545 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -96,7 +96,7 @@ def build_payload(self): self.switch_issu_details.refresh() for serial_number in self.serial_numbers: - self.switch_issu_details.serial_number = serial_number + self.switch_issu_details.retrieval_key = serial_number payload = {} payload["policyName"] = self.policy_name payload["hostName"] = self.switch_issu_details.device_name @@ -148,7 +148,7 @@ def validate_request(self): # Fail if the image policy does not support the switch platform self.image_policies.policy_name = self.policy_name for serial_number in self.serial_numbers: - self.switch_issu_details.serial_number = serial_number + self.switch_issu_details.retrieval_key = serial_number if self.switch_issu_details.platform not in self.image_policies.platform: msg = f"{self.class_name}.{method_name}: " msg += f"policy {self.policy_name} does not support platform " diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index b1ae6f7fd..afdcbcce7 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -142,7 +142,7 @@ def prune_serial_numbers(self): method_name = inspect.stack()[0][3] # pylint: disable=unused-variable serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: - self.issu_detail.serial_number = serial_number + self.issu_detail.retrieval_key = serial_number self.issu_detail.refresh() if self.issu_detail.image_staged == "Success": self.serial_numbers.remove(serial_number) @@ -154,7 +154,7 @@ def validate_serial_numbers(self): """ method_name = inspect.stack()[0][3] for serial_number in self.serial_numbers: - self.issu_detail.serial_number = serial_number + self.issu_detail.retrieval_key = serial_number self.issu_detail.refresh() if self.issu_detail.image_staged == "Failed": @@ -233,7 +233,7 @@ def _wait_for_current_actions_to_complete(self): if serial_number in self.serial_numbers_done: continue - self.issu_detail.serial_number = serial_number + self.issu_detail.retrieval_key = serial_number self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: @@ -266,7 +266,7 @@ def _wait_for_image_stage_to_complete(self): if serial_number in self.serial_numbers_done: continue - self.issu_detail.serial_number = serial_number + self.issu_detail.retrieval_key = serial_number self.issu_detail.refresh() ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index dec8be6c1..7f8fe26aa 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -200,7 +200,7 @@ def _validate_devices(self) -> None: method_name = inspect.stack()[0][3] # pylint: disable=unused-variable for device in self.devices: - self.issu_detail.ip_address = device.get("ip_address") + self.issu_detail.retrieval_key = device.get("ip_address") self.issu_detail.refresh() # Any device validation from issu_detail would go here. @@ -219,7 +219,7 @@ def _build_payload(self, device) -> None: """ method_name = inspect.stack()[0][3] - self.issu_detail.ip_address = device.get("ip_address") + self.issu_detail.retrieval_key = device.get("ip_address") self.issu_detail.refresh() self.install_options.serial_number = self.issu_detail.serial_number @@ -516,7 +516,7 @@ def _wait_for_current_actions_to_complete(self): if ipv4 in self.ipv4_done: continue - self.issu_detail.ip_address = ipv4 + self.issu_detail.retrieval_key = ipv4 self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: @@ -552,7 +552,7 @@ def _wait_for_image_upgrade_to_complete(self): if ipv4 in self.ipv4_done: continue - self.issu_detail.ip_address = ipv4 + self.issu_detail.retrieval_key = ipv4 self.issu_detail.refresh() ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index b8881e336..a27bf42ec 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -104,7 +104,7 @@ def prune_serial_numbers(self) -> None: self.issu_detail.refresh() serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: - self.issu_detail.serial_number = serial_number + self.issu_detail.retrieval_key = serial_number if self.issu_detail.validated == "Success": self.serial_numbers.remove(self.issu_detail.serial_number) @@ -124,7 +124,7 @@ def validate_serial_numbers(self) -> None: self.method_name = inspect.stack()[0][3] for serial_number in self.serial_numbers: - self.issu_detail.serial_number = serial_number + self.issu_detail.retrieval_key = serial_number self.issu_detail.refresh() if self.issu_detail.validated == "Failed": msg = f"{self.class_name}.{self.method_name}: " @@ -201,7 +201,7 @@ def _wait_for_current_actions_to_complete(self) -> None: if serial_number in self.serial_numbers_done: continue - self.issu_detail.serial_number = serial_number + self.issu_detail.retrieval_key = serial_number self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: @@ -234,7 +234,7 @@ def _wait_for_image_validate_to_complete(self) -> None: if serial_number in self.serial_numbers_done: continue - self.issu_detail.serial_number = serial_number + self.issu_detail.retrieval_key = serial_number self.issu_detail.refresh() ip_address = self.issu_detail.ip_address diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index d61e324f8..2b5960158 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -667,7 +667,7 @@ class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): instance = SwitchIssuDetailsByIpAddress(module) instance.refresh() - instance.ip_address = 10.1.1.1 + instance.retrieval_key = "10.1.1.1" image_staged = instance.image_staged image_upgraded = instance.image_upgraded serial_number = instance.serial_number @@ -684,7 +684,7 @@ def __init__(self, module): def _init_properties(self): super()._init_properties() self.method_name = inspect.stack()[0][3] - self.properties["ip_address"] = None + self.properties["retrieval_key"] = None def refresh(self): """ @@ -701,46 +701,46 @@ def refresh(self): def _get(self, item): self.method_name = inspect.stack()[0][3] - if self.ip_address is None: + if self.retrieval_key is None: msg = f"{self.class_name}.{self.method_name}: " - msg += "set instance.ip_address before accessing " - msg += f"property {item}." + msg += "set instance.retrieval_key to a switch ipAddress " + msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass.get(self.ip_address) is None: + if self.data_subclass.get(self.retrieval_key) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.ip_address} does not exist on the controller." + msg += f"{self.retrieval_key} does not exist on the controller." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass[self.ip_address].get(item) is None: + if self.data_subclass[self.retrieval_key].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.ip_address} unknown property name: {item}." + msg += f"{self.retrieval_key} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) return self.make_none( - self.make_boolean(self.data_subclass[self.ip_address].get(item)) + self.make_boolean(self.data_subclass[self.retrieval_key].get(item)) ) @property def filtered_data(self): """ - Return a dictionary of the switch matching self.ip_address. + Return a dictionary of the switch matching self.retrieval_key. Return None if the switch does not exist on the controller. """ - return self.data_subclass.get(self.ip_address) + return self.data_subclass.get(self.retrieval_key) @property - def ip_address(self): + def retrieval_key(self): """ Set the ip_address of the switch to query. This needs to be set before accessing this class's properties. """ - return self.properties.get("ip_address") + return self.properties.get("retrieval_key") - @ip_address.setter - def ip_address(self, value): - self.properties["ip_address"] = value + @retrieval_key.setter + def retrieval_key(self, value): + self.properties["retrieval_key"] = value class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): @@ -752,7 +752,7 @@ class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): instance = SwitchIssuDetailsBySerialNumber(module) instance.refresh() - instance.serial_number = "FDO211218GC" + instance.retrieval_key = "FDO211218GC" instance.refresh() image_staged = instance.image_staged image_upgraded = instance.image_upgraded @@ -771,7 +771,7 @@ def __init__(self, module): def _init_properties(self): super()._init_properties() self.method_name = inspect.stack()[0][3] - self.properties["serial_number"] = None + self.properties["retrieval_key"] = None def refresh(self): """ @@ -789,25 +789,25 @@ def refresh(self): def _get(self, item): self.method_name = inspect.stack()[0][3] - if self.serial_number is None: + if self.retrieval_key is None: msg = f"{self.class_name}.{self.method_name}: " - msg += "set instance.serial_number before " - msg += f"accessing property {item}." + msg += "set instance.retrieval_key to a switch serialNumber " + msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass.get(self.serial_number) is None: + if self.data_subclass.get(self.retrieval_key) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.serial_number} does not exist " + msg += f"{self.retrieval_key} does not exist " msg += "on the controller." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass[self.serial_number].get(item) is None: + if self.data_subclass[self.retrieval_key].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.serial_number} unknown property name: {item}." + msg += f"{self.retrieval_key} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) return self.make_none( - self.make_boolean(self.data_subclass[self.serial_number].get(item)) + self.make_boolean(self.data_subclass[self.retrieval_key].get(item)) ) @property @@ -816,20 +816,20 @@ def filtered_data(self): Return a dictionary of the switch matching self.serial_number. Return None of the switch does not exist in NDFC. """ - return self.data_subclass.get(self.serial_number) + return self.data_subclass.get(self.retrieval_key) @property - def serial_number(self): + def retrieval_key(self): """ Set the serial_number of the switch to query. This needs to be set before accessing this class's properties. """ - return self.properties.get("serial_number") + return self.properties.get("retrieval_key") - @serial_number.setter - def serial_number(self, value): - self.properties["serial_number"] = value + @retrieval_key.setter + def retrieval_key(self, value): + self.properties["retrieval_key"] = value class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): @@ -841,7 +841,7 @@ class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): instance = SwitchIssuDetailsByDeviceName(module) instance.refresh() - instance.device_name = "leaf_1" + instance.retrieval_key = "leaf_1" image_staged = instance.image_staged image_upgraded = instance.image_upgraded ip_address = instance.ip_address @@ -859,7 +859,7 @@ def __init__(self, module): def _init_properties(self): super()._init_properties() method_name = inspect.stack()[0][3] - self.properties["device_name"] = None + self.properties["retrieval_key"] = None def refresh(self): """ @@ -876,44 +876,44 @@ def refresh(self): def _get(self, item): self.method_name = inspect.stack()[0][3] - if self.device_name is None: + if self.retrieval_key is None: msg = f"{self.class_name}.{self.method_name}: " - msg += "set instance.device_name before " - msg += f"accessing property {item}." + msg += "set instance.retrieval_key to a switch deviceName " + msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass.get(self.device_name) is None: + if self.data_subclass.get(self.retrieval_key) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.device_name} does not exist " + msg += f"{self.retrieval_key} does not exist " msg += "on the controller." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass[self.device_name].get(item) is None: + if self.data_subclass[self.retrieval_key].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.device_name} unknown property name: {item}." + msg += f"{self.retrieval_key} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) return self.make_none( - self.make_boolean(self.data_subclass[self.device_name].get(item)) + self.make_boolean(self.data_subclass[self.retrieval_key].get(item)) ) @property def filtered_data(self): """ - Return a dictionary of the switch matching self.device_name. + Return a dictionary of the switch matching self.retrieval_key. Return None of the switch does not exist in NDFC. """ - return self.data_subclass.get(self.device_name) + return self.data_subclass.get(self.retrieval_key) @property - def device_name(self): + def retrieval_key(self): """ Set the device_name of the switch to query. This needs to be set before accessing this class's properties. """ - return self.properties.get("device_name") + return self.properties.get("retrieval_key") - @device_name.setter - def device_name(self, value): - self.properties["device_name"] = value + @retrieval_key.setter + def retrieval_key(self, value): + self.properties["retrieval_key"] = value diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 6d6b1c7aa..1846cadfb 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -573,7 +573,7 @@ def _build_idempotent_want(self, want) -> None: msg += f"want: {json.dumps(want, indent=4, sort_keys=True)}" self.log.log_msg(msg) - self.have.ip_address = want["ip_address"] + self.have.retrieval_key = want["ip_address"] want["policy_changed"] = True # The switch does not have an image policy attached. @@ -664,7 +664,7 @@ def get_need_merged(self) -> None: self.log.log_msg(msg) for want in self.want: - self.have.ip_address = want["ip_address"] + self.have.retrieval_key = want["ip_address"] msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"self.have.serial_number: {self.have.serial_number}" @@ -708,7 +708,7 @@ def get_need_deleted(self) -> None: need = [] for want in self.want: - self.have.ip_address = want["ip_address"] + self.have.retrieval_key = want["ip_address"] if self.have.serial_number is None: continue if self.have.policy is None: @@ -1270,7 +1270,7 @@ def handle_merged_state(self) -> None: self.switch_details.ip_address = switch.get("ip_address") device = {} device["serial_number"] = self.switch_details.serial_number - self.have.ip_address = self.switch_details.ip_address + self.have.retrieval_key = self.switch_details.ip_address device["policy_name"] = switch.get("policy") device["ip_address"] = self.switch_details.ip_address @@ -1337,7 +1337,7 @@ def handle_query_state(self) -> None: query_devices: List[Dict[str, Any]] = [] for switch in self.need: - instance.ip_address = switch.get("ip_address") + instance.retrieval_key = switch.get("ip_address") if instance.filtered_data is None: continue query_devices.append(instance.filtered_data) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 089e13b46..81371d319 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -130,9 +130,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance = image_upgrade_task instance.get_have() - instance.have.ip_address = "1.1.1.1" + instance.have.retrieval_key = "1.1.1.1" assert instance.have.device_name == "leaf1" - instance.have.ip_address = "2.2.2.2" + instance.have.retrieval_key = "2.2.2.2" assert instance.have.device_name == "cvd-2313-leaf" assert instance.have.serial_number == "FDO2112189M" assert instance.have.fabric == "hard" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index 82cc482f9..ea8e746fe 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -130,11 +130,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.device_name = "leaf1" + instance.retrieval_key = "leaf1" assert instance.device_name == "leaf1" assert instance.serial_number == "FDO21120U5D" # change device_name to a different switch, expect different information - instance.device_name = "cvd-2313-leaf" + instance.retrieval_key = "cvd-2313-leaf" assert instance.device_name == "cvd-2313-leaf" assert instance.serial_number == "FDO2112189M" # verify remaining properties using current device_name @@ -319,7 +319,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.device_name = "FOO" + instance.retrieval_key = "FOO" match = "SwitchIssuDetailsByDeviceName._get: FOO does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): @@ -353,7 +353,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.device_name = "leaf1" + instance.retrieval_key = "leaf1" match = "SwitchIssuDetailsByDeviceName._get: leaf1 unknown " match += "property name: FOO" with pytest.raises(AnsibleFailJson, match=match): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index e5ce0bbe1..233e53ab1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -132,11 +132,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.ip_address = "172.22.150.102" + instance.retrieval_key = "172.22.150.102" assert instance.device_name == "leaf1" assert instance.serial_number == "FDO21120U5D" # change ip_address to a different switch, expect different information - instance.ip_address = "172.22.150.108" + instance.retrieval_key = "172.22.150.108" assert instance.device_name == "cvd-2313-leaf" assert instance.serial_number == "FDO2112189M" # verify remaining properties using current ip_address @@ -322,7 +322,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.ip_address = "1.1.1.1" + instance.retrieval_key = "1.1.1.1" match = "SwitchIssuDetailsByIpAddress._get: 1.1.1.1 does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): @@ -355,7 +355,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.ip_address = "172.22.150.102" + instance.retrieval_key = "172.22.150.102" match = "SwitchIssuDetailsByIpAddress._get: 172.22.150.102 unknown " match += "property name: FOO" with pytest.raises(AnsibleFailJson, match=match): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index b15728e96..305c48b9f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -132,11 +132,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.serial_number = "FDO21120U5D" + instance.retrieval_key = "FDO21120U5D" assert instance.device_name == "leaf1" assert instance.serial_number == "FDO21120U5D" # change serial_number to a different switch, expect different information - instance.serial_number = "FDO2112189M" + instance.retrieval_key = "FDO2112189M" assert instance.device_name == "cvd-2313-leaf" assert instance.serial_number == "FDO2112189M" # verify remaining properties using current serial_number @@ -322,7 +322,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "on the controller." instance.refresh() - instance.serial_number = "FOO00000BAR" + instance.retrieval_key = "FOO00000BAR" with pytest.raises(AnsibleFailJson, match=match): instance._get("serialNumber") # pylint: disable=protected-access @@ -356,6 +356,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "property name: FOO" instance.refresh() - instance.serial_number = "FDO21120U5D" + instance.retrieval_key = "FDO21120U5D" with pytest.raises(AnsibleFailJson, match=match): instance._get("FOO") # pylint: disable=protected-access From a199ba11bb0090fe8ec2242f906ea67fd23c76c2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 31 Dec 2023 11:33:36 -1000 Subject: [PATCH 185/300] Change "retrieval_key" to "filter", more... Since these classes already have a "filtered_data" property which is used to return the data filtered by "retrieval_key", it's more intuitive to change the name of "retrieval_key" to "filter". --- .../image_mgmt/image_policy_action.py | 4 +- .../module_utils/image_mgmt/image_stage.py | 8 +- .../module_utils/image_mgmt/image_upgrade.py | 8 +- .../module_utils/image_mgmt/image_validate.py | 8 +- .../image_mgmt/switch_issu_details.py | 94 +++++++++---------- plugins/modules/dcnm_image_upgrade.py | 10 +- .../test_image_upgrade_image_upgrade_task.py | 4 +- ...rade_switch_issu_details_by_device_name.py | 8 +- ...grade_switch_issu_details_by_ip_address.py | 8 +- ...de_switch_issu_details_by_serial_number.py | 8 +- 10 files changed, 80 insertions(+), 80 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 99ac09545..fce2baace 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -96,7 +96,7 @@ def build_payload(self): self.switch_issu_details.refresh() for serial_number in self.serial_numbers: - self.switch_issu_details.retrieval_key = serial_number + self.switch_issu_details.filter = serial_number payload = {} payload["policyName"] = self.policy_name payload["hostName"] = self.switch_issu_details.device_name @@ -148,7 +148,7 @@ def validate_request(self): # Fail if the image policy does not support the switch platform self.image_policies.policy_name = self.policy_name for serial_number in self.serial_numbers: - self.switch_issu_details.retrieval_key = serial_number + self.switch_issu_details.filter = serial_number if self.switch_issu_details.platform not in self.image_policies.platform: msg = f"{self.class_name}.{method_name}: " msg += f"policy {self.policy_name} does not support platform " diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index afdcbcce7..55c42cf64 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -142,7 +142,7 @@ def prune_serial_numbers(self): method_name = inspect.stack()[0][3] # pylint: disable=unused-variable serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: - self.issu_detail.retrieval_key = serial_number + self.issu_detail.filter = serial_number self.issu_detail.refresh() if self.issu_detail.image_staged == "Success": self.serial_numbers.remove(serial_number) @@ -154,7 +154,7 @@ def validate_serial_numbers(self): """ method_name = inspect.stack()[0][3] for serial_number in self.serial_numbers: - self.issu_detail.retrieval_key = serial_number + self.issu_detail.filter = serial_number self.issu_detail.refresh() if self.issu_detail.image_staged == "Failed": @@ -233,7 +233,7 @@ def _wait_for_current_actions_to_complete(self): if serial_number in self.serial_numbers_done: continue - self.issu_detail.retrieval_key = serial_number + self.issu_detail.filter = serial_number self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: @@ -266,7 +266,7 @@ def _wait_for_image_stage_to_complete(self): if serial_number in self.serial_numbers_done: continue - self.issu_detail.retrieval_key = serial_number + self.issu_detail.filter = serial_number self.issu_detail.refresh() ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 7f8fe26aa..9309e7982 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -200,7 +200,7 @@ def _validate_devices(self) -> None: method_name = inspect.stack()[0][3] # pylint: disable=unused-variable for device in self.devices: - self.issu_detail.retrieval_key = device.get("ip_address") + self.issu_detail.filter = device.get("ip_address") self.issu_detail.refresh() # Any device validation from issu_detail would go here. @@ -219,7 +219,7 @@ def _build_payload(self, device) -> None: """ method_name = inspect.stack()[0][3] - self.issu_detail.retrieval_key = device.get("ip_address") + self.issu_detail.filter = device.get("ip_address") self.issu_detail.refresh() self.install_options.serial_number = self.issu_detail.serial_number @@ -516,7 +516,7 @@ def _wait_for_current_actions_to_complete(self): if ipv4 in self.ipv4_done: continue - self.issu_detail.retrieval_key = ipv4 + self.issu_detail.filter = ipv4 self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: @@ -552,7 +552,7 @@ def _wait_for_image_upgrade_to_complete(self): if ipv4 in self.ipv4_done: continue - self.issu_detail.retrieval_key = ipv4 + self.issu_detail.filter = ipv4 self.issu_detail.refresh() ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index a27bf42ec..bb08346fe 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -104,7 +104,7 @@ def prune_serial_numbers(self) -> None: self.issu_detail.refresh() serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: - self.issu_detail.retrieval_key = serial_number + self.issu_detail.filter = serial_number if self.issu_detail.validated == "Success": self.serial_numbers.remove(self.issu_detail.serial_number) @@ -124,7 +124,7 @@ def validate_serial_numbers(self) -> None: self.method_name = inspect.stack()[0][3] for serial_number in self.serial_numbers: - self.issu_detail.retrieval_key = serial_number + self.issu_detail.filter = serial_number self.issu_detail.refresh() if self.issu_detail.validated == "Failed": msg = f"{self.class_name}.{self.method_name}: " @@ -201,7 +201,7 @@ def _wait_for_current_actions_to_complete(self) -> None: if serial_number in self.serial_numbers_done: continue - self.issu_detail.retrieval_key = serial_number + self.issu_detail.filter = serial_number self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: @@ -234,7 +234,7 @@ def _wait_for_image_validate_to_complete(self) -> None: if serial_number in self.serial_numbers_done: continue - self.issu_detail.retrieval_key = serial_number + self.issu_detail.filter = serial_number self.issu_detail.refresh() ip_address = self.issu_detail.ip_address diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 2b5960158..d16203774 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -667,7 +667,7 @@ class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): instance = SwitchIssuDetailsByIpAddress(module) instance.refresh() - instance.retrieval_key = "10.1.1.1" + instance.filter = "10.1.1.1" image_staged = instance.image_staged image_upgraded = instance.image_upgraded serial_number = instance.serial_number @@ -684,7 +684,7 @@ def __init__(self, module): def _init_properties(self): super()._init_properties() self.method_name = inspect.stack()[0][3] - self.properties["retrieval_key"] = None + self.properties["filter"] = None def refresh(self): """ @@ -701,46 +701,46 @@ def refresh(self): def _get(self, item): self.method_name = inspect.stack()[0][3] - if self.retrieval_key is None: + if self.filter is None: msg = f"{self.class_name}.{self.method_name}: " - msg += "set instance.retrieval_key to a switch ipAddress " + msg += "set instance.filter to a switch ipAddress " msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass.get(self.retrieval_key) is None: + if self.data_subclass.get(self.filter) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.retrieval_key} does not exist on the controller." + msg += f"{self.filter} does not exist on the controller." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass[self.retrieval_key].get(item) is None: + if self.data_subclass[self.filter].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.retrieval_key} unknown property name: {item}." + msg += f"{self.filter} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) return self.make_none( - self.make_boolean(self.data_subclass[self.retrieval_key].get(item)) + self.make_boolean(self.data_subclass[self.filter].get(item)) ) @property def filtered_data(self): """ - Return a dictionary of the switch matching self.retrieval_key. + Return a dictionary of the switch matching self.filter. Return None if the switch does not exist on the controller. """ - return self.data_subclass.get(self.retrieval_key) + return self.data_subclass.get(self.filter) @property - def retrieval_key(self): + def filter(self): """ Set the ip_address of the switch to query. This needs to be set before accessing this class's properties. """ - return self.properties.get("retrieval_key") + return self.properties.get("filter") - @retrieval_key.setter - def retrieval_key(self, value): - self.properties["retrieval_key"] = value + @filter.setter + def filter(self, value): + self.properties["filter"] = value class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): @@ -752,7 +752,7 @@ class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): instance = SwitchIssuDetailsBySerialNumber(module) instance.refresh() - instance.retrieval_key = "FDO211218GC" + instance.filter = "FDO211218GC" instance.refresh() image_staged = instance.image_staged image_upgraded = instance.image_upgraded @@ -771,7 +771,7 @@ def __init__(self, module): def _init_properties(self): super()._init_properties() self.method_name = inspect.stack()[0][3] - self.properties["retrieval_key"] = None + self.properties["filter"] = None def refresh(self): """ @@ -789,25 +789,25 @@ def refresh(self): def _get(self, item): self.method_name = inspect.stack()[0][3] - if self.retrieval_key is None: + if self.filter is None: msg = f"{self.class_name}.{self.method_name}: " - msg += "set instance.retrieval_key to a switch serialNumber " + msg += "set instance.filter to a switch serialNumber " msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass.get(self.retrieval_key) is None: + if self.data_subclass.get(self.filter) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.retrieval_key} does not exist " + msg += f"{self.filter} does not exist " msg += "on the controller." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass[self.retrieval_key].get(item) is None: + if self.data_subclass[self.filter].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.retrieval_key} unknown property name: {item}." + msg += f"{self.filter} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) return self.make_none( - self.make_boolean(self.data_subclass[self.retrieval_key].get(item)) + self.make_boolean(self.data_subclass[self.filter].get(item)) ) @property @@ -816,20 +816,20 @@ def filtered_data(self): Return a dictionary of the switch matching self.serial_number. Return None of the switch does not exist in NDFC. """ - return self.data_subclass.get(self.retrieval_key) + return self.data_subclass.get(self.filter) @property - def retrieval_key(self): + def filter(self): """ Set the serial_number of the switch to query. This needs to be set before accessing this class's properties. """ - return self.properties.get("retrieval_key") + return self.properties.get("filter") - @retrieval_key.setter - def retrieval_key(self, value): - self.properties["retrieval_key"] = value + @filter.setter + def filter(self, value): + self.properties["filter"] = value class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): @@ -841,7 +841,7 @@ class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): instance = SwitchIssuDetailsByDeviceName(module) instance.refresh() - instance.retrieval_key = "leaf_1" + instance.filter = "leaf_1" image_staged = instance.image_staged image_upgraded = instance.image_upgraded ip_address = instance.ip_address @@ -859,7 +859,7 @@ def __init__(self, module): def _init_properties(self): super()._init_properties() method_name = inspect.stack()[0][3] - self.properties["retrieval_key"] = None + self.properties["filter"] = None def refresh(self): """ @@ -876,44 +876,44 @@ def refresh(self): def _get(self, item): self.method_name = inspect.stack()[0][3] - if self.retrieval_key is None: + if self.filter is None: msg = f"{self.class_name}.{self.method_name}: " - msg += "set instance.retrieval_key to a switch deviceName " + msg += "set instance.filter to a switch deviceName " msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass.get(self.retrieval_key) is None: + if self.data_subclass.get(self.filter) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.retrieval_key} does not exist " + msg += f"{self.filter} does not exist " msg += "on the controller." self.module.fail_json(msg, **self.failed_result) - if self.data_subclass[self.retrieval_key].get(item) is None: + if self.data_subclass[self.filter].get(item) is None: msg = f"{self.class_name}.{self.method_name}: " - msg += f"{self.retrieval_key} unknown property name: {item}." + msg += f"{self.filter} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) return self.make_none( - self.make_boolean(self.data_subclass[self.retrieval_key].get(item)) + self.make_boolean(self.data_subclass[self.filter].get(item)) ) @property def filtered_data(self): """ - Return a dictionary of the switch matching self.retrieval_key. + Return a dictionary of the switch matching self.filter. Return None of the switch does not exist in NDFC. """ - return self.data_subclass.get(self.retrieval_key) + return self.data_subclass.get(self.filter) @property - def retrieval_key(self): + def filter(self): """ Set the device_name of the switch to query. This needs to be set before accessing this class's properties. """ - return self.properties.get("retrieval_key") + return self.properties.get("filter") - @retrieval_key.setter - def retrieval_key(self, value): - self.properties["retrieval_key"] = value + @filter.setter + def filter(self, value): + self.properties["filter"] = value diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 1846cadfb..bdaa32343 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -573,7 +573,7 @@ def _build_idempotent_want(self, want) -> None: msg += f"want: {json.dumps(want, indent=4, sort_keys=True)}" self.log.log_msg(msg) - self.have.retrieval_key = want["ip_address"] + self.have.filter = want["ip_address"] want["policy_changed"] = True # The switch does not have an image policy attached. @@ -664,7 +664,7 @@ def get_need_merged(self) -> None: self.log.log_msg(msg) for want in self.want: - self.have.retrieval_key = want["ip_address"] + self.have.filter = want["ip_address"] msg = f"DEBUG: {self.class_name}.{method_name}: " msg += f"self.have.serial_number: {self.have.serial_number}" @@ -708,7 +708,7 @@ def get_need_deleted(self) -> None: need = [] for want in self.want: - self.have.retrieval_key = want["ip_address"] + self.have.filter = want["ip_address"] if self.have.serial_number is None: continue if self.have.policy is None: @@ -1270,7 +1270,7 @@ def handle_merged_state(self) -> None: self.switch_details.ip_address = switch.get("ip_address") device = {} device["serial_number"] = self.switch_details.serial_number - self.have.retrieval_key = self.switch_details.ip_address + self.have.filter = self.switch_details.ip_address device["policy_name"] = switch.get("policy") device["ip_address"] = self.switch_details.ip_address @@ -1337,7 +1337,7 @@ def handle_query_state(self) -> None: query_devices: List[Dict[str, Any]] = [] for switch in self.need: - instance.retrieval_key = switch.get("ip_address") + instance.filter = switch.get("ip_address") if instance.filtered_data is None: continue query_devices.append(instance.filtered_data) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 81371d319..5386ecc89 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -130,9 +130,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance = image_upgrade_task instance.get_have() - instance.have.retrieval_key = "1.1.1.1" + instance.have.filter = "1.1.1.1" assert instance.have.device_name == "leaf1" - instance.have.retrieval_key = "2.2.2.2" + instance.have.filter = "2.2.2.2" assert instance.have.device_name == "cvd-2313-leaf" assert instance.have.serial_number == "FDO2112189M" assert instance.have.fabric == "hard" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index ea8e746fe..82d6f515f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -130,11 +130,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.retrieval_key = "leaf1" + instance.filter = "leaf1" assert instance.device_name == "leaf1" assert instance.serial_number == "FDO21120U5D" # change device_name to a different switch, expect different information - instance.retrieval_key = "cvd-2313-leaf" + instance.filter = "cvd-2313-leaf" assert instance.device_name == "cvd-2313-leaf" assert instance.serial_number == "FDO2112189M" # verify remaining properties using current device_name @@ -319,7 +319,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.retrieval_key = "FOO" + instance.filter = "FOO" match = "SwitchIssuDetailsByDeviceName._get: FOO does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): @@ -353,7 +353,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.retrieval_key = "leaf1" + instance.filter = "leaf1" match = "SwitchIssuDetailsByDeviceName._get: leaf1 unknown " match += "property name: FOO" with pytest.raises(AnsibleFailJson, match=match): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 233e53ab1..31189dfaa 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -132,11 +132,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.retrieval_key = "172.22.150.102" + instance.filter = "172.22.150.102" assert instance.device_name == "leaf1" assert instance.serial_number == "FDO21120U5D" # change ip_address to a different switch, expect different information - instance.retrieval_key = "172.22.150.108" + instance.filter = "172.22.150.108" assert instance.device_name == "cvd-2313-leaf" assert instance.serial_number == "FDO2112189M" # verify remaining properties using current ip_address @@ -322,7 +322,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.retrieval_key = "1.1.1.1" + instance.filter = "1.1.1.1" match = "SwitchIssuDetailsByIpAddress._get: 1.1.1.1 does not exist " match += "on the controller." with pytest.raises(AnsibleFailJson, match=match): @@ -355,7 +355,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.retrieval_key = "172.22.150.102" + instance.filter = "172.22.150.102" match = "SwitchIssuDetailsByIpAddress._get: 172.22.150.102 unknown " match += "property name: FOO" with pytest.raises(AnsibleFailJson, match=match): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 305c48b9f..0400d851e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -132,11 +132,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - instance.retrieval_key = "FDO21120U5D" + instance.filter = "FDO21120U5D" assert instance.device_name == "leaf1" assert instance.serial_number == "FDO21120U5D" # change serial_number to a different switch, expect different information - instance.retrieval_key = "FDO2112189M" + instance.filter = "FDO2112189M" assert instance.device_name == "cvd-2313-leaf" assert instance.serial_number == "FDO2112189M" # verify remaining properties using current serial_number @@ -322,7 +322,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "on the controller." instance.refresh() - instance.retrieval_key = "FOO00000BAR" + instance.filter = "FOO00000BAR" with pytest.raises(AnsibleFailJson, match=match): instance._get("serialNumber") # pylint: disable=protected-access @@ -356,6 +356,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "property name: FOO" instance.refresh() - instance.retrieval_key = "FDO21120U5D" + instance.filter = "FDO21120U5D" with pytest.raises(AnsibleFailJson, match=match): instance._get("FOO") # pylint: disable=protected-access From 9ce9a1c3462f9cae1e33337fd397b44af4577b8e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 8 Jan 2024 20:37:08 -1000 Subject: [PATCH 186/300] New logging system. New result system, more... 1. Added a common logging facility leveraging Ansible's logging.config.dictConfig so that logging can be changed based on the contents of a YAML file. Default (log.config = None) is not to log (i.e. the root logger is disabled). An example log.yaml config file is provided in module_utils/common/log.yaml. 2. Added a Result() class (using this also for the dcnm_image_policy module) and made the necessary modifications to leverage this. 3. Various other cleanup. --- plugins/module_utils/common/log.py | 92 ++++----- plugins/module_utils/common/log.yaml | 26 +++ .../common/params_merge_defaults.py | 44 +---- .../module_utils/common/params_validate.py | 39 +--- .../module_utils/image_mgmt/image_policies.py | 8 +- .../image_mgmt/image_policy_action.py | 18 +- .../module_utils/image_mgmt/image_stage.py | 19 +- .../module_utils/image_mgmt/image_upgrade.py | 54 +++-- .../image_mgmt/image_upgrade_common.py | 27 +-- .../module_utils/image_mgmt/image_validate.py | 9 +- .../image_mgmt/install_options.py | 12 +- .../module_utils/image_mgmt/switch_details.py | 4 +- .../image_mgmt/switch_issu_details.py | 72 ++++--- plugins/modules/dcnm_image_upgrade.py | 184 ++++++++---------- ...test_image_upgrade_image_upgrade_common.py | 149 +++++++++----- .../test_image_upgrade_image_upgrade_task.py | 11 +- 16 files changed, 379 insertions(+), 389 deletions(-) create mode 100644 plugins/module_utils/common/log.yaml diff --git a/plugins/module_utils/common/log.py b/plugins/module_utils/common/log.py index 90eb167ee..9394e9769 100644 --- a/plugins/module_utils/common/log.py +++ b/plugins/module_utils/common/log.py @@ -18,23 +18,13 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -import inspect +import logging +from logging.config import dictConfig +import yaml -class Log: - """ - A logging utility for use with Ansible modules. - - Log messages to a file. - - Usage: - - instance = Log(ansible_module) - instance.debug = True - instance.logfile = "/tmp/params_validate.log" - instance.log_msg("some message") - """ +class Log: def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module @@ -43,51 +33,51 @@ def __init__(self, ansible_module): def _build_properties(self) -> None: self.properties = {} - self.properties["debug"] = False - self.properties["logfile"] = None + self.properties["config"] = None + + def commit(self): + if self.config is None: + logger = logging.getLogger() + for handler in logger.handlers.copy(): + try: + logger.removeHandler(handler) + except ValueError: # if handler already removed + pass + logger.addHandler(logging.NullHandler()) + logger.propagate = False + return - def log_msg(self, msg): - """ - Open the logfile and write the message to it. + if isinstance(self.config, dict): + try: + dictConfig(self.config) + return + except ValueError as err: + msg = f"error configuring logging from dict. " + msg += f"detail: {err}" + self.ansible_module.fail_json(msg=msg) - Call fail_json() if there is an error writing to the logfile. - """ - if self.debug is False: - return - if self.logfile is None: - return try: - with open(f"{self.logfile}", "a+", encoding="UTF-8") as file_handle: - file_handle.write(f"{msg}\n") + with open(self.config, "r") as file: + logging_config = yaml.safe_load(file) except IOError as err: - msg = f"error writing to logfile {self.logfile}. " + msg = f"error reading logging config from {self.config}. " msg += f"detail: {err}" - self.ansible_module.fail_json(msg) + self.ansible_module.fail_json(msg=msg) + dictConfig(logging_config) @property - def debug(self): - """ - Enable/disable debugging to self.logfile + def config(self): """ - return self.properties["debug"] + Can be either: - @debug.setter - def debug(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += "Invalid type for debug. Expected bool. " - msg += f"Got {type(value)}." - self.ansible_module.fail_json(msg) - self.properties["debug"] = value - - @property - def logfile(self): - """ - Set file to which messages are written + 1. None, in which case logging is disabled + 2. A YAML file from which logging config is read. + Must conform to logging.config.dictConfig + 3. A dictionary containing logging config + Must conform to logging.config.dictConfig """ - return self.properties["logfile"] + return self.properties["config"] - @logfile.setter - def logfile(self, value): - self.properties["logfile"] = value + @config.setter + def config(self, value): + self.properties["config"] = value diff --git a/plugins/module_utils/common/log.yaml b/plugins/module_utils/common/log.yaml new file mode 100644 index 000000000..b8a9e3811 --- /dev/null +++ b/plugins/module_utils/common/log.yaml @@ -0,0 +1,26 @@ +--- +version: 1 +formatters: + standard: + class: logging.Formatter + format: "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s" +handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: standard + level: DEBUG + filename: "/tmp/dcnm.log" + mode: a + encoding: utf-8 + maxBytes: 500000 + backupCount: 4 +loggers: + dcnm: + handlers: + - file + level: DEBUG + propagate: false +root: + level: INFO + handlers: + - file diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index 4fe408129..3452a9e11 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -20,12 +20,10 @@ import copy import inspect +import logging from collections.abc import MutableMapping as Map from typing import Any, Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ - Log - class ParamsMergeDefaults: """ @@ -38,12 +36,11 @@ class ParamsMergeDefaults: """ def __init__(self, ansible_module): - self.class_name = self.__class__.__name__ + self.class_name = __class__.__name__ self.ansible_module = ansible_module - self.log = Log(self.ansible_module) - self.log.debug = False - self.log.logfile = None + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug(f"ENTERED") self._build_properties() self._build_reserved_params() @@ -53,13 +50,10 @@ def _build_properties(self): """ Container for the properties of this class. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties = {} self.properties["params_spec"] = None self.properties["parameters"] = None self.properties["merged_parameters"] = None - self.properties["debug"] = False - self.properties["logfile"] = None def _build_reserved_params(self): """ @@ -90,8 +84,6 @@ def _merge_default_params( 1. they are present in spec 2. they have a default value defined in spec """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - for spec_key, spec_value in spec.items(): if spec_key in self.reserved_params: continue @@ -131,34 +123,6 @@ def commit(self) -> None: self.params_spec, self.parameters ) - @property - def debug(self): - """ - Enable/disable debugging to self.logfile - """ - return self.log.debug - - @debug.setter - def debug(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += "Invalid type for debug. Expected bool. " - msg += f"Got {type(value)}." - self.ansible_module.fail_json(msg) - self.log.debug = value - - @property - def logfile(self): - """ - Set file to which debug log is written - """ - return self.log.logfile - - @logfile.setter - def logfile(self, value): - self.log.logfile = value - @property def merged_parameters(self): """ diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 0c3f8d0d2..0505e27c3 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -20,12 +20,12 @@ import inspect import ipaddress +import logging from collections.abc import MutableMapping as Map from typing import Any, List from ansible.module_utils.common import validation -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ - Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log class ParamsValidate: @@ -78,13 +78,12 @@ class ParamsValidate: """ def __init__(self, ansible_module): - self.class_name = self.__class__.__name__ + self.class_name = __class__.__name__ self.ansible_module = ansible_module self.validation = validation - self.log = Log(self.ansible_module) - self.log.debug = False - self.log.logfile = None + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug(f"ENTERED") self._build_properties() self._build_reserved_params() @@ -563,34 +562,6 @@ def _verify_expected_type(self, expected_type: str, param: str) -> None: msg += f"Got '{expected_type}'." self.ansible_module.fail_json(msg) - @property - def debug(self): - """ - Enable/disable debugging to self.log.logfile - """ - return self.log.debug - - @debug.setter - def debug(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += "Invalid type for debug. Expected bool. " - msg += f"Got {type(value)}." - self.ansible_module.fail_json(msg) - self.log.debug = value - - @property - def logfile(self): - """ - Set file to which debug log is written - """ - return self.log.logfile - - @logfile.setter - def logfile(self, value): - self.log.logfile = value - @property def parameters(self): """ diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 6761209a0..5c06d6e68 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -20,6 +20,7 @@ import copy import inspect +import logging from typing import Any, AnyStr, Dict from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -55,14 +56,17 @@ class ImagePolicies(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = self.__class__.__name__ + self.class_name = __class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("1. ENTERED") + self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.properties = {} + # self.properties is already initialized in the parent class self.properties["all_policies"] = None self.properties["policy_name"] = None self.properties["response_data"] = None diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index fce2baace..3ea7519c9 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -20,6 +20,7 @@ import inspect import json +import logging from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -63,7 +64,11 @@ class ImagePolicyAction(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = self.__class__.__name__ + self.class_name = __class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED") + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() self._init_properties() @@ -76,7 +81,7 @@ def __init__(self, module): def _init_properties(self): method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.properties = {} + # self.properties is already initialized in the parent class self.properties["action"] = None self.properties["response"] = None self.properties["result"] = None @@ -205,7 +210,7 @@ def _attach_policy(self): msg = f"{self.class_name}.{method_name}: " msg += f"response: {json.dumps(response, indent=4)}" - self.log.log_msg(msg) + self.log.debug(msg) if not result["success"]: msg = f"{self.class_name}.{method_name}: " @@ -237,16 +242,15 @@ def _detach_policy(self): self.properties["response"] = dcnm_send(self.module, self.verb, self.path) self.properties["result"] = self._handle_response(self.response, self.verb) - msg = f"{self.class_name}.{method_name}: " - msg += f"response: {json.dumps(self.response, indent=4)}" - self.log.log_msg(msg) - if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when detaching policy {self.policy_name} " msg += f"from the following device(s): {','.join(sorted(self.serial_numbers))}." self.module.fail_json(msg, **self.failed_result) + self.changed = True + self.diff = self.response + def _query_policy(self): """ Query the image policy diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 55c42cf64..ff7fc62a4 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -21,6 +21,7 @@ import copy import inspect import json +import logging from time import sleep from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ @@ -100,8 +101,11 @@ class ImageStage(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = self.__class__.__name__ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.class_name = __class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug(f"ENTERED") + self.endpoints = ApiEndpoints() self._init_properties() self.serial_numbers_done = set() @@ -112,8 +116,7 @@ def __init__(self, module): self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.properties = {} + # self.properties is already initialized in the parent class self.properties["serial_numbers"] = None self.properties["response_data"] = None self.properties["result"] = None @@ -129,7 +132,6 @@ def _populate_controller_version(self): 1. This cannot go into ImageUpgradeCommon() due to circular imports resulting in RecursionError """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable instance = ControllerVersion(self.module) instance.refresh() self.controller_version = instance.version @@ -139,7 +141,6 @@ def prune_serial_numbers(self): If the image is already staged on a switch, remove that switch's serial number from the list of serial numbers to stage. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable serial_numbers = copy.copy(self.serial_numbers) for serial_number in serial_numbers: self.issu_detail.filter = serial_number @@ -344,8 +345,9 @@ def check_interval(self): @check_interval.setter def check_interval(self, value): + method_name = inspect.stack()[0][3] if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_interval must " + msg = f"{self.class_name}.{method_name}: instance.check_interval must " msg += "be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @@ -359,8 +361,9 @@ def check_timeout(self): @check_timeout.setter def check_timeout(self, value): + method_name = inspect.stack()[0][3] if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_timeout must " + msg = f"{self.class_name}.{method_name}: instance.check_timeout must " msg += "be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 9309e7982..f208a9f11 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -21,6 +21,7 @@ import copy import inspect import json +import logging from time import sleep from typing import Any, Dict, List, Set @@ -132,8 +133,10 @@ class ImageUpgrade(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = self.__class__.__name__ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.class_name = __class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug(f"ENTERED") self.endpoints = ApiEndpoints() self.ipv4_done = set() @@ -145,7 +148,6 @@ def __init__(self, module): self._init_properties() self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) self.install_options = ImageInstallOptions(self.module) - self.log.log_msg("DEBUG: ImageUpgrade.__init__ DONE") def _init_properties(self) -> None: """ @@ -155,13 +157,11 @@ def _init_properties(self) -> None: per-switch given the payload structure is not amenable to that. Consider removing some of these. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - # self.ip_addresses is used in: # self._wait_for_current_actions_to_complete() # self._wait_for_image_upgrade_to_complete() self.ip_addresses: Set[str] = set() - self.properties: Dict[str, Any] = {} + # self.properties is already initialized in the parent class self.properties["bios_force"] = False self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds @@ -197,8 +197,6 @@ def _validate_devices(self) -> None: switches which can be upgraded. This is used in _wait_for_current_actions_to_complete """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - for device in self.devices: self.issu_detail.filter = device.get("ip_address") self.issu_detail.refresh() @@ -217,8 +215,6 @@ def _build_payload(self, device) -> None: """ Build the request payload to upgrade the switches. """ - method_name = inspect.stack()[0][3] - self.issu_detail.filter = device.get("ip_address") self.issu_detail.refresh() @@ -446,9 +442,8 @@ def commit(self) -> None: """ method_name = inspect.stack()[0][3] - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.devices: {json.dumps(self.devices, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"self.devices: {json.dumps(self.devices, indent=4, sort_keys=True)}" + self.log.debug(msg) if self.devices is None: msg = f"{self.class_name}.{method_name}: " @@ -461,31 +456,27 @@ def commit(self) -> None: self.path: str = self.endpoints.image_upgrade.get("path") self.verb: str = self.endpoints.image_upgrade.get("verb") - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.verb {self.verb}, self.path: {self.path}" - self.log.log_msg(msg) + msg = f"self.verb {self.verb}, self.path: {self.path}" + self.log.debug(msg) for device in self.devices: - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"device: {json.dumps(device, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"device: {json.dumps(device, indent=4, sort_keys=True)}" + self.log.debug(msg) self._build_payload(device) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) self.properties["response"] = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) self.properties["result"] = self._handle_response(self.response, self.verb) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += ( + msg = ( f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" ) - self.log.log_msg(msg) + self.log.debug(msg) if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " @@ -496,6 +487,9 @@ def commit(self) -> None: self.properties["response_data"] = self.response.get("DATA") self._wait_for_image_upgrade_to_complete() + self.changed = True + self.diff = self.response + def _wait_for_current_actions_to_complete(self): """ The controller will not upgrade an image if there are any actions @@ -859,9 +853,10 @@ def check_interval(self): @check_interval.setter def check_interval(self, value): + method_name = inspect.stack()[0][3] if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_interval must " - msg += "be an integer." + msg = f"{self.class_name}.{method_name}: " + msg = f"instance.check_interval must be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @@ -874,9 +869,10 @@ def check_timeout(self): @check_timeout.setter def check_timeout(self, value): + method_name = inspect.stack()[0][3] if not isinstance(value, int): - msg = f"{self.__class__.__name__}: instance.check_timeout must " - msg += "be an integer." + msg = f"{self.class_name}.{method_name}: " + msg = f"instance.check_timeout must be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 7bc832212..427fe750e 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -19,8 +19,9 @@ __author__ = "Allen Robel" import inspect -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import \ - Log +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log class ImageUpgradeCommon: @@ -36,17 +37,17 @@ def __init__(self, module): """ def __init__(self, module): - self.class_name = self.__class__.__name__ + self.class_name = __class__.__name__ - self.module = module - self.params = module.params - - self.log = Log(module) - self.log.debug = False - self.log.logfile = "/tmp/dcnm_image_upgrade.log" + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug(f"ENTERED") self.module = module - self.log.log_msg("ImageUpgradeCommon.__init__ DONE") + self.params = module.params + self.properties = {} + self.properties["changed"] = False + self.properties["diff"] = [] + self.properties["failed"] = False def _handle_response(self, response, verb): """ @@ -173,7 +174,7 @@ def changed(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"changed must be a bool. Got {value}" - self.ansible_module.fail_json(msg) + self.module.fail_json(msg) self.properties["changed"] = value @property @@ -189,7 +190,7 @@ def diff(self, value): if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " msg += f"diff must be a dict. Got {value}" - self.ansible_module.fail_json(msg) + self.module.fail_json(msg) self.properties["diff"].append(value) @property @@ -207,5 +208,5 @@ def failed(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"failed must be a bool. Got {value}" - self.ansible_module.fail_json(msg) + self.module.fail_json(msg) self.properties["failed"] = value diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index bb08346fe..1e8e1b347 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -21,6 +21,7 @@ import copy import inspect import json +import logging from time import sleep from typing import Any, Dict, List, Set @@ -69,8 +70,10 @@ class ImageValidate(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] + self.class_name = __class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug(f"ENTERED") self.endpoints = ApiEndpoints() @@ -85,7 +88,7 @@ def __init__(self, module): def _init_properties(self) -> None: self.method_name = inspect.stack()[0][3] - self.properties: Dict[str, Any] = {} + # self.properties is already initialized in the parent class self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds self.properties["response_data"] = {} diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 8feb29ac0..20c75119d 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -139,7 +139,7 @@ class ImageInstallOptions(ImageUpgradeCommon): def __init__(self, module) -> None: super().__init__(module) - self.class_name = self.__class__.__name__ + self.class_name = __class__.__name__ self.endpoints = ApiEndpoints() self.path = self.endpoints.install_options.get("path") @@ -151,7 +151,7 @@ def __init__(self, module) -> None: self._init_properties() def _init_properties(self): - self.properties = {} + # self.properties is already initialized in the parent class self.properties["epld"] = False self.properties["issu"] = True self.properties["response_data"] = None @@ -185,10 +185,9 @@ def refresh(self) -> None: # Mock the response such that the caller knows nothing needs to be # done. if self.epld is False and self.issu is False and self.package_install is False: - msg = f"{self.class_name}.{method_name}: " - msg += "At least one of epld, issu, or package_install " + msg = "At least one of epld, issu, or package_install " msg += "must be True before calling refresh(). Skipping." - self.log.log_msg(msg) + self.log.debug(msg) self.compatibility_status = {} self.properties["response_data"] = { "compatibilityStatusList": [], @@ -241,8 +240,6 @@ def _build_payload(self) -> None: "packageInstall": false } """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.payload: Dict[str, Any] = {} self.payload["devices"] = [] devices = {} @@ -254,7 +251,6 @@ def _build_payload(self) -> None: self.payload["packageInstall"] = self.package_install def _get(self, item): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable return self.make_boolean(self.make_none(self.response_data.get(item))) # Mandatory properties diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index ea28279de..f08cea002 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -52,14 +52,14 @@ def __init__(self, module): super().__init__(module) self.method_name = inspect.stack()[0][3] - self.class_name = self.__class__.__name__ + self.class_name = __class__.__name__ self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): self.method_name = inspect.stack()[0][3] - self.properties = {} + # self.properties is already initialized in the parent class self.properties["ip_address"] = None self.properties["response_data"] = None self.properties["response"] = None diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index d16203774..dbcb9a392 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -19,6 +19,7 @@ __author__ = "Allen Robel" import inspect +import logging from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -88,14 +89,16 @@ class SwitchIssuDetails(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = self.__class__.__name__ - self.method_name = inspect.stack()[0][3] + self.class_name = __class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED") + self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): - self.method_name = inspect.stack()[0][3] - self.properties = {} + # self.properties is already initialized in the parent class self.properties["response"] = None self.properties["result"] = None self.properties["response_data"] = None @@ -111,7 +114,7 @@ def refresh(self) -> None: """ Refresh current issu details from the controller. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] path = self.endpoints.issu_info.get("path") verb = self.endpoints.issu_info.get("verb") @@ -120,7 +123,7 @@ def refresh(self) -> None: self.properties["result"] = self._handle_response(self.response, verb) if self.result["success"] is False or self.result["found"] is False: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Bad result when retriving switch " msg += "information from the controller" self.module.fail_json(msg, **self.failed_result) @@ -128,12 +131,12 @@ def refresh(self) -> None: data = self.response.get("DATA").get("lastOperDataObject") if data is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "The controller has no switch ISSU information." self.module.fail_json(msg, **self.failed_result) if len(data) == 0: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "The controller has no switch ISSU information." self.module.fail_json(msg, **self.failed_result) @@ -147,8 +150,6 @@ def actions_in_progress(self): Return True if any actions are in progress Return False otherwise """ - self.method_name = inspect.stack()[0][3] - for action_key in self.properties["action_keys"]: if self._get(action_key) == "In-Progress": return True @@ -678,12 +679,15 @@ class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - self.method_name = inspect.stack()[0][3] + self.class_name = __class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED") + self._init_properties() def _init_properties(self): super()._init_properties() - self.method_name = inspect.stack()[0][3] self.properties["filter"] = None def refresh(self): @@ -693,27 +697,26 @@ def refresh(self): Refresh ip_address current issu details from the controller """ super().refresh() - self.method_name = inspect.stack()[0][3] self.data_subclass = {} for switch in self.response_data: self.data_subclass[switch["ipAddress"]] = switch def _get(self, item): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if self.filter is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "set instance.filter to a switch ipAddress " msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.filter) is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} does not exist on the controller." self.module.fail_json(msg, **self.failed_result) if self.data_subclass[self.filter].get(item) is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) @@ -765,12 +768,16 @@ class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - self.method_name = inspect.stack()[0][3] + + self.class_name = __class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED") + self._init_properties() def _init_properties(self): super()._init_properties() - self.method_name = inspect.stack()[0][3] self.properties["filter"] = None def refresh(self): @@ -780,29 +787,29 @@ def refresh(self): Refresh serial_number current issu details from NDFC """ super().refresh() - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.data_subclass = {} for switch in self.response_data: self.data_subclass[switch["serialNumber"]] = switch def _get(self, item): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if self.filter is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "set instance.filter to a switch serialNumber " msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.filter) is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} does not exist " msg += "on the controller." self.module.fail_json(msg, **self.failed_result) if self.data_subclass[self.filter].get(item) is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) @@ -853,12 +860,16 @@ class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - method_name = inspect.stack()[0][3] + + self.class_name = __class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED") + self._init_properties() def _init_properties(self): super()._init_properties() - method_name = inspect.stack()[0][3] self.properties["filter"] = None def refresh(self): @@ -868,28 +879,27 @@ def refresh(self): Refresh device_name current issu details from NDFC """ super().refresh() - self.method_name = inspect.stack()[0][3] self.data_subclass = {} for switch in self.response_data: self.data_subclass[switch["deviceName"]] = switch def _get(self, item): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if self.filter is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "set instance.filter to a switch deviceName " msg += f"before accessing property {item}." self.module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.filter) is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} does not exist " msg += "on the controller." self.module.fail_json(msg, **self.failed_result) if self.data_subclass[self.filter].get(item) is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} unknown property name: {item}." self.module.fail_json(msg, **self.failed_result) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index bdaa32343..fc2cc462e 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -406,12 +406,15 @@ from typing import Any, Dict, List from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ + MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ ParamsMergeDefaults -from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts \ - import MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.result import \ + Result from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ @@ -449,7 +452,7 @@ class ImageUpgradeTask(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = self.__class__.__name__ + self.class_name = __class__.__name__ method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() @@ -479,7 +482,8 @@ def __init__(self, module): self.want = [] self.need = [] - self.result = {"changed": False, "diff": [], "response": []} + self.result = Result(self.module) + self.result.result["changed"] = False self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) @@ -503,31 +507,28 @@ def get_want(self) -> None: """ method_name = inspect.stack()[0][3] - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "Calling _merge_global_and_switch_configs with " + msg = "Calling _merge_global_and_switch_configs with " msg += f"self.config: {json.dumps(self.config, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + self.log.debug(msg) self._merge_global_and_switch_configs(self.config) self._merge_defaults_to_switch_configs() - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "Calling _validate_switch_configs with self.switch_configs: " + msg = "Calling _validate_switch_configs with self.switch_configs: " msg += f"{json.dumps(self.switch_configs, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + self.log.debug(msg) self._validate_switch_configs() self.want = self.switch_configs - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.want: {json.dumps(self.want, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"self.want: {json.dumps(self.want, indent=4, sort_keys=True)}" + self.log.debug(msg) if len(self.want) == 0: - self.result["changed"] = False - self.module.exit_json(**self.result) + self.result.result["changed"] = False + self.module.exit_json(**self.result.result) def _build_idempotent_want(self, want) -> None: """ @@ -569,9 +570,8 @@ def _build_idempotent_want(self, want) -> None: """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"want: {json.dumps(want, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"want: {json.dumps(want, indent=4, sort_keys=True)}" + self.log.debug(msg) self.have.filter = want["ip_address"] @@ -626,26 +626,22 @@ def _build_idempotent_want(self, want) -> None: ) instance.refresh() - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "Calling ImageInstallOptions.response " - msg += "instance.response: " + msg = "ImageInstallOptions.response: " msg += f"{json.dumps(instance.response_data, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + self.log.debug(msg) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "self.idempotent_want PRE EPLD CHECK: " + msg = "self.idempotent_want PRE EPLD CHECK: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + self.log.debug(msg) # if InstallOptions indicates that EPLD is already upgraded, # don't upgrade it again. if self.needs_epld_upgrade(instance.epld_modules) is False: self.idempotent_want["upgrade"]["epld"] = False - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "self.idempotent_want POST EPLD CHECK: " + msg = "self.idempotent_want POST EPLD CHECK: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + self.log.debug(msg) def get_need_merged(self) -> None: """ @@ -655,28 +651,24 @@ def get_need_merged(self) -> None: our want list that are not in our have list. These items will be sent to the controller. """ - method_name = inspect.stack()[0][3] need: List[Dict] = [] - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "self.want: " + msg = "self.want: " msg += f"{json.dumps(self.want, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + self.log.debug(msg) for want in self.want: self.have.filter = want["ip_address"] - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"self.have.serial_number: {self.have.serial_number}" - self.log.log_msg(msg) + msg = f"self.have.serial_number: {self.have.serial_number}" + self.log.debug(msg) if self.have.serial_number is not None: self._build_idempotent_want(want) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "self.idempotent_want: " + msg = "self.idempotent_want: " msg += f"{json.dumps(self.idempotent_want, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + self.log.debug(msg) test_idempotence = set() test_idempotence.add(self.idempotent_want["policy_changed"]) @@ -704,8 +696,6 @@ def get_need_deleted(self) -> None: Policies are detached only if the policy name matches. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - need = [] for want in self.want: self.have.filter = want["ip_address"] @@ -727,21 +717,20 @@ def get_need_query(self) -> None: policy name is ignored for query state. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - need = [] for want in self.want: need.append(want) self.need = copy.copy(need) def _build_params_spec(self) -> Dict[str, Any]: - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] if self.module.params["state"] == "merged": return self._build_params_spec_for_merged_state() if self.module.params["state"] == "deleted": return self._build_params_spec_for_merged_state() if self.module.params["state"] == "query": return self._build_params_spec_for_query_state() + msg = f"{self.class_name}.{method_name}: " msg += f"Unsupported state: {self.module.params['state']}" self.module.fail_json(msg) @@ -923,15 +912,13 @@ def _merge_global_and_switch_configs(self, config) -> None: # because merge_dicts modifies it in place global_config = copy.deepcopy(config) global_config.pop("switches", None) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += ( + msg = ( f"global_config: {json.dumps(global_config, indent=4, sort_keys=True)}" ) - self.log.log_msg(msg) + self.log.debug(msg) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"switch PRE_MERGE : {json.dumps(switch, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"switch PRE_MERGE : {json.dumps(switch, indent=4, sort_keys=True)}" + self.log.debug(msg) merge_dicts = MergeDicts(self.module) merge_dicts.dict1 = global_config @@ -939,9 +926,8 @@ def _merge_global_and_switch_configs(self, config) -> None: merge_dicts.commit() switch_config = merge_dicts.dict_merged - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" + self.log.debug(msg) merged_configs.append(switch_config) self.switch_configs = copy.copy(merged_configs) @@ -951,8 +937,6 @@ def _merge_defaults_to_switch_configs(self) -> None: For any items in config which are not set, apply the default value from params_spec (if a default value exists). """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - configs_to_merge = copy.copy(self.switch_configs) merged_configs = [] merge = ParamsMergeDefaults(self.module) @@ -972,7 +956,6 @@ def _validate_switch_configs(self) -> None: Callers: - self.get_want """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable validator = ParamsValidate(self.module) validator.params_spec = self._build_params_spec() @@ -1048,8 +1031,6 @@ def _send_policy_attach_payload(self) -> None: Callers: - self.handle_merged_state """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if len(self.payloads) == 0: return @@ -1074,11 +1055,8 @@ def _stage_images(self, serial_numbers) -> None: Callers: - handle_merged_state """ - method_name = inspect.stack()[0][3] - - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"serial_numbers: {serial_numbers}" - self.log.log_msg(msg) + msg = f"serial_numbers: {serial_numbers}" + self.log.debug(msg) instance = ImageStage(self.module) instance.serial_numbers = serial_numbers @@ -1091,11 +1069,8 @@ def _validate_images(self, serial_numbers) -> None: Callers: - handle_merged_state """ - method_name = inspect.stack()[0][3] - - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"serial_numbers: {serial_numbers}" - self.log.log_msg(msg) + msg = f"serial_numbers: {serial_numbers}" + self.log.debug(msg) instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers @@ -1145,9 +1120,8 @@ def _verify_install_options(self, devices) -> None: verify_devices = copy.deepcopy(devices) for device in verify_devices: - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"device: {json.dumps(device, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"device: {json.dumps(device, indent=4, sort_keys=True)}" + self.log.debug(msg) self.switch_details.ip_address = device.get("ip_address") install_options.serial_number = self.switch_details.serial_number @@ -1156,12 +1130,11 @@ def _verify_install_options(self, devices) -> None: install_options.issu = device.get("upgrade", {}).get("nxos", False) install_options.refresh() - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "install_options.response_data: " + msg = "install_options.response_data: " msg += ( f"{json.dumps(install_options.response_data, indent=4, sort_keys=True)}" ) - self.log.log_msg(msg) + self.log.debug(msg) if ( install_options.status not in ["Success", "Skipped"] @@ -1174,16 +1147,14 @@ def _verify_install_options(self, devices) -> None: msg += "NX-OS image" self.module.fail_json(msg) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"install_options.epld: {install_options.epld}" - self.log.log_msg(msg) + msg = f"install_options.epld: {install_options.epld}" + self.log.debug(msg) - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += "install_options.epld_modules: " + msg = "install_options.epld_modules: " msg += ( f"{json.dumps(install_options.epld_modules, indent=4, sort_keys=True)}" ) - self.log.log_msg(msg) + self.log.debug(msg) if install_options.epld_modules is None and install_options.epld is True: msg = f"{self.class_name}.{method_name}: " @@ -1205,8 +1176,6 @@ def needs_epld_upgrade(self, epld_modules) -> bool: Callers: - self._build_idempotent_want """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if epld_modules is None: return False if epld_modules.get("moduleList") is None: @@ -1218,14 +1187,13 @@ def needs_epld_upgrade(self, epld_modules) -> bool: # of the str when converting to int. An # error is thrown without this. if int(new_version, 0) > int(old_version, 0): - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"(device: {module.get('deviceName')}), " + msg = f"(device: {module.get('deviceName')}), " msg += f"(IP: {module.get('ipAddress')}), " msg += f"(module#: {module.get('module')}), " msg += f"(module: {module.get('moduleType')}), " msg += f"new_version {new_version} > old_version {old_version}, " msg += "returning True" - self.log.log_msg(msg) + self.log.debug(msg) return True return False @@ -1236,11 +1204,11 @@ def _upgrade_images(self, devices) -> None: Callers: - handle_merged_state """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - upgrade = ImageUpgrade(self.module) upgrade.devices = devices upgrade.commit() + for diff in upgrade.diff: + self.result.merged.append(diff) def handle_merged_state(self) -> None: """ @@ -1251,8 +1219,6 @@ def handle_merged_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self._build_policy_attach_payload() self._send_policy_attach_payload() @@ -1263,9 +1229,8 @@ def handle_merged_state(self) -> None: self.switch_details.refresh() for switch in self.need: - msg = f"DEBUG: {self.class_name}.{method_name}: " - msg += f"switch: {json.dumps(switch, indent=4, sort_keys=True)}" - self.log.log_msg(msg) + msg = f"switch: {json.dumps(switch, indent=4, sort_keys=True)}" + self.log.debug(msg) self.switch_details.ip_address = switch.get("ip_address") device = {} @@ -1296,8 +1261,6 @@ def handle_deleted_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - detach_policy_devices: Dict[str, Any] = {} self.switch_details.refresh() @@ -1314,7 +1277,7 @@ def handle_deleted_state(self) -> None: ) if len(detach_policy_devices) == 0: - self.result = {"changed": False, "diff": [], "response": []} + self.result.result["changed"] = False return instance = ImagePolicyAction(self.module) @@ -1323,6 +1286,8 @@ def handle_deleted_state(self) -> None: instance.action = "detach" instance.serial_numbers = value instance.commit() + for diff in instance.diff: + self.result.deleted = diff def handle_query_state(self) -> None: """ @@ -1330,21 +1295,16 @@ def handle_query_state(self) -> None: Caller: main() """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.log.debug(f"ENTERED") instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() - query_devices: List[Dict[str, Any]] = [] for switch in self.need: instance.filter = switch.get("ip_address") if instance.filtered_data is None: continue - query_devices.append(instance.filtered_data) - - self.result["response"] = query_devices - self.result["diff"] = [] - self.result["changed"] = False + self.result.query = instance.filtered_data def _failure(self, resp) -> None: """ @@ -1387,6 +1347,18 @@ def main(): } ansible_module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) + + # Create the base dcnm logger + # To disable logging, comment out log.config = below + # log.config can be either a dictionary, or a path to a yaml file + # Both dictionary and yaml file formats must be conformant with + # logging.config.dictConfig and must not log to the console. + # For an example configuration, see: + # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/log.yaml + log = Log(ansible_module) + # log.config = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm/plugins/module_utils/common/log.yaml" + log.commit() + task_module = ImageUpgradeTask(ansible_module) task_module.get_want() @@ -1399,15 +1371,15 @@ def main(): elif ansible_module.params["state"] == "query": task_module.get_need_query() - task_module.result["changed"] = False + task_module.result.result["changed"] = False if len(task_module.need) == 0: - ansible_module.exit_json(**task_module.result) + ansible_module.exit_json(**task_module.result.result) if ansible_module.check_mode: - ansible_module.exit_json(**task_module.result) + ansible_module.exit_json(**task_module.result.result) if ansible_module.params["state"] in ["merged", "deleted"]: - task_module.result["changed"] = True + task_module.result.result["changed"] = True if ansible_module.params["state"] == "merged": task_module.handle_merged_state() @@ -1416,7 +1388,7 @@ def main(): elif ansible_module.params["state"] == "query": task_module.handle_query_state() - ansible_module.exit_json(**task_module.result) + ansible_module.exit_json(**task_module.result.result) if __name__ == "__main__": diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 553833467..7f16dbc64 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -35,6 +35,7 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from .image_upgrade_utils import (does_not_raise, image_upgrade_common_fixture, responses_image_upgrade_common) @@ -57,8 +58,6 @@ def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: with does_not_raise(): instance = image_upgrade_common assert instance.params == test_params - assert instance.log.debug is False - assert instance.log.logfile == "/tmp/dcnm_image_upgrade.log" @pytest.mark.parametrize( @@ -405,58 +404,100 @@ def test_image_mgmt_image_upgrade_common_00110(image_upgrade_common) -> None: - log.log_msg Test - - log.log_msg returns None when debug is False + - log.debug returns None when the base logger is disabled + - Base logger is disabled if Log.config is None (which is the default) """ instance = image_upgrade_common - error_message = "This is an error message" - instance.log.debug = False - assert instance.log.log_msg(error_message) is None - - -def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) -> None: - """ - Function - - log.log_msg - - Test - - log_msg writes to the log.logfile when log.debug is True - """ - instance = image_upgrade_common - - directory = tmp_path / "test_log_msg" - directory.mkdir() - filename = directory / "test_log_msg.txt" - - error_message = "This is an error message" - instance.log.debug = True - instance.log.logfile = filename - instance.log.log_msg(error_message) - - assert filename.read_text(encoding="UTF-8") == error_message + "\n" - assert len(list(tmp_path.iterdir())) == 1 - - -def test_image_mgmt_image_upgrade_common_00112(tmp_path, image_upgrade_common) -> None: - """ - Function - - log.log_msg - - Test - - log.log_msg calls fail_json if the logfile cannot be opened - - Description - To ensure an error is generated, we attempt a write to a filename - that is too long for the target OS. - """ - instance = image_upgrade_common - - directory = tmp_path / "test_log_msg" - directory.mkdir() - filename = directory / f"test_{'a' * 2000}_log_msg.txt" - - error_message = "This is an error message" - instance.log.debug = True - instance.log.logfile = filename - with pytest.raises(AnsibleFailJson, match="error writing to logfile"): - instance.log.log_msg(error_message) + message = "This is a message" + assert instance.log.debug(message) is None + assert instance.log.info(message) is None + + +# def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) -> None: +# """ +# Function +# - log.log_msg + +# Test +# - log_msg writes to the log.logfile when log.config is set +# """ +# instance = image_upgrade_common + +# directory = tmp_path / "test_log_msg" +# directory.mkdir() +# filename = directory / "test_log_msg.txt" + +# log = Log(instance.module) +# config = { +# "version": 1, +# "formatters": { +# "standard": { +# "class": "logging.Formatter", +# "format": "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s" +# } +# }, +# "handlers": { +# "file": { +# "class": "logging.handlers.RotatingFileHandler", +# "formatter": "standard", +# "level": "DEBUG", +# "filename": "foo", +# "mode": "a", +# "encoding": "utf-8", +# "maxBytes": 500000, +# "backupCount": 4 +# } +# }, +# "loggers": { +# "dcnm": { +# "handlers": [ +# "file" +# ], +# "level": "DEBUG", +# "propagate": False +# } +# }, +# "root": { +# "level": "INFO", +# "handlers": [ +# "file" +# ] +# } +# } + +# config["handlers"]["file"]["filename"] = filename +# log.config = config +# message = "This is a message" +# # instance.log.debug = True +# # instance.log.logfile = filename +# # instance.log.log_msg(message) +# instance.log.debug(message) + +# assert filename.read_text(encoding="UTF-8") == message + "\n" +# assert len(list(tmp_path.iterdir())) == 1 + + +# def test_image_mgmt_image_upgrade_common_00112(tmp_path, image_upgrade_common) -> None: +# """ +# Function +# - log.log_msg + +# Test +# - log.log_msg calls fail_json if the logfile cannot be opened + +# Description +# To ensure an error is generated, we attempt a write to a filename +# that is too long for the target OS. +# """ +# instance = image_upgrade_common + +# directory = tmp_path / "test_log_msg" +# directory.mkdir() +# filename = directory / f"test_{'a' * 2000}_log_msg.txt" + +# error_message = "This is an error message" +# instance.log.debug = True +# instance.log.logfile = filename +# with pytest.raises(AnsibleFailJson, match="error writing to logfile"): +# instance.log.log_msg(error_message) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 5386ecc89..2bbbd375e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -90,7 +90,16 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: assert instance.validated == {} assert instance.want == [] assert instance.need == [] - assert instance.result == {"changed": False, "diff": [], "response": []} + assert instance.result.result == { + "changed": False, + "diff": { + "deleted": [], + "merged": [], + "overridden": [], + "query": [], + "replaced": [], + }, + } assert isinstance(instance.switch_details, SwitchDetails) assert isinstance(instance.image_policies, ImagePolicies) From dffc8b586781550d90a7ef6f322e5a5bce7a7f1a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 8 Jan 2024 20:44:15 -1000 Subject: [PATCH 187/300] Forgot to add module_utils/common/result.py --- plugins/module_utils/common/result.py | 159 ++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 plugins/module_utils/common/result.py diff --git a/plugins/module_utils/common/result.py b/plugins/module_utils/common/result.py new file mode 100644 index 000000000..1ab73a3d2 --- /dev/null +++ b/plugins/module_utils/common/result.py @@ -0,0 +1,159 @@ +# +# Copyright (c) 2024 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__ = "Allen Robel" + +import inspect +from typing import Any, Dict + + +class Result: + """ + Storage for module result + + Usage: + + result = Result(ansible_module) + result.deleted = deleted.diff # Appends to deleted-state changes + result.merged = merged.diff # Appends to merged-state changes + etc for other states + result = result.result + + print(result) + + # output will be a dict with the following structure: + { + "changed": True, # or False + "diff": [ + { + "deleted": [], + "merged": [], + "overridden": [], + "query": [], + "replaced": [] + } + ] + } + """ + + def __init__(self, ansible_module): + self.class_name = self.__class__.__name__ + self.ansible_module = ansible_module + self.states = ["deleted", "merged", "overridden", "query", "replaced"] + self._build_properties() + + def _build_properties(self): + """ + self.properties holds property values for the class + """ + self.properties: Dict[str, Any] = {} + for state in self.states: + self.properties[state] = [] + self.properties["changed"] = False + + def did_anything_change(self): + """ + return True if things have been appended to any of the state lists. + """ + for state in self.states: + if state == "query": + continue + if len(self.properties[state]) != 0: + return True + return False + + @property + def deleted(self): + """ + return changes for deleted state + """ + return self.properties["deleted"] + + @deleted.setter + def deleted(self, value): + self._verify_is_dict(value) + self.properties["deleted"].append(value) + + @property + def merged(self): + """ + return changes for merged state + """ + return self.properties["merged"] + + @merged.setter + def merged(self, value): + self._verify_is_dict(value) + self.properties["merged"].append(value) + + @property + def overridden(self): + """ + return changes for overridden state + """ + return self.properties["overridden"] + + @overridden.setter + def overridden(self, value): + self._verify_is_dict(value) + self.properties["overridden"].append(value) + + @property + def query(self): + """ + return changes for query state + """ + return self.properties["query"] + + @query.setter + def query(self, value): + self._verify_is_dict(value) + self.properties["query"].append(value) + + def _verify_is_dict(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "value must be a dict. " + msg += f"got {type(value).__name__} for " + msg += f"value {value}" + self.ansible_module.fail_json(msg, **self.result) + + @property + def replaced(self): + """ + return changes for replaced state + """ + return self.properties["replaced"] + + @replaced.setter + def replaced(self, value): + self._verify_is_dict(value) + self.properties["replaced"].append(value) + + @property + def result(self): + """ + return a result that AnsibleModule can use + """ + result = {} + result["changed"] = self.did_anything_change() + result["diff"] = {} + for state in self.states: + result["diff"][state] = self.properties[state] + return result From 95049e9849fef4ca8f8f9dee0a3bc88e0bc043a6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 8 Jan 2024 21:09:30 -1000 Subject: [PATCH 188/300] Switching to JSON for the logging config, more... Opps. YAML is not natively supported by Python. While it would be nicer to be able to use a YAML file for configuring the logging system, we'd have to update requirements.txt to include PyYAML, which wouldn't be good. Using JSON instead since it's build-in. --- plugins/module_utils/common/log.py | 31 ++++++++++++++-- plugins/module_utils/common/log.yaml | 26 -------------- .../module_utils/common/logging_config.json | 36 +++++++++++++++++++ plugins/modules/dcnm_image_upgrade.py | 10 +++--- 4 files changed, 69 insertions(+), 34 deletions(-) delete mode 100644 plugins/module_utils/common/log.yaml create mode 100644 plugins/module_utils/common/logging_config.json diff --git a/plugins/module_utils/common/log.py b/plugins/module_utils/common/log.py index 9394e9769..dc9a6a31d 100644 --- a/plugins/module_utils/common/log.py +++ b/plugins/module_utils/common/log.py @@ -21,10 +21,35 @@ import logging from logging.config import dictConfig -import yaml +import json class Log: + """ + Create the base dcnm logging object. + + Usage (where ansible_module is an instance of AnsibleModule): + + Below, config.json is a logging config file in JSON format conformant + with Python's logging.config.dictConfig. The file can be located + anywhere on the filesystem. See the following for an example: + + cisco/dcnm/plugins/module_utils/common/logging_config.json + + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log + log = Log(ansible_module) + log.config = "/path/to/logging/config.json" + log.commit() + + At this point, a base/parent logger is created for which all other + loggers throughout the dcnm collection will be children. + This allows for a single logging config to be used for all dcnm + modules, and allows for the logging config to be specified in a + single place external to the code. + + If log.config is set to None (which is the default if it's not explictely set), + then logging is disabled. + """ def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module @@ -58,7 +83,7 @@ def commit(self): try: with open(self.config, "r") as file: - logging_config = yaml.safe_load(file) + logging_config = json.load(file) except IOError as err: msg = f"error reading logging config from {self.config}. " msg += f"detail: {err}" @@ -71,7 +96,7 @@ def config(self): Can be either: 1. None, in which case logging is disabled - 2. A YAML file from which logging config is read. + 2. A JSON file from which logging config is read. Must conform to logging.config.dictConfig 3. A dictionary containing logging config Must conform to logging.config.dictConfig diff --git a/plugins/module_utils/common/log.yaml b/plugins/module_utils/common/log.yaml deleted file mode 100644 index b8a9e3811..000000000 --- a/plugins/module_utils/common/log.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -version: 1 -formatters: - standard: - class: logging.Formatter - format: "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s" -handlers: - file: - class: logging.handlers.RotatingFileHandler - formatter: standard - level: DEBUG - filename: "/tmp/dcnm.log" - mode: a - encoding: utf-8 - maxBytes: 500000 - backupCount: 4 -loggers: - dcnm: - handlers: - - file - level: DEBUG - propagate: false -root: - level: INFO - handlers: - - file diff --git a/plugins/module_utils/common/logging_config.json b/plugins/module_utils/common/logging_config.json new file mode 100644 index 000000000..64a660bf2 --- /dev/null +++ b/plugins/module_utils/common/logging_config.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "formatters": { + "standard": { + "class": "logging.Formatter", + "format": "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s" + } + }, + "handlers": { + "file": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "standard", + "level": "DEBUG", + "filename": "/tmp/dcnm.log", + "mode": "a", + "encoding": "utf-8", + "maxBytes": 500000, + "backupCount": 4 + } + }, + "loggers": { + "dcnm": { + "handlers": [ + "file" + ], + "level": "DEBUG", + "propagate": false + } + }, + "root": { + "level": "INFO", + "handlers": [ + "file" + ] + } +} \ No newline at end of file diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index fc2cc462e..2a0a09338 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1348,15 +1348,15 @@ def main(): ansible_module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) - # Create the base dcnm logger + # Create the base/parent logger for the dcnm collection. # To disable logging, comment out log.config = below - # log.config can be either a dictionary, or a path to a yaml file - # Both dictionary and yaml file formats must be conformant with + # log.config can be either a dictionary, or a path to a JSON file + # Both dictionary and JSON file formats must be conformant with # logging.config.dictConfig and must not log to the console. # For an example configuration, see: - # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/log.yaml + # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - # log.config = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm/plugins/module_utils/common/log.yaml" + # log.config = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm/plugins/module_utils/common/logging_config.json" log.commit() task_module = ImageUpgradeTask(ansible_module) From 198404e6b2423f9632b99ececf1ea69664547f60 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 8 Jan 2024 21:18:01 -1000 Subject: [PATCH 189/300] Appease the linters --- plugins/module_utils/common/log.py | 2 +- plugins/module_utils/common/params_merge_defaults.py | 2 +- plugins/module_utils/common/params_validate.py | 2 +- plugins/module_utils/image_mgmt/image_stage.py | 2 +- plugins/module_utils/image_mgmt/image_upgrade.py | 6 +++--- plugins/module_utils/image_mgmt/image_upgrade_common.py | 2 +- plugins/module_utils/image_mgmt/image_validate.py | 2 +- plugins/modules/dcnm_image_upgrade.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/module_utils/common/log.py b/plugins/module_utils/common/log.py index dc9a6a31d..db384366e 100644 --- a/plugins/module_utils/common/log.py +++ b/plugins/module_utils/common/log.py @@ -77,7 +77,7 @@ def commit(self): dictConfig(self.config) return except ValueError as err: - msg = f"error configuring logging from dict. " + msg = "error configuring logging from dict. " msg += f"detail: {err}" self.ansible_module.fail_json(msg=msg) diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index 3452a9e11..e989455b0 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -40,7 +40,7 @@ def __init__(self, ansible_module): self.ansible_module = ansible_module self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug(f"ENTERED") + self.log.debug("ENTERED") self._build_properties() self._build_reserved_params() diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 0505e27c3..a65bbbeee 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -83,7 +83,7 @@ def __init__(self, ansible_module): self.validation = validation self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug(f"ENTERED") + self.log.debug("ENTERED") self._build_properties() self._build_reserved_params() diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index ff7fc62a4..c1d4be219 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -104,7 +104,7 @@ def __init__(self, module): self.class_name = __class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug(f"ENTERED") + self.log.debug("ENTERED") self.endpoints = ApiEndpoints() self._init_properties() diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index f208a9f11..947a07b18 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -136,7 +136,7 @@ def __init__(self, module): self.class_name = __class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug(f"ENTERED") + self.log.debug("ENTERED") self.endpoints = ApiEndpoints() self.ipv4_done = set() @@ -856,7 +856,7 @@ def check_interval(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, int): msg = f"{self.class_name}.{method_name}: " - msg = f"instance.check_interval must be an integer." + msg = "instance.check_interval must be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @@ -872,7 +872,7 @@ def check_timeout(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, int): msg = f"{self.class_name}.{method_name}: " - msg = f"instance.check_timeout must be an integer." + msg = "instance.check_timeout must be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 427fe750e..76b1ed212 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -40,7 +40,7 @@ def __init__(self, module): self.class_name = __class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug(f"ENTERED") + self.log.debug("ENTERED") self.module = module self.params = module.params diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 1e8e1b347..9099c6565 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -73,7 +73,7 @@ def __init__(self, module): self.class_name = __class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug(f"ENTERED") + self.log.debug("ENTERED") self.endpoints = ApiEndpoints() diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 2a0a09338..b09d5bd2b 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1295,7 +1295,7 @@ def handle_query_state(self) -> None: Caller: main() """ - self.log.debug(f"ENTERED") + self.log.debug("ENTERED") instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() From 816b51895f820fe938a0cdc8716eba3f3383e82e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 8 Jan 2024 21:56:08 -1000 Subject: [PATCH 190/300] Fix pylint error for a couple earlier python versions Looks like bare __class__.__name__ doesn't work for some python versions. In all classes, changed: self.class_name = __class__.__name__ To: self.class_name = type(self).__name__ --- .../module_utils/common/controller_version.py | 2 +- plugins/module_utils/common/log.py | 2 +- plugins/module_utils/common/merge_dicts.py | 2 +- .../common/params_merge_defaults.py | 2 +- plugins/module_utils/common/params_validate.py | 2 +- .../module_utils/image_mgmt/image_policies.py | 2 +- .../image_mgmt/image_policy_action.py | 2 +- plugins/module_utils/image_mgmt/image_stage.py | 2 +- .../module_utils/image_mgmt/image_upgrade.py | 2 +- .../image_mgmt/image_upgrade_common.py | 2 +- .../module_utils/image_mgmt/image_validate.py | 2 +- .../module_utils/image_mgmt/install_options.py | 2 +- .../module_utils/image_mgmt/switch_details.py | 17 +++++++---------- .../image_mgmt/switch_issu_details.py | 8 ++++---- plugins/modules/dcnm_image_upgrade.py | 10 ++++------ 15 files changed, 27 insertions(+), 32 deletions(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 8f5a60e2a..51be5f0bf 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -66,7 +66,7 @@ class ControllerVersion(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = self.__class__.__name__ + self.class_name = type(self).__name__ self.endpoints = ApiEndpoints() self._init_properties() diff --git a/plugins/module_utils/common/log.py b/plugins/module_utils/common/log.py index db384366e..c8408a925 100644 --- a/plugins/module_utils/common/log.py +++ b/plugins/module_utils/common/log.py @@ -51,7 +51,7 @@ class Log: then logging is disabled. """ def __init__(self, ansible_module): - self.class_name = self.__class__.__name__ + self.class_name = type(self).__name__ self.ansible_module = ansible_module self._build_properties() diff --git a/plugins/module_utils/common/merge_dicts.py b/plugins/module_utils/common/merge_dicts.py index a7821e53f..f38ed1af5 100644 --- a/plugins/module_utils/common/merge_dicts.py +++ b/plugins/module_utils/common/merge_dicts.py @@ -49,7 +49,7 @@ class MergeDicts: """ def __init__(self, ansible_module): - self.class_name = self.__class__.__name__ + self.class_name = type(self).__name__ self.ansible_module = ansible_module self.log = Log(self.ansible_module) diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index e989455b0..fc1060355 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -36,7 +36,7 @@ class ParamsMergeDefaults: """ def __init__(self, ansible_module): - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.ansible_module = ansible_module self.log = logging.getLogger(f"dcnm.{self.class_name}") diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index a65bbbeee..2001592d4 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -78,7 +78,7 @@ class ParamsValidate: """ def __init__(self, ansible_module): - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.ansible_module = ansible_module self.validation = validation diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 5c06d6e68..7c5026676 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -56,7 +56,7 @@ class ImagePolicies(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("1. ENTERED") diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 3ea7519c9..64e880478 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -64,7 +64,7 @@ class ImagePolicyAction(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index c1d4be219..66db0a585 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -101,7 +101,7 @@ class ImageStage(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 947a07b18..a8e939812 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -133,7 +133,7 @@ class ImageUpgrade(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 76b1ed212..ea187f846 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -37,7 +37,7 @@ def __init__(self, module): """ def __init__(self, module): - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 9099c6565..3cd741754 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -70,7 +70,7 @@ class ImageValidate(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 20c75119d..4b74d0933 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -139,7 +139,7 @@ class ImageInstallOptions(ImageUpgradeCommon): def __init__(self, module) -> None: super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.endpoints = ApiEndpoints() self.path = self.endpoints.install_options.get("path") diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index f08cea002..9e8fbb737 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -50,15 +50,12 @@ class SwitchDetails(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.method_name = inspect.stack()[0][3] + self.class_name = type(self).__name__ - self.class_name = __class__.__name__ self.endpoints = ApiEndpoints() self._init_properties() def _init_properties(self): - self.method_name = inspect.stack()[0][3] - # self.properties is already initialized in the parent class self.properties["ip_address"] = None self.properties["response_data"] = None @@ -72,7 +69,7 @@ def refresh(self): Refresh switch_details with current switch details from the controller. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] path = self.endpoints.switches_info.get("path") verb = self.endpoints.switches_info.get("verb") @@ -81,7 +78,7 @@ def refresh(self): self.properties["result"] = self._handle_response(self.response, verb) if self.response["RETURN_CODE"] != 200: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Unable to retrieve switch information from the controller. " msg += f"Got response {self.response}" self.module.fail_json(msg, **self.failed_result) @@ -92,21 +89,21 @@ def refresh(self): self.properties["response_data"][switch["ipAddress"]] = switch def _get(self, item): - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] if self.ip_address is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "set instance.ip_address before accessing " msg += f"property {item}." self.module.fail_json(msg, **self.failed_result) if self.ip_address not in self.properties["response_data"]: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"{self.ip_address} does not exist on the controller." self.module.fail_json(msg, **self.failed_result) if item not in self.properties["response_data"][self.ip_address]: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += f"{self.ip_address} does not have a key named {item}." self.module.fail_json(msg, **self.failed_result) diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index dbcb9a392..9418acde4 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -89,7 +89,7 @@ class SwitchIssuDetails(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") @@ -679,7 +679,7 @@ class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") @@ -769,7 +769,7 @@ class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") @@ -861,7 +861,7 @@ class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED") diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index b09d5bd2b..1365beb8f 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -452,7 +452,7 @@ class ImageUpgradeTask(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = __class__.__name__ + self.class_name = type(self).__name__ method_name = inspect.stack()[0][3] self.endpoints = ApiEndpoints() @@ -505,8 +505,6 @@ def get_want(self) -> None: Update self.want for all switches defined in the playbook """ - method_name = inspect.stack()[0][3] - msg = "Calling _merge_global_and_switch_configs with " msg += f"self.config: {json.dumps(self.config, indent=4, sort_keys=True)}" self.log.debug(msg) @@ -568,8 +566,6 @@ def _build_idempotent_want(self, want) -> None: and the information returned by ImageInstallOptions. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - msg = f"want: {json.dumps(want, indent=4, sort_keys=True)}" self.log.debug(msg) @@ -1356,7 +1352,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - # log.config = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm/plugins/module_utils/common/logging_config.json" + # COLLECTION_PATH="/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + # CONFIG_FILE=f"{COLLECTION_PATH}/plugins/module_utils/common/logging_config.json" + # log.config = CONFIG_FILE log.commit() task_module = ImageUpgradeTask(ansible_module) From 82f3305062978d97d20432c88a5919b9f97d2684 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 9 Jan 2024 13:22:06 -1000 Subject: [PATCH 191/300] Fix a few things found with local linter, more... Also: 1. After some testing, it appears that the following are equivalent self.class_name = type(self).__name__ self.class_name = self.__class__.__name__ The second one seems more readable to me, so I changed things back. The issue I was trying to fix was that, when subclasses call super() on their parent class(es), the parent logs the subclass's class name. After a lot of testing, I think this is just the way things work. To make the logs more clear, I added an explicit (hardcoded) class name in the "ENTERED" debug log each class's __init__() method. This way, it's much clearer when a subclass calls super() on it's parent class. 2. Added encoding="utf-8" to the open() call in log.py to satisfy (local) pylint. 3. Added better docstring to the Log().commit() method. 4. Added logging to some classes that I'd missed (mostly in module_utils/common/). --- .../module_utils/common/controller_version.py | 8 +++- plugins/module_utils/common/log.py | 18 +++++++-- plugins/module_utils/common/merge_dicts.py | 38 ++----------------- .../common/params_merge_defaults.py | 4 +- .../module_utils/common/params_validate.py | 5 +-- plugins/module_utils/common/result.py | 23 ++++++++++- .../module_utils/image_mgmt/api_endpoints.py | 7 ++++ .../module_utils/image_mgmt/image_policies.py | 5 ++- .../image_mgmt/image_policy_action.py | 4 +- .../module_utils/image_mgmt/image_stage.py | 4 +- .../module_utils/image_mgmt/image_upgrade.py | 4 +- .../image_mgmt/image_upgrade_common.py | 7 ++-- .../module_utils/image_mgmt/image_validate.py | 6 +-- .../image_mgmt/install_options.py | 7 +++- .../module_utils/image_mgmt/switch_details.py | 6 ++- .../image_mgmt/switch_issu_details.py | 22 +++++------ plugins/modules/dcnm_image_upgrade.py | 14 ++++--- 17 files changed, 102 insertions(+), 80 deletions(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 51be5f0bf..55201e80a 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -22,6 +22,8 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import logging + from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ @@ -66,7 +68,11 @@ class ControllerVersion(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ControllerVersion()") + self.endpoints = ApiEndpoints() self._init_properties() diff --git a/plugins/module_utils/common/log.py b/plugins/module_utils/common/log.py index c8408a925..33eff8d2f 100644 --- a/plugins/module_utils/common/log.py +++ b/plugins/module_utils/common/log.py @@ -18,11 +18,10 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import json import logging from logging.config import dictConfig -import json - class Log: """ @@ -50,8 +49,9 @@ class Log: If log.config is set to None (which is the default if it's not explictely set), then logging is disabled. """ + def __init__(self, ansible_module): - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.ansible_module = ansible_module self._build_properties() @@ -61,6 +61,16 @@ def _build_properties(self) -> None: self.properties["config"] = None def commit(self): + """ + Create the base logger instance from a source conformant with + logging.config.dictConfig. + + 1. If self.config is None, then logging is disabled. + 2. If self.config is a JSON file, then it is read and logging + is configured from the JSON file. + 3. If self.config is a dictionary, then logging is configured + from the dictionary. + """ if self.config is None: logger = logging.getLogger() for handler in logger.handlers.copy(): @@ -82,7 +92,7 @@ def commit(self): self.ansible_module.fail_json(msg=msg) try: - with open(self.config, "r") as file: + with open(self.config, "r", encoding="utf-8") as file: logging_config = json.load(file) except IOError as err: msg = f"error reading logging config from {self.config}. " diff --git a/plugins/module_utils/common/merge_dicts.py b/plugins/module_utils/common/merge_dicts.py index f38ed1af5..561a71afd 100644 --- a/plugins/module_utils/common/merge_dicts.py +++ b/plugins/module_utils/common/merge_dicts.py @@ -20,11 +20,10 @@ import copy import inspect +import logging from collections.abc import MutableMapping as Map from typing import Any, Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log - class MergeDicts: """ @@ -49,12 +48,11 @@ class MergeDicts: """ def __init__(self, ansible_module): - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.ansible_module = ansible_module - self.log = Log(self.ansible_module) - self.log.debug = False - self.log.logfile = None + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED MergeDicts()") self._build_properties() @@ -95,34 +93,6 @@ def merge_dicts( dict1[key] = dict2[key] return copy.deepcopy(dict1) - @property - def debug(self): - """ - Enable/disable debugging to self.logfile - """ - return self.log.debug - - @debug.setter - def debug(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += "Invalid type for debug. Expected bool. " - msg += f"Got {type(value)}." - self.ansible_module.fail_json(msg) - self.log.debug = value - - @property - def logfile(self): - """ - Set file to which debug log is written - """ - return self.log.logfile - - @logfile.setter - def logfile(self, value): - self.log.logfile = value - @property def dict_merged(self): """ diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index fc1060355..cd28bc6f1 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -36,11 +36,11 @@ class ParamsMergeDefaults: """ def __init__(self, ansible_module): - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.ansible_module = ansible_module self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED ParamsMergeDefaults()") self._build_properties() self._build_reserved_params() diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 2001592d4..cf6dca2a2 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -25,7 +25,6 @@ from typing import Any, List from ansible.module_utils.common import validation -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log class ParamsValidate: @@ -78,12 +77,12 @@ class ParamsValidate: """ def __init__(self, ansible_module): - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.ansible_module = ansible_module self.validation = validation self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED ParamsValidate()") self._build_properties() self._build_reserved_params() diff --git a/plugins/module_utils/common/result.py b/plugins/module_utils/common/result.py index 1ab73a3d2..e2b0fd6f1 100644 --- a/plugins/module_utils/common/result.py +++ b/plugins/module_utils/common/result.py @@ -19,6 +19,7 @@ __author__ = "Allen Robel" import inspect +import logging from typing import Any, Dict @@ -28,9 +29,23 @@ class Result: Usage: + NOTES: + 1. Assumes deleted and merged are class instances with diff properties + that return the diff for the deleted and merged states. + 2. diff must be a dict() + 3. result.deleted, etc do not overwrite the existing value. They append + to it. So, for example: + result.deleted = {"foo": "bar"} + result.deleted = {"baz": "qux"} + print(result.deleted) + Output: [{"foo": "bar"}, {"baz": "qux"}] + result = Result(ansible_module) - result.deleted = deleted.diff # Appends to deleted-state changes - result.merged = merged.diff # Appends to merged-state changes + result.deleted = deleted.diff # Appends to deleted-state changes + result.merged = merged.diff # Appends to merged-state changes + # If a class doesn't have a diff property, then just append the dict + # that represents the changes for a given state. + result.overridden = {"foo": "bar"} etc for other states result = result.result @@ -54,6 +69,10 @@ class Result: def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED Result()") + self.states = ["deleted", "merged", "overridden", "query", "replaced"] self._build_properties() diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_mgmt/api_endpoints.py index 713350ae3..33462b7c7 100644 --- a/plugins/module_utils/image_mgmt/api_endpoints.py +++ b/plugins/module_utils/image_mgmt/api_endpoints.py @@ -18,6 +18,8 @@ __metaclass__ = type __author__ = "Allen Robel" +import logging + class ApiEndpoints: """ @@ -25,6 +27,11 @@ class ApiEndpoints: """ def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ApiEndpoints()") + self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" self.endpoint_feature_manager = f"{self.endpoint_api_v1}/fm" diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 7c5026676..fb6568d34 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -56,9 +56,10 @@ class ImagePolicies(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("1. ENTERED") + self.log.debug("ENTERED ImagePolicies()") self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 64e880478..5e65f3261 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -64,10 +64,10 @@ class ImagePolicyAction(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED ImagePolicyAction()") method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 66db0a585..90a1d5f82 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -101,10 +101,10 @@ class ImageStage(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED ImageStage()") self.endpoints = ApiEndpoints() self._init_properties() diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index a8e939812..35a0b9fc5 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -133,10 +133,10 @@ class ImageUpgrade(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED ImageUpgrade()") self.endpoints = ApiEndpoints() self.ipv4_done = set() diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index ea187f846..2eeb6fbd6 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -21,8 +21,6 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log - class ImageUpgradeCommon: """ @@ -37,10 +35,11 @@ def __init__(self, module): """ def __init__(self, module): - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + msg = "ENTERED ImageUpgradeCommon()" + self.log.debug(msg) self.module = module self.params = module.params diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 3cd741754..e17f33001 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -23,7 +23,7 @@ import json import logging from time import sleep -from typing import Any, Dict, List, Set +from typing import List, Set from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -70,10 +70,10 @@ class ImageValidate(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED ImageValidate()") self.endpoints = ApiEndpoints() diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 4b74d0933..c3d37f075 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -20,6 +20,7 @@ import inspect import json +import logging from typing import Any, Dict from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -139,7 +140,11 @@ class ImageInstallOptions(ImageUpgradeCommon): def __init__(self, module) -> None: super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ImageInstallOptions()") + self.endpoints = ApiEndpoints() self.path = self.endpoints.install_options.get("path") diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index 9e8fbb737..ae2566989 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -19,6 +19,7 @@ __author__ = "Allen Robel" import inspect +import logging from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -50,7 +51,10 @@ class SwitchDetails(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED SwitchDetails()") self.endpoints = ApiEndpoints() self._init_properties() diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 9418acde4..f476e74a3 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -89,10 +89,10 @@ class SwitchIssuDetails(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED SwitchIssuDetails()") self.endpoints = ApiEndpoints() self._init_properties() @@ -159,7 +159,6 @@ def _get(self, item): """ overridden in subclasses """ - pass @property def response_data(self): @@ -679,11 +678,12 @@ class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED SwitchIssuDetailsByIpAddress()") + self.data_subclass = {} self._init_properties() def _init_properties(self): @@ -768,12 +768,12 @@ class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED SwitchIssuDetailsBySerialNumber()") + self.data_subclass = {} self._init_properties() def _init_properties(self): @@ -860,12 +860,12 @@ class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): def __init__(self, module): super().__init__(module) - - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED") + self.log.debug("ENTERED SwitchIssuDetailsByDeviceName()") + self.data_subclass = {} self._init_properties() def _init_properties(self): diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 1365beb8f..01c9fee47 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -403,6 +403,7 @@ import copy import inspect import json +import logging from typing import Any, Dict, List from ansible.module_utils.basic import AnsibleModule @@ -452,9 +453,12 @@ class ImageUpgradeTask(ImageUpgradeCommon): def __init__(self, module): super().__init__(module) - self.class_name = type(self).__name__ + self.class_name = self.__class__.__name__ method_name = inspect.stack()[0][3] + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ImageUpgradeTask()") + self.endpoints = ApiEndpoints() self.have = None @@ -1291,8 +1295,6 @@ def handle_query_state(self) -> None: Caller: main() """ - self.log.debug("ENTERED") - instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() @@ -1352,9 +1354,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - # COLLECTION_PATH="/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - # CONFIG_FILE=f"{COLLECTION_PATH}/plugins/module_utils/common/logging_config.json" - # log.config = CONFIG_FILE + # collection_path="/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + # config_file=f"{collection_path}/plugins/module_utils/common/logging_config.json" + # log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) From 0bbafccfa2dfe4a23da7522954964a85abf2e060 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 9 Jan 2024 16:51:33 -1000 Subject: [PATCH 192/300] Remove unit test for MergeDicts.debug @property --- .../module_utils/common/test_merge_dicts.py | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/tests/unit/module_utils/common/test_merge_dicts.py b/tests/unit/module_utils/common/test_merge_dicts.py index 51f90ac9d..e089f526c 100644 --- a/tests/unit/module_utils/common/test_merge_dicts.py +++ b/tests/unit/module_utils/common/test_merge_dicts.py @@ -52,8 +52,6 @@ def test_merge_dicts_00001(merge_dicts) -> None: assert isinstance(instance, MergeDicts) assert isinstance(instance.properties, dict) assert instance.class_name == "MergeDicts" - assert instance.log.logfile is None - assert instance.log.debug is False assert instance.properties.get("dict1", "foo") is None assert instance.properties.get("dict2", "foo") is None assert instance.properties.get("dict_merged", "foo") is None @@ -152,38 +150,6 @@ def test_merge_dicts_00030(merge_dicts, dict1, dict2, expected) -> None: instance.commit() -MATCH_00040 = "Invalid type for debug. Expected bool. " - - -@pytest.mark.parametrize( - "value, expected", - [ - (True, does_not_raise()), - (False, does_not_raise()), - ([], pytest.raises(AnsibleFailJson, match=MATCH_00040)), - ((), pytest.raises(AnsibleFailJson, match=MATCH_00040)), - (None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), - (1, pytest.raises(AnsibleFailJson, match=MATCH_00040)), - (1.1, pytest.raises(AnsibleFailJson, match=MATCH_00040)), - ("foo", pytest.raises(AnsibleFailJson, match=MATCH_00040)), - ], -) -def test_merge_dicts_00040(merge_dicts, value, expected) -> None: - """ - Function - - debug - - Test - - debug accepts boolean values - - debug calls fail_json for non-boolean values - """ - with does_not_raise(): - instance = merge_dicts - - with expected: - instance.debug = value - - def test_merge_dicts_00041(merge_dicts) -> None: """ Function From 655579610790fcbcccc75390d88abddc0ced1fe2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 9 Jan 2024 20:25:27 -1000 Subject: [PATCH 193/300] Modify unit tests for the new logger implementation 1. Remove log tests from test_params_validate.py 2. Rewrite test in test_log.py --- tests/unit/module_utils/common/test_log.py | 170 +++++++++++------- .../common/test_params_validate.py | 2 - 2 files changed, 101 insertions(+), 71 deletions(-) diff --git a/tests/unit/module_utils/common/test_log.py b/tests/unit/module_utils/common/test_log.py index b25ada2c1..9d9367cdf 100644 --- a/tests/unit/module_utils/common/test_log.py +++ b/tests/unit/module_utils/common/test_log.py @@ -28,119 +28,151 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import logging from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.mock_ansible_module import \ + MockAnsibleModule from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( does_not_raise, log_fixture) -def test_log_00010(log) -> None: +def test_log_00010(tmp_path, log) -> None: """ Function - - log_msg + - log.info() + - log.debug() + - log.warning() + - log.critical() Test - - log_msg returns None when debug is False + - log. logs to the logfile + - The function name is in the log message + (the formatter includes the function name) """ with does_not_raise(): - instance = log - - error_message = "This is an error message" - instance.debug = False - assert instance.log_msg(error_message) is None + ansible_module = MockAnsibleModule() + instance = Log(ansible_module) - -def test_log_00011(tmp_path, log) -> None: - """ - Function - - log_msg - - Test - - log_msg does not write to the logfile when debug is False - """ directory = tmp_path / "test_log_msg" directory.mkdir() filename = directory / "test_log_msg.txt" - msg = "This is an error message" - - with does_not_raise(): - instance = log - instance.debug = False - instance.logfile = filename - instance.log_msg(msg) - - match = r"\[Errno 2\] " - match += "No such file or directory" - with pytest.raises(FileNotFoundError, match=match): - filename.read_text(encoding="UTF-8") - - -def test_log_00012(tmp_path, log) -> None: + config = { + "version": 1, + "formatters": { + "standard": { + "class": "logging.Formatter", + "format": "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s", + } + }, + "handlers": { + "file": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "standard", + "level": "DEBUG", + "filename": filename, + "mode": "a", + "encoding": "utf-8", + "maxBytes": 500000, + "backupCount": 4, + } + }, + "loggers": { + "dcnm": {"handlers": ["file"], "level": "DEBUG", "propagate": False} + }, + "root": {"level": "INFO", "handlers": ["file"]}, + } + info_msg = "foo" + debug_msg = "bing" + warning_msg = "bar" + critical_msg = "baz" + instance.config = config + instance.commit() + log = logging.getLogger(f"dcnm.test_logger") + log.info(info_msg) + log.debug(debug_msg) + log.warning(warning_msg) + log.critical(critical_msg) + assert logging.getLevelName(log.getEffectiveLevel()) == "DEBUG" + assert info_msg in filename.read_text(encoding="UTF-8") + assert debug_msg in filename.read_text(encoding="UTF-8") + assert warning_msg in filename.read_text(encoding="UTF-8") + assert critical_msg in filename.read_text(encoding="UTF-8") + # test that the function name is in the log message + assert "test_log_00010" in filename.read_text(encoding="UTF-8") + + +def test_log_00011(caplog, log) -> None: """ Function - - log_msg + - log.config + - log.commit() Test - - log_msg writes to the logfile when debug is True + - Nothing is logged when config is None """ - directory = tmp_path / "test_log_msg" - directory.mkdir() - filename = directory / "test_log_msg.txt" - - msg = "This is an error message" - with does_not_raise(): - instance = log - instance.debug = True - instance.logfile = filename - instance.log_msg(msg) - - assert filename.read_text(encoding="UTF-8") == msg + "\n" - - -def test_log_00013(tmp_path, log) -> None: + ansible_module = MockAnsibleModule() + instance = Log(ansible_module) + instance.commit() + + info_msg = "foo" + debug_msg = "bing" + warning_msg = "bar" + critical_msg = "baz" + log = logging.getLogger(f"dcnm.test_logger") + log.info(info_msg) + log.debug(debug_msg) + log.warning(warning_msg) + log.critical(critical_msg) + assert info_msg not in caplog.text + assert debug_msg not in caplog.text + assert warning_msg not in caplog.text + assert critical_msg not in caplog.text + + +def test_log_00012(log) -> None: """ Function - - log_msg + - log.config + - log.commit() Test - - log_msg calls fail_json if the logfile cannot be opened - - Description - To ensure an error is generated, we attempt a write to a filename - that is too long for the target OS. + - fail_json is called if dictConfig raises a ValueError """ - directory = tmp_path / "test_log_msg" - directory.mkdir() - filename = directory / f"test_{'a' * 2000}_log_msg.txt" - - msg = "This is an error message" + match = "error configuring logging from dict. " + match += "detail: dictionary doesn't specify a version" with does_not_raise(): instance = log - instance.debug = True - instance.logfile = filename - match = "error writing to logfile" + instance.config = {} with pytest.raises(AnsibleFailJson, match=match): - instance.log_msg(msg) + instance.commit() -def test_log_00020(log) -> None: +def test_log_00013(log) -> None: """ Function - - debug + - log.config + - log.commit() Test - - log_msg calls fail_json if debug is not a boolean + - fail_json is called if config file doesn't exist """ + match = "error reading logging config from " + match += r"\/foo\/bar\/baz\/loony\.json\. " + match += r"detail: \[Errno 2\] No such file or directory: " + match += r"'\/foo\/bar\/baz\/loony\.json'" + with does_not_raise(): instance = log - match = "Invalid type for debug. Expected bool. " + instance.config = "/foo/bar/baz/loony.json" with pytest.raises(AnsibleFailJson, match=match): - instance.debug = 10 + instance.commit() diff --git a/tests/unit/module_utils/common/test_params_validate.py b/tests/unit/module_utils/common/test_params_validate.py index 1c7cb217a..c5361b3d1 100644 --- a/tests/unit/module_utils/common/test_params_validate.py +++ b/tests/unit/module_utils/common/test_params_validate.py @@ -65,8 +65,6 @@ def test_params_validate_00001(params_validate) -> None: } assert instance.mandatory_param_spec_keys == {"required", "type"} assert instance.class_name == "ParamsValidate" - assert instance.log.logfile is None - assert instance.log.debug is False assert instance.properties.get("parameters", "foo") is None assert instance.properties.get("params_spec", "foo") is None From c16309ce003e6482ed0db401f69b47fcb0c9d01e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 9 Jan 2024 20:32:21 -1000 Subject: [PATCH 194/300] Fix pylint f-string errors --- tests/unit/module_utils/common/test_log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/module_utils/common/test_log.py b/tests/unit/module_utils/common/test_log.py index 9d9367cdf..cd22e21ef 100644 --- a/tests/unit/module_utils/common/test_log.py +++ b/tests/unit/module_utils/common/test_log.py @@ -93,7 +93,7 @@ def test_log_00010(tmp_path, log) -> None: critical_msg = "baz" instance.config = config instance.commit() - log = logging.getLogger(f"dcnm.test_logger") + log = logging.getLogger("dcnm.test_logger") log.info(info_msg) log.debug(debug_msg) log.warning(warning_msg) @@ -125,7 +125,7 @@ def test_log_00011(caplog, log) -> None: debug_msg = "bing" warning_msg = "bar" critical_msg = "baz" - log = logging.getLogger(f"dcnm.test_logger") + log = logging.getLogger("dcnm.test_logger") log.info(info_msg) log.debug(debug_msg) log.warning(warning_msg) From 54f63c354ebbf008907a3f6d066c0faeb1aba5d8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 10 Jan 2024 15:01:08 -1000 Subject: [PATCH 195/300] Enhance playbook results 1. Enhance result for "deleted" state. Per-switch result for "deleted" state now includes action, ip_address, logical_name, policy, and serial_number. 2. Add response list to Result class and update "deleted" state responses to include per-policy response. 3. Update "query" state results to include controller responses. TODO: Need to update merged state results... --- plugins/module_utils/common/result.py | 19 ++++++++ .../image_mgmt/image_policy_action.py | 8 +++- .../image_mgmt/image_upgrade_common.py | 1 + plugins/modules/dcnm_image_upgrade.py | 46 ++++++++++++++----- .../test_image_upgrade_image_upgrade_task.py | 1 + 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/plugins/module_utils/common/result.py b/plugins/module_utils/common/result.py index e2b0fd6f1..469692b77 100644 --- a/plugins/module_utils/common/result.py +++ b/plugins/module_utils/common/result.py @@ -39,6 +39,8 @@ class Result: result.deleted = {"baz": "qux"} print(result.deleted) Output: [{"foo": "bar"}, {"baz": "qux"}] + 4. result.response is a list of dicts. Each dict represents a response + from the controller. result = Result(ansible_module) result.deleted = deleted.diff # Appends to deleted-state changes @@ -47,6 +49,8 @@ class Result: # that represents the changes for a given state. result.overridden = {"foo": "bar"} etc for other states + # If you want to append a response from the controller, then do this: + result.response = response result = result.result print(result) @@ -63,6 +67,7 @@ class Result: "replaced": [] } ] + "response": [] } """ @@ -84,6 +89,7 @@ def _build_properties(self): for state in self.states: self.properties[state] = [] self.properties["changed"] = False + self.properties["response"] = [] def did_anything_change(self): """ @@ -175,4 +181,17 @@ def result(self): result["diff"] = {} for state in self.states: result["diff"][state] = self.properties[state] + result["response"] = self.properties["response"] return result + + @property + def response(self): + """ + return the controller response(s) + """ + return self.properties["response"] + + @response.setter + def response(self, value): + self._verify_is_dict(value) + self.properties["response"].append(value) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 5e65f3261..deeb41fca 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -80,7 +80,6 @@ def __init__(self, module): self.verb = None def _init_properties(self): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable # self.properties is already initialized in the parent class self.properties["action"] = None self.properties["response"] = None @@ -242,6 +241,12 @@ def _detach_policy(self): self.properties["response"] = dcnm_send(self.module, self.verb, self.path) self.properties["result"] = self._handle_response(self.response, self.verb) + msg = f"result: {json.dumps(self.result, indent=4)}" + self.log.debug(msg) + + msg = f"response: {json.dumps(self.response, indent=4)}" + self.log.debug(msg) + if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when detaching policy {self.policy_name} " @@ -338,7 +343,6 @@ def policy_name(self): @policy_name.setter def policy_name(self, value): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.properties["policy_name"] = value @property diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 2eeb6fbd6..2b29d7acc 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -47,6 +47,7 @@ def __init__(self, module): self.properties["changed"] = False self.properties["diff"] = [] self.properties["failed"] = False + self.properties["response"] = [] def _handle_response(self, response, verb): """ diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 01c9fee47..37f9118da 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1208,7 +1208,9 @@ def _upgrade_images(self, devices) -> None: upgrade.devices = devices upgrade.commit() for diff in upgrade.diff: - self.result.merged.append(diff) + self.log.debug(f"diff: {json.dumps(diff, indent=4, sort_keys=True)}") + # self.result.merged.append(diff) + self.result.merged = diff def handle_merged_state(self) -> None: """ @@ -1262,32 +1264,52 @@ def handle_deleted_state(self) -> None: Caller: main() """ detach_policy_devices: Dict[str, Any] = {} - + detach_policy_serial_numbers: Dict[str, Any] = {} self.switch_details.refresh() self.image_policies.refresh() for switch in self.need: self.switch_details.ip_address = switch.get("ip_address") self.image_policies.policy_name = switch.get("policy") + device = {} + device["serial_number"] = self.switch_details.serial_number + device["logical_name"] = self.switch_details.logical_name + device["ip_address"] = self.switch_details.ip_address + device["policy"] = self.image_policies.policy_name if self.image_policies.name not in detach_policy_devices: detach_policy_devices[self.image_policies.policy_name] = [] - detach_policy_devices[self.image_policies.policy_name].append( + if self.image_policies.name not in detach_policy_serial_numbers: + detach_policy_serial_numbers[self.image_policies.policy_name] = [] + + detach_policy_serial_numbers[self.image_policies.policy_name].append( self.switch_details.serial_number ) + detach_policy_devices[self.image_policies.policy_name].append(device) if len(detach_policy_devices) == 0: - self.result.result["changed"] = False return instance = ImagePolicyAction(self.module) - for key, value in detach_policy_devices.items(): + detach_results = [] + for key, value in detach_policy_serial_numbers.items(): + detach_result = {} instance.policy_name = key instance.action = "detach" instance.serial_numbers = value instance.commit() - for diff in instance.diff: - self.result.deleted = diff + self.result.response = copy.deepcopy(instance.response) + for device in detach_policy_devices[key]: + detach_result["action"] = "detach" + detach_result["ip_address"] = device.get("ip_address") + detach_result["logical_name"] = device.get("logical_name") + detach_result["policy"] = key + detach_result["serial_number"] = device.get("serial_number") + detach_results.append(copy.deepcopy(detach_result)) + for item in detach_results: + msg = f"item: {json.dumps(item, indent=4, sort_keys=True)}" + self.log.debug(msg) + self.result.deleted = item def handle_query_state(self) -> None: """ @@ -1297,7 +1319,9 @@ def handle_query_state(self) -> None: """ instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() - + response = copy.deepcopy(instance.response) + response.pop("DATA") + self.result.response = copy.deepcopy(response) for switch in self.need: instance.filter = switch.get("ip_address") if instance.filtered_data is None: @@ -1354,9 +1378,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - # collection_path="/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - # config_file=f"{collection_path}/plugins/module_utils/common/logging_config.json" - # log.config = config_file + collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 2bbbd375e..bed3ec72a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -99,6 +99,7 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: "query": [], "replaced": [], }, + "response": [] } assert isinstance(instance.switch_details, SwitchDetails) assert isinstance(instance.image_policies, ImagePolicies) From 28af1eb3903cc245210e3978892a6eca825d23b0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 10 Jan 2024 15:05:32 -1000 Subject: [PATCH 196/300] Fix pylint error --- plugins/modules/dcnm_image_upgrade.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 37f9118da..744df6fd7 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1208,8 +1208,6 @@ def _upgrade_images(self, devices) -> None: upgrade.devices = devices upgrade.commit() for diff in upgrade.diff: - self.log.debug(f"diff: {json.dumps(diff, indent=4, sort_keys=True)}") - # self.result.merged.append(diff) self.result.merged = diff def handle_merged_state(self) -> None: From 4243cb7c3dda55016bd4c14a1bf350c43e56da41 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 10 Jan 2024 16:56:28 -1000 Subject: [PATCH 197/300] Result handling for merged state --- .../module_utils/image_mgmt/image_upgrade.py | 37 +++++++++++++------ plugins/modules/dcnm_image_upgrade.py | 2 + 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 35a0b9fc5..086e1c5dd 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -172,9 +172,9 @@ def _init_properties(self) -> None: self.properties["epld_module"] = "ALL" self.properties["epld_upgrade"] = False self.properties["force_non_disruptive"] = False - self.properties["response_data"] = None - self.properties["result"] = None - self.properties["response"] = None + self.properties["response_data"] = [] + self.properties["result"] = [] + self.properties["response"] = [] self.properties["non_disruptive"] = False self.properties["package_install"] = False self.properties["package_uninstall"] = False @@ -468,14 +468,12 @@ def commit(self) -> None: msg = f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - self.properties["response"] = dcnm_send( + response = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) - self.properties["result"] = self._handle_response(self.response, self.verb) + self.properties["result"] = self._handle_response(response, self.verb) - msg = ( - f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" - ) + msg = f"response: {json.dumps(response, indent=4, sort_keys=True)}" self.log.debug(msg) if not self.result["success"]: @@ -484,11 +482,13 @@ def commit(self) -> None: msg += f"Controller response: {self.response}" self.module.fail_json(msg, **self.failed_result) - self.properties["response_data"] = self.response.get("DATA") - self._wait_for_image_upgrade_to_complete() + response_data = response.get("DATA") - self.changed = True - self.diff = self.response + self.response = copy.deepcopy(response) + self.response_data = response_data + self.diff = copy.deepcopy(self.payload) + + self._wait_for_image_upgrade_to_complete() def _wait_for_current_actions_to_complete(self): """ @@ -889,6 +889,10 @@ def response_data(self): """ return self.properties.get("response_data") + @response_data.setter + def response_data(self, value): + self.properties["response_data"].append(value) + @property def result(self): """ @@ -906,3 +910,12 @@ def response(self): instance.commit() must be called first. """ return self.properties.get("response") + + @response.setter + def response(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response must be a dict." + self.module.fail_json(msg, **self.failed_result) + self.properties["response"].append(value) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 744df6fd7..f46825eba 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1209,6 +1209,8 @@ def _upgrade_images(self, devices) -> None: upgrade.commit() for diff in upgrade.diff: self.result.merged = diff + for response in upgrade.response: + self.result.response = response def handle_merged_state(self) -> None: """ From 2fbf843cf3f06c2587f3acec3863e3f2ddfb801e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 10 Jan 2024 18:47:22 -1000 Subject: [PATCH 198/300] Fix unit tests to reflect changes in the last commit --- plugins/module_utils/image_mgmt/image_upgrade.py | 2 +- .../test_image_upgrade_image_upgrade.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 086e1c5dd..7fa91d17f 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -479,7 +479,7 @@ def commit(self) -> None: if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.result}. " - msg += f"Controller response: {self.response}" + msg += f"Controller response: {response}" self.module.fail_json(msg, **self.failed_result) response_data = response.get("DATA") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index bd2cab7d6..07bd3eca2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -90,9 +90,9 @@ def test_image_mgmt_upgrade_00003(image_upgrade) -> None: assert instance.properties.get("epld_module") == "ALL" assert instance.properties.get("epld_upgrade") is False assert instance.properties.get("force_non_disruptive") is False - assert instance.properties.get("response_data") is None - assert instance.properties.get("response") is None - assert instance.properties.get("result") is None + assert instance.properties.get("response_data") == [] + assert instance.properties.get("response") == [] + assert instance.properties.get("result") == [] assert instance.properties.get("non_disruptive") is False assert instance.properties.get("force_non_disruptive") is False assert instance.properties.get("package_install") is False @@ -1516,7 +1516,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] instance.commit() - assert instance.response_data == 121 + assert instance.response_data == [121] def test_image_mgmt_upgrade_00046(monkeypatch, image_upgrade) -> None: @@ -1609,7 +1609,7 @@ def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: Expected results: - 1. instance.response is a dict + 1. instance.response is a list """ instance = image_upgrade @@ -1663,8 +1663,8 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): ] instance.commit() print(f"instance.response: {instance.response}") - assert isinstance(instance.response, dict) - assert instance.response["DATA"] == 121 + assert isinstance(instance.response, list) + assert instance.response[0]["DATA"] == 121 # setters From b5b5d8e16f21d4b34f1bf5130213b58d700b0c02 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 11 Jan 2024 09:07:04 -1000 Subject: [PATCH 199/300] Remove one call to issu_details.refresh() in _build_payload(), more... Also: 1. Add debug logs in for all refresh() calls (for all instances that touch the controller) in image_upgrade.py 2. Fix typo in docstring for filtered_data property 3. Update key for unit test: test_image_mgmt_upgrade_00004 4. Comment out log.config assignment in dcnm_image_upgrade.py to disable logging --- plugins/module_utils/image_mgmt/image_upgrade.py | 16 +++++++++++++++- .../image_mgmt/switch_issu_details.py | 2 +- plugins/modules/dcnm_image_upgrade.py | 6 +++--- .../test_image_upgrade_image_upgrade.py | 3 ++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 7fa91d17f..6d42a9a37 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -199,6 +199,9 @@ def _validate_devices(self) -> None: """ for device in self.devices: self.issu_detail.filter = device.get("ip_address") + # See TODO in commit() method regarding removal of this + # call to refresh() in the future. + self.log.debug("Calling issu_detail.refresh()") self.issu_detail.refresh() # Any device validation from issu_detail would go here. @@ -215,8 +218,9 @@ def _build_payload(self, device) -> None: """ Build the request payload to upgrade the switches. """ + # issu_detail.refresh() has already been called in _validate_devices() + # so no need to call it here. self.issu_detail.filter = device.get("ip_address") - self.issu_detail.refresh() self.install_options.serial_number = self.issu_detail.serial_number # install_options will fail_json if any of these are invalid @@ -227,6 +231,7 @@ def _build_payload(self, device) -> None: self.install_options.package_install = ( device.get("options", {}).get("package", {}).get("install", None) ) + self.log.debug("Calling install_options.refresh()") self.install_options.refresh() # devices_to_upgrade must currently be a single device @@ -450,6 +455,13 @@ def commit(self) -> None: msg += "call instance.devices before calling commit." self.module.fail_json(msg, **self.failed_result) + # TODO: We could get rid of calls to issu_detail.refresh() + # in _validate_devices() and _build_payload() by calling + # it once here. However, unit test for _validate_devices() + # will need to be changed or removed. I'll work on this + # later. + # self.issu_detail.refresh() + self._validate_devices() self._wait_for_current_actions_to_complete() @@ -511,6 +523,7 @@ def _wait_for_current_actions_to_complete(self): continue self.issu_detail.filter = ipv4 + self.log.debug("Calling issu_detail.refresh()") self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: @@ -547,6 +560,7 @@ def _wait_for_image_upgrade_to_complete(self): continue self.issu_detail.filter = ipv4 + self.log.debug("Calling issu_detail.refresh()") self.issu_detail.refresh() ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index f476e74a3..7f80b91a3 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -821,7 +821,7 @@ def _get(self, item): def filtered_data(self): """ Return a dictionary of the switch matching self.serial_number. - Return None of the switch does not exist in NDFC. + Return None if the switch does not exist in NDFC. """ return self.data_subclass.get(self.filter) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index f46825eba..505612499 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1378,9 +1378,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - log.config = config_file + # collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + # config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + # log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 07bd3eca2..e2c33d1c7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -128,7 +128,7 @@ def test_image_mgmt_upgrade_00004(monkeypatch, image_upgrade) -> None: instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_upgrade_00005a" + key = "test_image_mgmt_upgrade_00004a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -136,6 +136,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] instance.devices = devices + instance._validate_devices() # pylint: disable=protected-access assert isinstance(instance.ip_addresses, set) assert len(instance.ip_addresses) == 2 From 4dc4cfdc4eef18a7a12abacc8acf177b193dde8c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 11 Jan 2024 09:46:10 -1000 Subject: [PATCH 200/300] Remove a TODO that was harder than I thought 1. Getting rid of one extra call to issu_detail.refresh() isn't worth the effort given that we call issu_detail.refresh() many times within the for loops (by nesessity) in both _wait_for_current_actions_to_complete() and _wait_for_image_upgrade_to_complete(). 2. Rearranged lines in test_image_mgmt_upgrade_00004 (cosmetic change). --- plugins/module_utils/image_mgmt/image_upgrade.py | 10 ---------- .../test_image_upgrade_image_upgrade.py | 7 +++---- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 6d42a9a37..a2fa2de4f 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -199,9 +199,6 @@ def _validate_devices(self) -> None: """ for device in self.devices: self.issu_detail.filter = device.get("ip_address") - # See TODO in commit() method regarding removal of this - # call to refresh() in the future. - self.log.debug("Calling issu_detail.refresh()") self.issu_detail.refresh() # Any device validation from issu_detail would go here. @@ -455,13 +452,6 @@ def commit(self) -> None: msg += "call instance.devices before calling commit." self.module.fail_json(msg, **self.failed_result) - # TODO: We could get rid of calls to issu_detail.refresh() - # in _validate_devices() and _build_payload() by calling - # it once here. However, unit test for _validate_devices() - # will need to be changed or removed. I'll work on this - # later. - # self.issu_detail.refresh() - self._validate_devices() self._wait_for_current_actions_to_complete() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index e2c33d1c7..408fc0ab3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -125,7 +125,10 @@ def test_image_mgmt_upgrade_00004(monkeypatch, image_upgrade) -> None: 1. instance.ip_addresses will contain {"172.22.150.102", "172.22.150.108"} """ + devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] + instance = image_upgrade + instance.devices = devices def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_upgrade_00004a" @@ -133,10 +136,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - devices = [{"ip_address": "172.22.150.102"}, {"ip_address": "172.22.150.108"}] - - instance.devices = devices - instance._validate_devices() # pylint: disable=protected-access assert isinstance(instance.ip_addresses, set) assert len(instance.ip_addresses) == 2 From 06c673e4c740be4d865eef63e763e683d48df3a3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 11 Jan 2024 21:57:35 -1000 Subject: [PATCH 201/300] Modified returned result not to use ansible state names In preparation for writing integration tests, I realized that the returned results weren't very good. There wasn't a clean way to categorize the result sections by Ansible state (at least I couldn't think of one). 1. Deleted state detaches image policies from switches 2. Merged state attaches policies, stages and validates images, and upgrades switches. 3. Query state simply returns the current ISSU state of the switch(es). Given that the above don't really fix neatly into similar per-state actions, I removed Ansible state names from result section names and, instead, used names that describe what is being returned. So, we have the following subsections in both the "diff" and "response" sections: attach_policy detach_policy issu_status stage upgrade validate --- .../module_utils/image_mgmt/image_stage.py | 21 +- .../module_utils/image_mgmt/image_upgrade.py | 6 + .../image_mgmt/image_upgrade_common.py | 28 +- .../image_mgmt/image_upgrade_task_result.py | 342 ++++++++++++++++++ .../module_utils/image_mgmt/image_validate.py | 15 +- plugins/modules/dcnm_image_upgrade.py | 82 +++-- .../test_image_upgrade_image_upgrade_task.py | 22 +- 7 files changed, 472 insertions(+), 44 deletions(-) create mode 100644 plugins/module_utils/image_mgmt/image_upgrade_task_result.py diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 90a1d5f82..ca0cccdc3 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -175,13 +175,23 @@ def commit(self): """ method_name = inspect.stack()[0][3] + msg = "ENTERED commit()" + self.log.debug(msg) + if self.serial_numbers is None: msg = f"{self.class_name}.{method_name}: " msg += "call instance.serial_numbers " msg += "before calling commit." self.module.fail_json(msg, **self.failed_result) + msg = f"self.serial_numbers: {self.serial_numbers}" + self.log.debug(msg) if len(self.serial_numbers) == 0: + self.properties["response"] = {"response": "No serial numbers to stage."} + self.properties["response_data"] = { + "response": "No serial numbers to stage." + } + self.properties["result"] = {"success": True} return self.prune_serial_numbers() @@ -211,7 +221,9 @@ def commit(self): msg += f"Controller response: {self.response}" self.module.fail_json(msg, **self.failed_result) - self.properties["response_data"] = self.response.get("DATA") + msg = f"self.response: {self.response}" + self.log.debug(msg) + self.properties["response_data"] = self.response.get("DATA", "No Stage DATA") self._wait_for_image_stage_to_complete() def _wait_for_current_actions_to_complete(self): @@ -284,6 +296,13 @@ def _wait_for_image_stage_to_complete(self): if staged_status == "Success": self.serial_numbers_done.add(serial_number) + msg = f"seconds remaining {timeout}" + self.log.debug(msg) + msg = f"serial_numbers_todo: {serial_numbers_todo}" + self.log.debug(msg) + msg = f"serial_numbers_done: {self.serial_numbers_done}" + self.log.debug(msg) + if self.serial_numbers_done != serial_numbers_todo: msg = f"{self.class_name}.{method_name}: " msg += "Timed out waiting for image stage to complete. " diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index a2fa2de4f..dfd3c6c97 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -572,6 +572,12 @@ def _wait_for_image_upgrade_to_complete(self): if upgrade_status == "Success": self.ipv4_done.add(ipv4) + msg = f"seconds remaining {timeout}" + self.log.debug(msg) + msg = f"ipv4_done: {self.ipv4_done}" + self.log.debug(msg) + msg = f"ipv4_todo: {self.ipv4_todo}" + self.log.debug(msg) if self.ipv4_done != self.ipv4_todo: msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 2b29d7acc..b0530dd8f 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -20,6 +20,9 @@ import inspect import logging +# Using only for its failed_result property +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ + ImageUpgradeTaskResult as Result class ImageUpgradeCommon: @@ -43,6 +46,7 @@ def __init__(self, module): self.module = module self.params = module.params + self.properties = {} self.properties["changed"] = False self.properties["diff"] = [] @@ -156,10 +160,26 @@ def failed_result(self): """ return a result for a failed task with no changes """ - result = {} - result["changed"] = False - result["diff"] = [] - return result + result = Result(self.module) + return result.failed_result + + # @property + # def failed_result(self): + # """ + # return a result for a failed task with no changes + # """ + # result = {} + # result["changed"] = False + # result["diff"] = { + # "deleted": [], + # "merged": [], + # "overridden": [], + # "query": [], + # "replaced": [], + # } + # result["failed"] = True + # result["response"] = [] + # return result @property def changed(self): diff --git a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py new file mode 100644 index 000000000..0c41f434c --- /dev/null +++ b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 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 +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.result import \ + Result + + +class ImageUpgradeTaskResult(Result): + """ + Storage for ImageUpgradeTask result + + Override properties in Result() for use with ImageUpgradeTask module. + + Specifically, this class adds the following properties: + + stage: dict() - represents the image stage result + validate: dict() - represents the image validate result + install: dict() - represents the image install result + + stage, validate and install are added to diff["merged"] like so: + + diff["merged"]["stage"] = stage + diff["merged"]["validate"] = validate + diff["merged"]["install"] = install + + Usage: + + """ + + +class ImageUpgradeTaskResult: + def __init__(self, ansible_module): + self.class_name = self.__class__.__name__ + self.ansible_module = ansible_module + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ImageUpgradeTaskResult()") + + self.diff_properties = {} + self.diff_properties["diff_attach_policy"] = "attach_policy" + self.diff_properties["diff_detach_policy"] = "detach_policy" + self.diff_properties["diff_issu_status"] = "issu_status" + self.diff_properties["diff_stage"] = "stage" + self.diff_properties["diff_upgrade"] = "upgrade" + self.diff_properties["diff_validate"] = "validate" + self.response_properties = {} + self.response_properties["response_attach_policy"] = "attach_policy" + self.response_properties["response_detach_policy"] = "detach_policy" + self.response_properties["response_issu_status"] = "issu_status" + self.response_properties["response_stage"] = "stage" + self.response_properties["response_upgrade"] = "upgrade" + self.response_properties["response_validate"] = "validate" + + self._build_properties() + + def _build_properties(self): + """ + Build the properties dict() with default values + """ + self.properties = {} + self.properties["diff_attach_policy"] = [] + self.properties["diff_detach_policy"] = [] + self.properties["diff_issu_status"] = [] + self.properties["diff_stage"] = [] + self.properties["diff_upgrade"] = [] + self.properties["diff_validate"] = [] + + self.properties["response_attach_policy"] = [] + self.properties["response_issu_status"] = [] + self.properties["response_detach_policy"] = [] + self.properties["response_stage"] = [] + self.properties["response_upgrade"] = [] + self.properties["response_validate"] = [] + + def did_anything_change(self): + """ + return True if diffs have been appended to any of the diff lists. + """ + for key in self.diff_properties.keys(): + if len(self.properties[key]) != 0: + return True + return False + + def _verify_is_dict(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "value must be a dict. " + msg += f"got {type(value).__name__} for " + msg += f"value {value}" + self.ansible_module.fail_json(msg, **self.failed_result) + + @property + def failed_result(self): + """ + return a result for a failed task with no changes + """ + result = {} + result["changed"] = False + result["failed"] = True + result["diff"] = {} + result["response"] = {} + for key in self.diff_properties.keys(): + result["diff"][key] = [] + for key in self.response_properties.keys(): + result["response"][key] = [] + return result + + @property + def module_result(self): + """ + return a result that AnsibleModule can use + """ + result = {} + result["changed"] = self.did_anything_change() + result["diff"] = {} + result["response"] = {} + for key, diff_key in self.diff_properties.items(): + result["diff"][diff_key] = self.properties[key] + for key, response_key in self.response_properties.items(): + result["response"][response_key] = self.properties[key] + return result + + # diff properties + @property + def diff_attach_policy(self): + """ + Getter for diff_attach_policy property + + Used for merged state where we attach image policies + to devices. + """ + return self.properties["diff_attach_policy"] + + @diff_attach_policy.setter + def diff_attach_policy(self, value): + """ + Setter for diff_attach_policy property + """ + self._verify_is_dict(value) + self.properties["diff_attach_policy"].append(value) + + @property + def diff_detach_policy(self): + """ + Getter for diff_detach_policy property + + This is used for deleted state where we detach image policies + from devices. + """ + return self.properties["diff_detach_policy"] + + @diff_detach_policy.setter + def diff_detach_policy(self, value): + """ + Setter for diff_detach_policy property + """ + self._verify_is_dict(value) + self.properties["diff_detach_policy"].append(value) + + @property + def diff_issu_status(self): + """ + Getter for diff_issu_status property + + This is used query state diffs of switch issu state + """ + return self.properties["diff_issu_status"] + + @diff_issu_status.setter + def diff_issu_status(self, value): + """ + Setter for diff_issu_status property + """ + self._verify_is_dict(value) + self.properties["diff_issu_status"].append(value) + + @property + def diff_stage(self): + """ + Getter for diff_stage property + """ + return self.properties["diff_stage"] + + @diff_stage.setter + def diff_stage(self, value): + """ + Setter for diff_stage property + """ + self._verify_is_dict(value) + self.properties["diff_stage"].append(value) + + @property + def diff_upgrade(self): + """ + Getter for diff_upgrade property + """ + return self.properties["diff_upgrade"] + + @diff_upgrade.setter + def diff_upgrade(self, value): + """ + Setter for diff_upgrade property + """ + self._verify_is_dict(value) + self.properties["diff_upgrade"].append(value) + + @property + def diff_validate(self): + """ + Getter for diff_validate property + """ + return self.properties["diff_validate"] + + @diff_validate.setter + def diff_validate(self, value): + """ + Setter for diff_validate property + """ + self._verify_is_dict(value) + self.properties["diff_validate"].append(value) + + # response properties + @property + def response_attach_policy(self): + """ + Getter for response_attach_policy property + + Used for merged state where we attach image policies + to devices. + """ + return self.properties["response_attach_policy"] + + @response_attach_policy.setter + def response_attach_policy(self, value): + """ + Setter for response_attach_policy property + """ + self._verify_is_dict(value) + self.properties["response_attach_policy"].append(value) + + @property + def response_detach_policy(self): + """ + Getter for response_detach_policy property + + This is used for deleted state where we detach image policies + from devices. + """ + return self.properties["response_detach_policy"] + + @response_detach_policy.setter + def response_detach_policy(self, value): + """ + Setter for response_detach_policy property + """ + self._verify_is_dict(value) + self.properties["response_detach_policy"].append(value) + + @property + def response_issu_status(self): + """ + Getter for response_issu_status property + + This is used for deleted state where we detach image policies + from devices. + """ + return self.properties["response_issu_status"] + + @response_issu_status.setter + def response_issu_status(self, value): + """ + Setter for response_issu_status property + """ + self._verify_is_dict(value) + self.properties["response_issu_status"].append(value) + + @property + def response_stage(self): + """ + Getter for response_stage property + """ + return self.properties["response_stage"] + + @response_stage.setter + def response_stage(self, value): + """ + Setter for response_stage property + """ + self._verify_is_dict(value) + self.properties["response_stage"].append(value) + + @property + def response_upgrade(self): + """ + Getter for diff_upgrade property + """ + return self.properties["response_upgrade"] + + @response_upgrade.setter + def response_upgrade(self, value): + """ + Setter for response_upgrade property + """ + self._verify_is_dict(value) + self.properties["response_upgrade"].append(value) + + @property + def response_validate(self): + """ + Getter for response_validate property + """ + return self.properties["response_validate"] + + @response_validate.setter + def response_validate(self, value): + """ + Setter for response_validate property + """ + self._verify_is_dict(value) + self.properties["response_validate"].append(value) diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index e17f33001..97eae08e9 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -175,13 +175,20 @@ def commit(self) -> None: ) self.properties["result"] = self._handle_response(self.response, self.verb) + msg = f"payload: {self.payload}" + self.log.debug(msg) + msg = f"response: {self.response}" + self.log.debug(msg) + msg = f"result: {self.result}" + self.log.debug(msg) + if not self.result["success"]: msg = f"{self.class_name}.{self.method_name}: " msg = f"failed: {self.result}. " msg += f"Controller response: {self.response}" self.module.fail_json(msg, **self.failed_result) - self.properties["response_data"] = self.response.get("DATA") + self.properties["response_data"] = self.response self._wait_for_image_validate_to_complete() def _wait_for_current_actions_to_complete(self) -> None: @@ -260,6 +267,12 @@ def _wait_for_image_validate_to_complete(self) -> None: if validated_status == "Success": self.serial_numbers_done.add(serial_number) + msg = f"seconds remaining {timeout}" + self.log.debug(msg) + msg = f"serial_numbers_todo: {serial_numbers_todo}" + self.log.debug(msg) + msg = f"serial_numbers_done: {self.serial_numbers_done}" + self.log.debug(msg) if self.serial_numbers_done != serial_numbers_todo: msg = f"{self.class_name}.{self.method_name}: " diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 505612499..72d741bde 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -414,8 +414,8 @@ ParamsMergeDefaults from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.common.result import \ - Result +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ + ImageUpgradeTaskResult from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ @@ -486,8 +486,8 @@ def __init__(self, module): self.want = [] self.need = [] - self.result = Result(self.module) - self.result.result["changed"] = False + self.result = ImageUpgradeTaskResult(self.module) + self.result.changed = False self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) @@ -975,6 +975,7 @@ def _build_policy_attach_payload(self) -> None: method_name = inspect.stack()[0][3] self.payloads = [] + self.attach_devices = [] self.switch_details.refresh() self.image_policies.refresh() for switch in self.need: @@ -1010,7 +1011,13 @@ def _build_policy_attach_payload(self) -> None: payload["ipAddr"] = self.switch_details.ip_address payload["platform"] = self.switch_details.platform payload["serialNumber"] = self.switch_details.serial_number - # payload["bootstrapMode"] = switch.get('bootstrap_mode') + + attach_device: Dict[str, Any] = {} + attach_device["action"] = "attach" + attach_device["logical_name"] = self.switch_details.logical_name + attach_device["ip_address"] = self.switch_details.ip_address + attach_device["serial_number"] = self.switch_details.serial_number + attach_device["policy"] = self.image_policies.name for key, value in payload.items(): if value is None: @@ -1021,17 +1028,20 @@ def _build_policy_attach_payload(self) -> None: msg += "the controller." self.module.fail_json(msg) - self.payloads.append(payload) + self.payloads.append(copy.deepcopy(payload)) + self.attach_devices.append(copy.deepcopy(attach_device)) def _send_policy_attach_payload(self) -> None: """ - Send the policy attach payload to the controller and - handle the response + Send the policy attach payload to the controller + Handle the response + Set the result and diff Callers: - self.handle_merged_state """ if len(self.payloads) == 0: + # TODO: set a proper result and diff here! return self.path = self.endpoints.policy_attach.get("path") @@ -1043,9 +1053,14 @@ def _send_policy_attach_payload(self) -> None: self.module, self.verb, self.path, data=json.dumps(payload) ) result = self._handle_response(response, self.verb) + self.result.response_attach_policy = copy.deepcopy(response) + for attach_device in self.attach_devices: + msg = f"attach_device: {json.dumps(attach_device, indent=4, sort_keys=True)}" + self.log.debug(msg) + self.result.diff_attach_policy = copy.deepcopy(attach_device) if not result["success"]: - self._failure(response) + self._failure(self.result.response_attach_policy) def _stage_images(self, serial_numbers) -> None: """ @@ -1061,6 +1076,9 @@ def _stage_images(self, serial_numbers) -> None: instance = ImageStage(self.module) instance.serial_numbers = serial_numbers instance.commit() + self.result.diff_stage = instance.response + instance.response.pop("DATA", None) + self.result.response_stage = instance.response def _validate_images(self, serial_numbers) -> None: """ @@ -1075,6 +1093,8 @@ def _validate_images(self, serial_numbers) -> None: instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers instance.commit() + self.result.diff_validate = instance.payload + self.result.response_validate = instance.response def _verify_install_options(self, devices) -> None: """ @@ -1208,9 +1228,9 @@ def _upgrade_images(self, devices) -> None: upgrade.devices = devices upgrade.commit() for diff in upgrade.diff: - self.result.merged = diff + self.result.diff_upgrade = diff for response in upgrade.response: - self.result.response = response + self.result.response_upgrade = response def handle_merged_state(self) -> None: """ @@ -1288,28 +1308,28 @@ def handle_deleted_state(self) -> None: detach_policy_devices[self.image_policies.policy_name].append(device) if len(detach_policy_devices) == 0: + # TODO: Need to build a proper response here. return instance = ImagePolicyAction(self.module) - detach_results = [] for key, value in detach_policy_serial_numbers.items(): - detach_result = {} + detach_diff = {} instance.policy_name = key instance.action = "detach" instance.serial_numbers = value instance.commit() - self.result.response = copy.deepcopy(instance.response) + self.result.response_detach_policy = copy.deepcopy(instance.response) for device in detach_policy_devices[key]: - detach_result["action"] = "detach" - detach_result["ip_address"] = device.get("ip_address") - detach_result["logical_name"] = device.get("logical_name") - detach_result["policy"] = key - detach_result["serial_number"] = device.get("serial_number") - detach_results.append(copy.deepcopy(detach_result)) - for item in detach_results: - msg = f"item: {json.dumps(item, indent=4, sort_keys=True)}" - self.log.debug(msg) - self.result.deleted = item + detach_diff["action"] = "detach" + detach_diff["ip_address"] = device.get("ip_address") + detach_diff["logical_name"] = device.get("logical_name") + detach_diff["policy"] = key + detach_diff["serial_number"] = device.get("serial_number") + + msg = f"item: {json.dumps(detach_diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.result.diff_detach_policy = copy.deepcopy(detach_diff) def handle_query_state(self) -> None: """ @@ -1321,12 +1341,12 @@ def handle_query_state(self) -> None: instance.refresh() response = copy.deepcopy(instance.response) response.pop("DATA") - self.result.response = copy.deepcopy(response) + self.result.response_issu_status = copy.deepcopy(response) for switch in self.need: instance.filter = switch.get("ip_address") if instance.filtered_data is None: continue - self.result.query = instance.filtered_data + self.result.diff_issu_status = instance.filtered_data def _failure(self, resp) -> None: """ @@ -1395,15 +1415,15 @@ def main(): elif ansible_module.params["state"] == "query": task_module.get_need_query() - task_module.result.result["changed"] = False + task_module.result.changed = False if len(task_module.need) == 0: - ansible_module.exit_json(**task_module.result.result) + ansible_module.exit_json(**task_module.result.module_result) if ansible_module.check_mode: - ansible_module.exit_json(**task_module.result.result) + ansible_module.exit_json(**task_module.result.module_result) if ansible_module.params["state"] in ["merged", "deleted"]: - task_module.result.result["changed"] = True + task_module.result.changed = True if ansible_module.params["state"] == "merged": task_module.handle_merged_state() @@ -1412,7 +1432,7 @@ def main(): elif ansible_module.params["state"] == "query": task_module.handle_query_state() - ansible_module.exit_json(**task_module.result.result) + ansible_module.exit_json(**task_module.result.module_result) if __name__ == "__main__": diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index bed3ec72a..30ed3fc88 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -90,16 +90,24 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: assert instance.validated == {} assert instance.want == [] assert instance.need == [] - assert instance.result.result == { + assert instance.result.module_result == { "changed": False, "diff": { - "deleted": [], - "merged": [], - "overridden": [], - "query": [], - "replaced": [], + "attach_policy": [], + "detach_policy": [], + "issu_status": [], + "stage": [], + "upgrade": [], + "validate": [], }, - "response": [] + "response": { + "attach_policy": [], + "detach_policy": [], + "issu_status": [], + "stage": [], + "upgrade": [], + "validate": [], + }, } assert isinstance(instance.switch_details, SwitchDetails) assert isinstance(instance.image_policies, ImagePolicies) From 5e78862d274c758a68c11bf1a474cfece37f6477 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 11 Jan 2024 22:02:51 -1000 Subject: [PATCH 202/300] Fix linter shebang error --- plugins/module_utils/image_mgmt/image_upgrade_task_result.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py index 0c41f434c..075ca9259 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # # Copyright (c) 2024 Cisco and/or its affiliates. # From 677bf19a6c78ed03d2819a57571dfcf515c1a40a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 11 Jan 2024 22:09:35 -1000 Subject: [PATCH 203/300] Fix trailing whitespace linter error --- .../dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 30ed3fc88..88e988ff1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -107,7 +107,7 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: "stage": [], "upgrade": [], "validate": [], - }, + }, } assert isinstance(instance.switch_details, SwitchDetails) assert isinstance(instance.image_policies, ImagePolicies) From aeb787842beb8ed4904760163ab5fdcbdd395e5e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 12 Jan 2024 09:12:01 -1000 Subject: [PATCH 204/300] Improve ImageStage results and logging, more... 1. Sort todo and done sets when logging them so that serial_numbers/ip_addresses line up within the log. 2. Improve ImageStage diff to include the same info as other diffs (ip, serial, logical name, action). 3. TODO: need the same diff mods for ImageValidate...next commit... 4. Remove failed_result propery from ImageUpgradeCommon since we're using ImageUpgradeTaskResult() for that now. --- .../module_utils/image_mgmt/image_stage.py | 14 ++++++++++++-- .../module_utils/image_mgmt/image_upgrade.py | 4 ++-- .../image_mgmt/image_upgrade_common.py | 19 +------------------ .../module_utils/image_mgmt/image_validate.py | 4 ++-- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index ca0cccdc3..86647d34c 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -226,6 +226,16 @@ def commit(self): self.properties["response_data"] = self.response.get("DATA", "No Stage DATA") self._wait_for_image_stage_to_complete() + for serial_number in self.serial_numbers_done: + self.issu_detail.filter = serial_number + self.issu_detail.refresh() + diff = {} + diff["serial_number"] = serial_number + diff["action"] = "stage" + diff["logical_name"] = self.issu_detail.device_name + diff["ip_address"] = self.issu_detail.ip_address + self.diff = copy.deepcopy(diff) + def _wait_for_current_actions_to_complete(self): """ The controller will not stage an image if there are any actions in @@ -298,9 +308,9 @@ def _wait_for_image_stage_to_complete(self): msg = f"seconds remaining {timeout}" self.log.debug(msg) - msg = f"serial_numbers_todo: {serial_numbers_todo}" + msg = f"serial_numbers_todo: {sorted(serial_numbers_todo)}" self.log.debug(msg) - msg = f"serial_numbers_done: {self.serial_numbers_done}" + msg = f"serial_numbers_done: {sorted(self.serial_numbers_done)}" self.log.debug(msg) if self.serial_numbers_done != serial_numbers_todo: diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index dfd3c6c97..18ec2b9c6 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -574,9 +574,9 @@ def _wait_for_image_upgrade_to_complete(self): self.ipv4_done.add(ipv4) msg = f"seconds remaining {timeout}" self.log.debug(msg) - msg = f"ipv4_done: {self.ipv4_done}" + msg = f"ipv4_done: {sorted(self.ipv4_done)}" self.log.debug(msg) - msg = f"ipv4_todo: {self.ipv4_todo}" + msg = f"ipv4_todo: {sorted(self.ipv4_todo)}" self.log.debug(msg) if self.ipv4_done != self.ipv4_todo: diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index b0530dd8f..58a67a7ff 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -20,6 +20,7 @@ import inspect import logging + # Using only for its failed_result property from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ ImageUpgradeTaskResult as Result @@ -163,24 +164,6 @@ def failed_result(self): result = Result(self.module) return result.failed_result - # @property - # def failed_result(self): - # """ - # return a result for a failed task with no changes - # """ - # result = {} - # result["changed"] = False - # result["diff"] = { - # "deleted": [], - # "merged": [], - # "overridden": [], - # "query": [], - # "replaced": [], - # } - # result["failed"] = True - # result["response"] = [] - # return result - @property def changed(self): """ diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 97eae08e9..548b3c46c 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -269,9 +269,9 @@ def _wait_for_image_validate_to_complete(self) -> None: self.serial_numbers_done.add(serial_number) msg = f"seconds remaining {timeout}" self.log.debug(msg) - msg = f"serial_numbers_todo: {serial_numbers_todo}" + msg = f"serial_numbers_todo: {sorted(serial_numbers_todo)}" self.log.debug(msg) - msg = f"serial_numbers_done: {self.serial_numbers_done}" + msg = f"serial_numbers_done: {sorted(self.serial_numbers_done)}" self.log.debug(msg) if self.serial_numbers_done != serial_numbers_todo: From 6d2957c7193380cde851f51796f173db1a441856 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 12 Jan 2024 12:37:45 -1000 Subject: [PATCH 205/300] Consistent diff between ImageStage and ImageValidate 1. Align the diff returned by ImageStage and ImageValidate 2. ImageStage, improve the response when no serial numbers need to be processed. 3. ImageValidate, return a response when no serial numbers need to be processed (similar code to 2, above). 4. ImageValidate, update unit test to reflect different response from ImageValidate in the case where no serial_numbers need to be processed. --- .../module_utils/image_mgmt/image_stage.py | 23 ++++++++------ .../module_utils/image_mgmt/image_validate.py | 30 +++++++++++++++---- plugins/modules/dcnm_image_upgrade.py | 6 ++-- .../test_image_upgrade_image_validate.py | 4 +-- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 86647d34c..8c2f26d5f 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -186,11 +186,11 @@ def commit(self): msg = f"self.serial_numbers: {self.serial_numbers}" self.log.debug(msg) + if len(self.serial_numbers) == 0: - self.properties["response"] = {"response": "No serial numbers to stage."} - self.properties["response_data"] = { - "response": "No serial numbers to stage." - } + msg = "No serial numbers to stage." + self.properties["response"] = {"response": msg} + self.properties["response_data"] = {"response": msg} self.properties["result"] = {"success": True} return @@ -215,25 +215,30 @@ def commit(self): ) self.properties["result"] = self._handle_response(self.response, self.verb) + msg = f"payload: {self.payload}" + self.log.debug(msg) + msg = f"response: {self.response}" + self.log.debug(msg) + msg = f"result: {self.result}" + self.log.debug(msg) + if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " msg = f"failed: {self.result}. " msg += f"Controller response: {self.response}" self.module.fail_json(msg, **self.failed_result) - msg = f"self.response: {self.response}" - self.log.debug(msg) self.properties["response_data"] = self.response.get("DATA", "No Stage DATA") self._wait_for_image_stage_to_complete() for serial_number in self.serial_numbers_done: self.issu_detail.filter = serial_number - self.issu_detail.refresh() diff = {} - diff["serial_number"] = serial_number diff["action"] = "stage" - diff["logical_name"] = self.issu_detail.device_name diff["ip_address"] = self.issu_detail.ip_address + diff["logical_name"] = self.issu_detail.device_name + diff["policy"] = self.issu_detail.policy + diff["serial_number"] = serial_number self.diff = copy.deepcopy(diff) def _wait_for_current_actions_to_complete(self): diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 548b3c46c..2f5eff5b2 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -154,15 +154,25 @@ def commit(self) -> None: Commit the image validation request to the controller and wait for the images to be validated. """ - self.method_name = inspect.stack()[0][3] + method_name = inspect.stack()[0][3] + + msg = "ENTERED commit()" + self.log.debug(msg) if self.serial_numbers is None: - msg = f"{self.class_name}.{self.method_name}: " - msg += "call instance.serial_numbers before " - msg += "calling commit." + msg = f"{self.class_name}.{method_name}: " + msg += "call instance.serial_numbers " + msg += "before calling commit." self.module.fail_json(msg, **self.failed_result) + msg = f"self.serial_numbers: {self.serial_numbers}" + self.log.debug(msg) + if len(self.serial_numbers) == 0: + msg = "No serial numbers to validate." + self.properties["response"] = {"response": msg} + self.properties["response_data"] = {"response": msg} + self.properties["result"] = {"success": True} return self.prune_serial_numbers() @@ -183,7 +193,7 @@ def commit(self) -> None: self.log.debug(msg) if not self.result["success"]: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg = f"failed: {self.result}. " msg += f"Controller response: {self.response}" self.module.fail_json(msg, **self.failed_result) @@ -191,6 +201,16 @@ def commit(self) -> None: self.properties["response_data"] = self.response self._wait_for_image_validate_to_complete() + for serial_number in self.serial_numbers_done: + self.issu_detail.filter = serial_number + diff = {} + diff["action"] = "validate" + diff["ip_address"] = self.issu_detail.ip_address + diff["logical_name"] = self.issu_detail.device_name + diff["policy"] = self.issu_detail.policy + diff["serial_number"] = serial_number + self.diff = copy.deepcopy(diff) + def _wait_for_current_actions_to_complete(self) -> None: """ The controller will not validate an image if there are any actions in diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 72d741bde..33f0cfae0 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1076,7 +1076,8 @@ def _stage_images(self, serial_numbers) -> None: instance = ImageStage(self.module) instance.serial_numbers = serial_numbers instance.commit() - self.result.diff_stage = instance.response + for diff in instance.diff: + self.result.diff_stage = copy.deepcopy(diff) instance.response.pop("DATA", None) self.result.response_stage = instance.response @@ -1093,7 +1094,8 @@ def _validate_images(self, serial_numbers) -> None: instance = ImageValidate(self.module) instance.serial_numbers = serial_numbers instance.commit() - self.result.diff_validate = instance.payload + for diff in instance.diff: + self.result.diff_validate = copy.deepcopy(diff) self.result.response_validate = instance.response def _verify_install_options(self, devices) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 1774118ab..2e8a20224 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -494,8 +494,8 @@ def test_image_mgmt_validate_00022(image_validate) -> None: instance = image_validate instance.serial_numbers = [] instance.commit() - assert instance.response == {} - assert instance.result == {} + assert instance.response == {'response': 'No serial numbers to validate.'} + assert instance.result == {'success': True} def test_image_mgmt_validate_00023(monkeypatch, image_validate) -> None: From cd329e032b9546351e35cf6d7841b3bc23b2202f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 17 Jan 2024 14:18:45 -1000 Subject: [PATCH 206/300] Refactor, more... 1. ImageUpgradeTask: refactor _build_policy_attach_payload(), _self_policy_attach_payload(), and handle_deleted_state() into a single method _attach_or_detach_policy() 2. ImageUpgradeTask: Fix _failure() and remove docstring contents that described the issue. 3. Modify test_image_mgmt_upgrade_task_00001 to remove assert for instance.payloads as this is no longer needed 4. test_image_upgrade_validate.py: run through black and isort 5. ImagePolicyAction() : modify to store list of diffs and leverage this in ImageUpgradeTask() when generating results. 6. ImagePolicyAction().validate_request() - add check for image policy does not exist on the controller. 7. ImagePolicyAction() - Add typing for all dict assignments. --- .../image_mgmt/image_policy_action.py | 102 ++++++--- plugins/modules/dcnm_image_upgrade.py | 203 +++++------------- .../test_image_upgrade_image_upgrade_task.py | 1 - .../test_image_upgrade_image_validate.py | 4 +- 4 files changed, 130 insertions(+), 180 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index deeb41fca..85e2d8772 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -18,9 +18,11 @@ __metaclass__ = type __author__ = "Allen Robel" +import copy import inspect import json import logging +from typing import Any, Dict from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints @@ -69,7 +71,6 @@ def __init__(self, module): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED ImagePolicyAction()") - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.endpoints = ApiEndpoints() self._init_properties() self.image_policies = ImagePolicies(self.module) @@ -96,6 +97,10 @@ def build_payload(self): caller _attach_policy() """ method_name = inspect.stack()[0][3] + + msg = "ENTERED" + self.log.debug(msg) + self.payloads = [] self.switch_issu_details.refresh() @@ -125,6 +130,9 @@ def validate_request(self): """ method_name = inspect.stack()[0][3] + msg = "ENTERED" + self.log.debug(msg) + if self.action is None: msg = f"{self.class_name}.{method_name}: " msg += "instance.action must be set before " @@ -149,10 +157,18 @@ def validate_request(self): self.image_policies.refresh() self.switch_issu_details.refresh() - # Fail if the image policy does not support the switch platform self.image_policies.policy_name = self.policy_name + # Fail if the image policy does not exist. + # Image policy creation is handled by a different module. + if self.image_policies.name is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"policy {self.policy_name} does not exist on " + msg += "the controller" + self.module.fail_json(msg) + for serial_number in self.serial_numbers: self.switch_issu_details.filter = serial_number + # Fail if the image policy does not support the switch platform if self.switch_issu_details.platform not in self.image_policies.platform: msg = f"{self.class_name}.{method_name}: " msg += f"policy {self.policy_name} does not support platform " @@ -170,6 +186,9 @@ def commit(self): """ method_name = inspect.stack()[0][3] + msg = "ENTERED" + self.log.debug(msg) + self.validate_request() if self.action == "attach": self._attach_policy() @@ -186,42 +205,46 @@ def _attach_policy(self): """ Attach policy_name to the switch(es) associated with serial_numbers - NOTES: - 1. This method creates a list of responses and results which - are accessible via properties response and result, - respectively. + This method creates a list of diffs, one result, and one response. + These are accessable via: + self.diff = List[Dict[str, Any]] + self.result = result from the controller + self.response = response from the controller """ method_name = inspect.stack()[0][3] + msg = "ENTERED" + self.log.debug(msg) + self.build_payload() self.path = self.endpoints.policy_attach.get("path") self.verb = self.endpoints.policy_attach.get("verb") - responses = [] - results = [] - - for payload in self.payloads: - response = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(payload) - ) - result = self._handle_response(response, self.verb) + payload: Dict[str, Any] = {} + payload["mappingList"] = self.payloads + response = dcnm_send( + self.module, self.verb, self.path, data=json.dumps(payload) + ) + result = self._handle_response(response, self.verb) + if not result["success"]: msg = f"{self.class_name}.{method_name}: " - msg += f"response: {json.dumps(response, indent=4)}" - self.log.debug(msg) - - if not result["success"]: - msg = f"{self.class_name}.{method_name}: " - msg += f"Bad result when attaching policy {self.policy_name} " - msg += f"to switch {payload['ipAddr']}." - self.module.fail_json(msg, **self.failed_result) + msg += f"Bad result when attaching policy {self.policy_name} " + msg += f"to switch {payload['ipAddr']}." + self.module.fail_json(msg, **self.failed_result) - responses.append(response) - results.append(result) + self.properties["result"] = result + self.properties["response"] = response - self.properties["response"] = responses - self.properties["result"] = results + for payload in self.payloads: + diff = {} + diff["action"] = self.action + diff["ip_address"] = payload["ipAddr"] + diff["logical_name"] = payload["hostName"] + diff["policy_name"] = payload["policyName"] + diff["serial_number"] = payload["serialNumber"] + self.diff = copy.deepcopy(diff) def _detach_policy(self): """ @@ -232,6 +255,9 @@ def _detach_policy(self): """ method_name = inspect.stack()[0][3] + msg = "ENTERED" + self.log.debug(msg) + self.path = self.endpoints.policy_detach.get("path") self.verb = self.endpoints.policy_detach.get("verb") @@ -253,8 +279,16 @@ def _detach_policy(self): msg += f"from the following device(s): {','.join(sorted(self.serial_numbers))}." self.module.fail_json(msg, **self.failed_result) + for serial_number in self.serial_numbers: + self.switch_issu_details.filter = serial_number + diff = {} + diff["action"] = self.action + diff["ip_address"] = self.switch_issu_details.ip_address + diff["logical_name"] = self.switch_issu_details.device_name + diff["policy_name"] = self.policy_name + diff["serial_number"] = serial_number + self.diff = copy.deepcopy(diff) self.changed = True - self.diff = self.response def _query_policy(self): """ @@ -278,6 +312,20 @@ def _query_policy(self): self.module.fail_json(msg, **self.failed_result) self.properties["query_result"] = self.response.get("DATA") + self.diff = self.response + + @property + def diff_null(self): + """ + Convenience property to return a null diff when no action is taken. + """ + diff: Dict[str, Any] = {} + diff["action"] = None + diff["ip_address"] = None + diff["logical_name"] = None + diff["policy"] = None + diff["serial_number"] = None + return diff @property def query_result(self): diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 33f0cfae0..b0130e95a 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -414,8 +414,6 @@ ParamsMergeDefaults from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ - ImageUpgradeTaskResult from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ @@ -428,6 +426,8 @@ ImageUpgrade from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ + ImageUpgradeTaskResult from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ ImageValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ @@ -469,9 +469,6 @@ def __init__(self, module): self.path = None self.verb = None - # populated in self._build_policy_attach_payload() - self.payloads = [] - self.config = module.params.get("config", {}) if not isinstance(self.config, dict): @@ -963,104 +960,65 @@ def _validate_switch_configs(self) -> None: validator.parameters = switch validator.commit() - def _build_policy_attach_payload(self) -> None: + def _attach_or_detach_image_policy(self, action=None) -> None: """ - Build the payload for the policy attach request - Verify that the image policy exists on the controller - Verify that the image policy supports the switch platform + Attach or detach image policies to/from switches + action valid values: attach, detach - Callers: + Caller: - self.handle_merged_state + - self.handle_deleted_state + + NOTES: + - Sanity checking for action is done in ImagePolicyAction """ - method_name = inspect.stack()[0][3] + msg = f"ENTERED: action: {action}" + self.log.debug(msg) - self.payloads = [] - self.attach_devices = [] + serial_numbers_to_update: Dict[str, Any] = {} self.switch_details.refresh() self.image_policies.refresh() - for switch in self.need: - if switch.get("policy_changed") is False: - continue + for switch in self.need: self.switch_details.ip_address = switch.get("ip_address") self.image_policies.policy_name = switch.get("policy") + # ImagePolicyAction wants a policy name and a list of serial_number + # Build dictionary, serial_numbers_to_udate, keyed on policy name + # whose value is the list of serial numbers to attach/detach. + if self.image_policies.name not in serial_numbers_to_update: + serial_numbers_to_update[self.image_policies.policy_name] = [] - # Fail if the image policy does not exist. - # Image policy creation is handled by a different module. - if self.image_policies.name is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"policy {switch.get('policy')} does not exist on " - msg += "the controller" - self.module.fail_json(msg) - - # Fail if the image policy does not support the switch platform - if self.switch_details.platform not in self.image_policies.platform: - msg = f"{self.class_name}.{method_name}: " - msg += f"policy {switch.get('policy')} does not support " - msg += f"platform {self.switch_details.platform}. " - msg += f"Policy {switch.get('policy')} " - msg += "supports the following platform(s): " - msg += f"{self.image_policies.platform}" - self.module.fail_json(msg) + serial_numbers_to_update[self.image_policies.policy_name].append( + self.switch_details.serial_number + ) - payload: Dict[str, Any] = {} - payload["policyName"] = self.image_policies.name - # switch_details.host_name is always None in 12.1.2e - # so we're using logical_name instead - payload["hostName"] = self.switch_details.logical_name - payload["ipAddr"] = self.switch_details.ip_address - payload["platform"] = self.switch_details.platform - payload["serialNumber"] = self.switch_details.serial_number - - attach_device: Dict[str, Any] = {} - attach_device["action"] = "attach" - attach_device["logical_name"] = self.switch_details.logical_name - attach_device["ip_address"] = self.switch_details.ip_address - attach_device["serial_number"] = self.switch_details.serial_number - attach_device["policy"] = self.image_policies.name - - for key, value in payload.items(): - if value is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"Unable to determine {key} for switch " - msg += f"{switch.get('ip_address')}. " - msg += "Please verify that the switch is managed by " - msg += "the controller." - self.module.fail_json(msg) - - self.payloads.append(copy.deepcopy(payload)) - self.attach_devices.append(copy.deepcopy(attach_device)) - - def _send_policy_attach_payload(self) -> None: - """ - Send the policy attach payload to the controller - Handle the response - Set the result and diff + instance = ImagePolicyAction(self.module) + if len(serial_numbers_to_update) == 0: + msg = f"No policies to {action}" + self.log.debug(msg) - Callers: - - self.handle_merged_state - """ - if len(self.payloads) == 0: - # TODO: set a proper result and diff here! + if action == "attach": + self.result.diff_attach_policy = instance.diff_null + elif action == "detach": + self.result.diff_detach_policy = instance.diff_null return - self.path = self.endpoints.policy_attach.get("path") - self.verb = self.endpoints.policy_attach.get("verb") + for key, value in serial_numbers_to_update.items(): + instance.policy_name = key + instance.action = action + instance.serial_numbers = value + instance.commit() + self.result.response_attach_policy = copy.deepcopy(instance.response) - payload: Dict[str, Any] = {} - payload["mappingList"] = self.payloads - response = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(payload) - ) - result = self._handle_response(response, self.verb) - self.result.response_attach_policy = copy.deepcopy(response) - for attach_device in self.attach_devices: - msg = f"attach_device: {json.dumps(attach_device, indent=4, sort_keys=True)}" + for diff in instance.diff: + msg = ( + f"{instance.action} diff: {json.dumps(diff, indent=4, sort_keys=True)}" + ) self.log.debug(msg) - self.result.diff_attach_policy = copy.deepcopy(attach_device) - - if not result["success"]: - self._failure(self.result.response_attach_policy) + if action == "attach": + self.result.diff_attach_policy = copy.deepcopy(diff) + elif action == "detach": + self.result.diff_detach_policy = copy.deepcopy(diff) def _stage_images(self, serial_numbers) -> None: """ @@ -1096,6 +1054,7 @@ def _validate_images(self, serial_numbers) -> None: instance.commit() for diff in instance.diff: self.result.diff_validate = copy.deepcopy(diff) + instance.response.pop("DATA", None) self.result.response_validate = instance.response def _verify_install_options(self, devices) -> None: @@ -1243,8 +1202,10 @@ def handle_merged_state(self) -> None: Caller: main() """ - self._build_policy_attach_payload() - self._send_policy_attach_payload() + msg = "ENTERED" + self.log.debug(msg) + + self._attach_or_detach_image_policy(action="attach") stage_devices: List[str] = [] validate_devices: List[str] = [] @@ -1285,53 +1246,10 @@ def handle_deleted_state(self) -> None: Caller: main() """ - detach_policy_devices: Dict[str, Any] = {} - detach_policy_serial_numbers: Dict[str, Any] = {} - self.switch_details.refresh() - self.image_policies.refresh() - - for switch in self.need: - self.switch_details.ip_address = switch.get("ip_address") - self.image_policies.policy_name = switch.get("policy") - device = {} - device["serial_number"] = self.switch_details.serial_number - device["logical_name"] = self.switch_details.logical_name - device["ip_address"] = self.switch_details.ip_address - device["policy"] = self.image_policies.policy_name - - if self.image_policies.name not in detach_policy_devices: - detach_policy_devices[self.image_policies.policy_name] = [] - if self.image_policies.name not in detach_policy_serial_numbers: - detach_policy_serial_numbers[self.image_policies.policy_name] = [] - - detach_policy_serial_numbers[self.image_policies.policy_name].append( - self.switch_details.serial_number - ) - detach_policy_devices[self.image_policies.policy_name].append(device) - - if len(detach_policy_devices) == 0: - # TODO: Need to build a proper response here. - return - - instance = ImagePolicyAction(self.module) - for key, value in detach_policy_serial_numbers.items(): - detach_diff = {} - instance.policy_name = key - instance.action = "detach" - instance.serial_numbers = value - instance.commit() - self.result.response_detach_policy = copy.deepcopy(instance.response) - for device in detach_policy_devices[key]: - detach_diff["action"] = "detach" - detach_diff["ip_address"] = device.get("ip_address") - detach_diff["logical_name"] = device.get("logical_name") - detach_diff["policy"] = key - detach_diff["serial_number"] = device.get("serial_number") - - msg = f"item: {json.dumps(detach_diff, indent=4, sort_keys=True)}" - self.log.debug(msg) + msg = "ENTERED" + self.log.debug(msg) - self.result.diff_detach_policy = copy.deepcopy(detach_diff) + self._attach_or_detach_image_policy("detach") def handle_query_state(self) -> None: """ @@ -1353,25 +1271,10 @@ def handle_query_state(self) -> None: def _failure(self, resp) -> None: """ Caller: self.attach_policies() - - This came from dcnm_inventory.py, but doesn't seem to be correct - for the case where resp["DATA"] does not exist? - - If resp["DATA"] does not exist, the contents of the - if block don't seem to actually do anything: - - data will be None - - Hence, data.get("stackTrace") will also be None - - Hence, data.update() and res.update() are never executed - - So, the only two lines that will actually ever be executed are - the happy path: - - res = copy.deepcopy(resp) - self.module.fail_json(msg=res) """ res = copy.deepcopy(resp) - if not resp.get("DATA"): + if resp.get("DATA"): data = copy.deepcopy(resp.get("DATA")) if data.get("stackTrace"): data.update( diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index 88e988ff1..c9eb6096d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -84,7 +84,6 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: assert instance.switch_configs == [] assert instance.path is None assert instance.verb is None - assert instance.payloads == [] assert instance.config == {"switches": [{"ip_address": "172.22.150.105"}]} assert instance.check_mode is False assert instance.validated == {} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 2e8a20224..54f80d2e2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -494,8 +494,8 @@ def test_image_mgmt_validate_00022(image_validate) -> None: instance = image_validate instance.serial_numbers = [] instance.commit() - assert instance.response == {'response': 'No serial numbers to validate.'} - assert instance.result == {'success': True} + assert instance.response == {"response": "No serial numbers to validate."} + assert instance.result == {"success": True} def test_image_mgmt_validate_00023(monkeypatch, image_validate) -> None: From 71cd2fc6fb6b5b31a38d7dee5550521c60ac9b08 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 17 Jan 2024 15:56:21 -1000 Subject: [PATCH 207/300] Add type defininitions for dicts, more... 1. ImagePolicyAction: Add type definitions for dicts 2. ImageStage, ImageValidate, ImageUpgrade: Add clarifying comments --- plugins/module_utils/image_mgmt/image_policy_action.py | 6 +++--- plugins/module_utils/image_mgmt/image_stage.py | 1 + plugins/module_utils/image_mgmt/image_upgrade.py | 1 + plugins/module_utils/image_mgmt/image_validate.py | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 85e2d8772..3564a12ee 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -106,7 +106,7 @@ def build_payload(self): self.switch_issu_details.refresh() for serial_number in self.serial_numbers: self.switch_issu_details.filter = serial_number - payload = {} + payload: Dict[str, Any] = {} payload["policyName"] = self.policy_name payload["hostName"] = self.switch_issu_details.device_name payload["ipAddr"] = self.switch_issu_details.ip_address @@ -238,7 +238,7 @@ def _attach_policy(self): self.properties["response"] = response for payload in self.payloads: - diff = {} + diff: Dict[str, Any] = {} diff["action"] = self.action diff["ip_address"] = payload["ipAddr"] diff["logical_name"] = payload["hostName"] @@ -281,7 +281,7 @@ def _detach_policy(self): for serial_number in self.serial_numbers: self.switch_issu_details.filter = serial_number - diff = {} + diff:Dict[str, Any] = {} diff["action"] = self.action diff["ip_address"] = self.switch_issu_details.ip_address diff["logical_name"] = self.switch_issu_details.device_name diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 8c2f26d5f..6bff94b9d 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -239,6 +239,7 @@ def commit(self): diff["logical_name"] = self.issu_detail.device_name diff["policy"] = self.issu_detail.policy diff["serial_number"] = serial_number + # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(diff) def _wait_for_current_actions_to_complete(self): diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 18ec2b9c6..c0f5c4456 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -488,6 +488,7 @@ def commit(self) -> None: self.response = copy.deepcopy(response) self.response_data = response_data + # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(self.payload) self._wait_for_image_upgrade_to_complete() diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 2f5eff5b2..0f2f3bfcb 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -209,6 +209,7 @@ def commit(self) -> None: diff["logical_name"] = self.issu_detail.device_name diff["policy"] = self.issu_detail.policy diff["serial_number"] = serial_number + # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(diff) def _wait_for_current_actions_to_complete(self) -> None: From 5cc8f6819ca6b0f0a76af0ec4b7a3a91a9d985a7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 17 Jan 2024 16:55:04 -1000 Subject: [PATCH 208/300] Fix linter pycodestyle linter error --- plugins/module_utils/image_mgmt/image_policy_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 3564a12ee..87616b0f9 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -281,7 +281,7 @@ def _detach_policy(self): for serial_number in self.serial_numbers: self.switch_issu_details.filter = serial_number - diff:Dict[str, Any] = {} + diff: Dict[str, Any] = {} diff["action"] = self.action diff["ip_address"] = self.switch_issu_details.ip_address diff["logical_name"] = self.switch_issu_details.device_name From 46de1a07100b1effc9c1ffb12a2b142fc3901f5f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 18 Jan 2024 16:58:50 -1000 Subject: [PATCH 209/300] Initial integration test for "merged" state --- .../dcnm_image_upgrade/defaults/main.yaml | 2 + .../targets/dcnm_image_upgrade/meta/main.yaml | 1 + .../dcnm_image_upgrade/tasks/dcnm.yaml | 20 ++ .../dcnm_image_upgrade/tasks/main.yaml | 2 + .../dcnm_image_upgrade/tests/merged.yaml | 293 ++++++++++++++++++ 5 files changed, 318 insertions(+) create mode 100644 tests/integration/targets/dcnm_image_upgrade/defaults/main.yaml create mode 100644 tests/integration/targets/dcnm_image_upgrade/meta/main.yaml create mode 100644 tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml create mode 100644 tests/integration/targets/dcnm_image_upgrade/tasks/main.yaml create mode 100644 tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml diff --git a/tests/integration/targets/dcnm_image_upgrade/defaults/main.yaml b/tests/integration/targets/dcnm_image_upgrade/defaults/main.yaml new file mode 100644 index 000000000..55a93fc23 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upgrade/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_image_upgrade/meta/main.yaml b/tests/integration/targets/dcnm_image_upgrade/meta/main.yaml new file mode 100644 index 000000000..32cf5dda7 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upgrade/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml b/tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml new file mode 100644 index 000000000..0f3410d0f --- /dev/null +++ b/tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml @@ -0,0 +1,20 @@ +--- +- name: collect dcnm test cases + find: + paths: "{{ role_path }}/tests" + 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_upgrade/tasks/main.yaml b/tests/integration/targets/dcnm_image_upgrade/tasks/main.yaml new file mode 100644 index 000000000..78c5fb834 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upgrade/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_upgrade/tests/merged.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml new file mode 100644 index 000000000..a9fc2b850 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml @@ -0,0 +1,293 @@ +################################################################################ +# RUNTIME +################################################################################ + +# About 1.5 hours + +################################################################################ +# STEPS +################################################################################ + +# 1. Create a fabric +# 2. Merge switches into fabric +# 3. Upgrade switches using global config +# 4. Upgrade switches using switch config, overriding policy in global config + +################################################################################ +# REQUIREMENTS +################################################################################ + +# 1. image policies are already configured on the controller: +# - KR5M (Kerry release maintenance 5) +# - NR3F (Niles release maintenance 3) +# The above include both NX-OS and EPLD images. +# +# TODO: Once dcnm_image_policy module is accepted, use that to +# configure the above policies. +# +# Example vars for dcnm_image_upgrade integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: merged +# fabric_name: f1 +# username: admin +# password: "foobar" +# switch_username: admin +# switch_password: "foobar" +# spine1: 172.22.150.114 +# spine2: 172.22.150.115 +# leaf1: 172.22.150.106 +# leaf2: 172.22.150.107 +# leaf3: 172.22.150.108 +# leaf4: 172.22.150.109 +# # for dcnm_image_upgrade role +# test_fabric: "{{ fabric_name }}" +# ansible_switch1: "{{ leaf1 }}" +# ansible_switch2: "{{ leaf2 }}" +# ansible_switch3: "{{ spine1 }}" +# image_policy_1: "KR5M" +# image_policy_2: "NR3F" + +################################################################################ +# SETUP +################################################################################ + +- set_fact: + rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" + +- name: MERGED - Verify if fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_fabric_create }}" + register: result + +- debug: + var: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: MERGED - setup - Clean up any existing devices + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted + +- name: MERGED - Merge switches + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: merged + config: + - seed_ip: "{{ ansible_switch1 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch2 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch3 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: spine + preserve_config: False + register: result + +- assert: + that: + - 'result.changed == true' + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + +################################################################################ +# MERGED global_config +################################################################################ + +- name: MERGED - Upgrade all switches using global config. + cisco.dcnm.dcnm_image_upgrade: &global_config + state: merged + config: + policy: "{{ image_policy_1}}" + reboot: false + stage: true + validate: true + upgrade: + nxos: true + epld: false + options: + nxos: + mode: disruptive + bios_force: false + epld: + module: ALL + golden: false + reboot: + config_reload: false + write_erase: false + package: + install: false + uninstall: false + switches: + - ip_address: "{{ leaf1 }}" + - ip_address: "{{ leaf2 }}" + - ip_address: "{{ spine1 }}" + register: result + +- debug: + var: result + +- assert: + that: + - 'result.changed == true' + - 'result.failed == false' + - 'result.diff.attach_policy[0].policy_name == "{{ image_policy_1 }}"' + - 'result.diff.attach_policy[1].policy_name == "{{ image_policy_1 }}"' + - 'result.diff.attach_policy[2].policy_name == "{{ image_policy_1 }}"' + - 'result.diff.upgrade[0].devices[0].policyName == "{{ image_policy_1 }}"' + - 'result.diff.upgrade[1].devices[0].policyName == "{{ image_policy_1 }}"' + - 'result.diff.upgrade[2].devices[0].policyName == "{{ image_policy_1 }}"' + - 'result.diff.validate[0].policy == "{{ image_policy_1 }}"' + - 'result.diff.validate[1].policy == "{{ image_policy_1 }}"' + - 'result.diff.validate[2].policy == "{{ image_policy_1 }}"' + - 'result.response.attach_policy[0].RETURN_CODE == 200' + - 'result.response.stage[0].RETURN_CODE == 200' + - 'result.response.upgrade[0].RETURN_CODE == 200' + - 'result.response.upgrade[1].RETURN_CODE == 200' + - 'result.response.upgrade[2].RETURN_CODE == 200' + - 'result.response.validate[0].RETURN_CODE == 200' + +################################################################################ +# MERGED IDEMPOTENCE global_config +################################################################################ + +- name: MERGED - global_config - Idempotence PAUSE 15 minutes + ansible.builtin.pause: + minutes: 15 + +- name: MERGED - global_config - Idempotence + cisco.dcnm.dcnm_image_upgrade: *global_config + register: result + +- debug: + var: result + +- assert: + that: + - 'result.changed == false' + - 'result.failed == false' + - '(result.diff.attach_policy | length) == 0' + - '(result.diff.stage | length) == 0' + - '(result.diff.upgrade | length) == 0' + - '(result.diff.validate | length) == 0' + - '(result.response.attach_policy | length) == 0' + - '(result.response.stage | length) == 0' + - '(result.response.upgrade | length) == 0' + - '(result.response.validate | length) == 0' + + +################################################################################ +# MERGED switch_config +################################################################################ + +- name: MERGED - Upgrade all switches using global config. Override policy in switch configs. + cisco.dcnm.dcnm_image_upgrade: &switch_config + state: merged + config: + policy: "{{ image_policy_1}}" + reboot: false + stage: true + validate: true + upgrade: + nxos: true + epld: false + options: + nxos: + mode: disruptive + bios_force: false + epld: + module: ALL + golden: false + reboot: + config_reload: false + write_erase: false + package: + install: false + uninstall: false + switches: + - ip_address: "{{ leaf1 }}" + policy: "{{ image_policy_2 }}" + - ip_address: "{{ leaf2 }}" + policy: "{{ image_policy_2 }}" + - ip_address: "{{ spine1 }}" + policy: "{{ image_policy_2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - 'result.changed == true' + - 'result.failed == false' + - 'result.diff.attach_policy[0].policy_name == "{{ image_policy_2 }}"' + - 'result.diff.attach_policy[1].policy_name == "{{ image_policy_2 }}"' + - 'result.diff.attach_policy[2].policy_name == "{{ image_policy_2 }}"' + - 'result.diff.upgrade[0].devices[0].policyName == "{{ image_policy_2 }}"' + - 'result.diff.upgrade[1].devices[0].policyName == "{{ image_policy_2 }}"' + - 'result.diff.upgrade[2].devices[0].policyName == "{{ image_policy_2 }}"' + - 'result.diff.validate[0].policy == "{{ image_policy_2 }}"' + - 'result.diff.validate[1].policy == "{{ image_policy_2 }}"' + - 'result.diff.validate[2].policy == "{{ image_policy_2 }}"' + - 'result.response.attach_policy[0].RETURN_CODE == 200' + - 'result.response.stage[0].RETURN_CODE == 200' + - 'result.response.upgrade[0].RETURN_CODE == 200' + - 'result.response.upgrade[1].RETURN_CODE == 200' + - 'result.response.upgrade[2].RETURN_CODE == 200' + - 'result.response.validate[0].RETURN_CODE == 200' + +################################################################################ +# MERGED IDEMPOTENCE switch_config +################################################################################ + +- name: MERGED - switch_config - Idempotence PAUSE 15 minutes + ansible.builtin.pause: + minutes: 15 + +- name: MERGED - switch_config - Idempotence + cisco.dcnm.dcnm_image_upgrade: *switch_config + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.failed == false' + - '(result.diff.attach_policy | length) == 0' + - '(result.diff.stage | length) == 0' + - '(result.diff.upgrade | length) == 0' + - '(result.diff.validate | length) == 0' + - '(result.response.attach_policy | length) == 0' + - '(result.response.stage | length) == 0' + - '(result.response.upgrade | length) == 0' + - '(result.response.validate | length) == 0' + +################################################################################ +# CLEAN-UP +################################################################################ + +- name: MERGED - cleanup - Clean up any existing devices + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted \ No newline at end of file From 21866a47906c0e17c9f5c3a1f8ff4a6a36df08b7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 18 Jan 2024 21:54:19 -1000 Subject: [PATCH 210/300] Appease YAML linter --- .../dcnm_image_upgrade/tests/merged.yaml | 400 +++++++++--------- 1 file changed, 200 insertions(+), 200 deletions(-) diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml index a9fc2b850..4a2b1bb00 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml @@ -54,54 +54,54 @@ # SETUP ################################################################################ -- set_fact: - rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" - -- name: MERGED - Verify if fabric is deployed. - cisco.dcnm.dcnm_rest: - method: GET - path: "{{ rest_fabric_create }}" - register: result - -- debug: - var: result - -- assert: - that: - - 'result.response.DATA != None' - -- name: MERGED - setup - Clean up any existing devices - cisco.dcnm.dcnm_inventory: - fabric: "{{ fabric_name }}" - state: deleted - -- name: MERGED - Merge switches - cisco.dcnm.dcnm_inventory: - fabric: "{{ fabric_name }}" - state: merged - config: - - seed_ip: "{{ ansible_switch1 }}" - auth_proto: MD5 - user_name: "{{ switch_username }}" - password: "{{ switch_password }}" - max_hops: 0 - role: leaf - preserve_config: False - - seed_ip: "{{ ansible_switch2 }}" - auth_proto: MD5 - user_name: "{{ switch_username }}" - password: "{{ switch_password }}" - max_hops: 0 - role: leaf - preserve_config: False - - seed_ip: "{{ ansible_switch3 }}" - auth_proto: MD5 - user_name: "{{ switch_username }}" - password: "{{ switch_password }}" - max_hops: 0 - role: spine - preserve_config: False - register: result +- set_fact: + rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" + +- name: MERGED - Verify if fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_fabric_create }}" + register: result + +- debug: + var: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: MERGED - setup - Clean up any existing devices + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted + +- name: MERGED - Merge switches + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: merged + config: + - seed_ip: "{{ ansible_switch1 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch2 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch3 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: spine + preserve_config: False + register: result - assert: that: @@ -109,185 +109,185 @@ - assert: that: - - 'item["RETURN_CODE"] == 200' + - 'item["RETURN_CODE"] == 200' loop: '{{ result.response }}' ################################################################################ # MERGED global_config ################################################################################ -- name: MERGED - Upgrade all switches using global config. - cisco.dcnm.dcnm_image_upgrade: &global_config - state: merged - config: - policy: "{{ image_policy_1}}" - reboot: false - stage: true - validate: true - upgrade: - nxos: true - epld: false - options: - nxos: - mode: disruptive - bios_force: false - epld: - module: ALL - golden: false - reboot: - config_reload: false - write_erase: false - package: - install: false - uninstall: false - switches: - - ip_address: "{{ leaf1 }}" - - ip_address: "{{ leaf2 }}" - - ip_address: "{{ spine1 }}" - register: result - -- debug: - var: result - -- assert: - that: - - 'result.changed == true' - - 'result.failed == false' - - 'result.diff.attach_policy[0].policy_name == "{{ image_policy_1 }}"' - - 'result.diff.attach_policy[1].policy_name == "{{ image_policy_1 }}"' - - 'result.diff.attach_policy[2].policy_name == "{{ image_policy_1 }}"' - - 'result.diff.upgrade[0].devices[0].policyName == "{{ image_policy_1 }}"' - - 'result.diff.upgrade[1].devices[0].policyName == "{{ image_policy_1 }}"' - - 'result.diff.upgrade[2].devices[0].policyName == "{{ image_policy_1 }}"' - - 'result.diff.validate[0].policy == "{{ image_policy_1 }}"' - - 'result.diff.validate[1].policy == "{{ image_policy_1 }}"' - - 'result.diff.validate[2].policy == "{{ image_policy_1 }}"' - - 'result.response.attach_policy[0].RETURN_CODE == 200' - - 'result.response.stage[0].RETURN_CODE == 200' - - 'result.response.upgrade[0].RETURN_CODE == 200' - - 'result.response.upgrade[1].RETURN_CODE == 200' - - 'result.response.upgrade[2].RETURN_CODE == 200' - - 'result.response.validate[0].RETURN_CODE == 200' +- name: MERGED - Upgrade all switches using global config + cisco.dcnm.dcnm_image_upgrade: &global_config + state: merged + config: + policy: "{{ image_policy_1}}" + reboot: false + stage: true + validate: true + upgrade: + nxos: true + epld: false + options: + nxos: + mode: disruptive + bios_force: false + epld: + module: ALL + golden: false + reboot: + config_reload: false + write_erase: false + package: + install: false + uninstall: false + switches: + - ip_address: "{{ leaf1 }}" + - ip_address: "{{ leaf2 }}" + - ip_address: "{{ spine1 }}" + register: result + +- debug: + var: result + +- assert: + that: + - 'result.changed == true' + - 'result.failed == false' + - 'result.diff.attach_policy[0].policy_name == "{{ image_policy_1 }}"' + - 'result.diff.attach_policy[1].policy_name == "{{ image_policy_1 }}"' + - 'result.diff.attach_policy[2].policy_name == "{{ image_policy_1 }}"' + - 'result.diff.upgrade[0].devices[0].policyName == "{{ image_policy_1 }}"' + - 'result.diff.upgrade[1].devices[0].policyName == "{{ image_policy_1 }}"' + - 'result.diff.upgrade[2].devices[0].policyName == "{{ image_policy_1 }}"' + - 'result.diff.validate[0].policy == "{{ image_policy_1 }}"' + - 'result.diff.validate[1].policy == "{{ image_policy_1 }}"' + - 'result.diff.validate[2].policy == "{{ image_policy_1 }}"' + - 'result.response.attach_policy[0].RETURN_CODE == 200' + - 'result.response.stage[0].RETURN_CODE == 200' + - 'result.response.upgrade[0].RETURN_CODE == 200' + - 'result.response.upgrade[1].RETURN_CODE == 200' + - 'result.response.upgrade[2].RETURN_CODE == 200' + - 'result.response.validate[0].RETURN_CODE == 200' ################################################################################ # MERGED IDEMPOTENCE global_config ################################################################################ -- name: MERGED - global_config - Idempotence PAUSE 15 minutes - ansible.builtin.pause: - minutes: 15 +- name: MERGED - global_config - Idempotence PAUSE 15 minutes + ansible.builtin.pause: + minutes: 15 -- name: MERGED - global_config - Idempotence - cisco.dcnm.dcnm_image_upgrade: *global_config - register: result +- name: MERGED - global_config - Idempotence + cisco.dcnm.dcnm_image_upgrade: *global_config + register: result -- debug: - var: result +- debug: + var: result -- assert: - that: - - 'result.changed == false' - - 'result.failed == false' - - '(result.diff.attach_policy | length) == 0' - - '(result.diff.stage | length) == 0' - - '(result.diff.upgrade | length) == 0' - - '(result.diff.validate | length) == 0' - - '(result.response.attach_policy | length) == 0' - - '(result.response.stage | length) == 0' - - '(result.response.upgrade | length) == 0' - - '(result.response.validate | length) == 0' +- assert: + that: + - 'result.changed == false' + - 'result.failed == false' + - '(result.diff.attach_policy | length) == 0' + - '(result.diff.stage | length) == 0' + - '(result.diff.upgrade | length) == 0' + - '(result.diff.validate | length) == 0' + - '(result.response.attach_policy | length) == 0' + - '(result.response.stage | length) == 0' + - '(result.response.upgrade | length) == 0' + - '(result.response.validate | length) == 0' ################################################################################ # MERGED switch_config ################################################################################ -- name: MERGED - Upgrade all switches using global config. Override policy in switch configs. - cisco.dcnm.dcnm_image_upgrade: &switch_config - state: merged - config: - policy: "{{ image_policy_1}}" - reboot: false - stage: true - validate: true - upgrade: - nxos: true - epld: false - options: - nxos: - mode: disruptive - bios_force: false - epld: - module: ALL - golden: false - reboot: - config_reload: false - write_erase: false - package: - install: false - uninstall: false - switches: - - ip_address: "{{ leaf1 }}" - policy: "{{ image_policy_2 }}" - - ip_address: "{{ leaf2 }}" - policy: "{{ image_policy_2 }}" - - ip_address: "{{ spine1 }}" - policy: "{{ image_policy_2 }}" - register: result - -- debug: - var: result - -- assert: - that: - - 'result.changed == true' - - 'result.failed == false' - - 'result.diff.attach_policy[0].policy_name == "{{ image_policy_2 }}"' - - 'result.diff.attach_policy[1].policy_name == "{{ image_policy_2 }}"' - - 'result.diff.attach_policy[2].policy_name == "{{ image_policy_2 }}"' - - 'result.diff.upgrade[0].devices[0].policyName == "{{ image_policy_2 }}"' - - 'result.diff.upgrade[1].devices[0].policyName == "{{ image_policy_2 }}"' - - 'result.diff.upgrade[2].devices[0].policyName == "{{ image_policy_2 }}"' - - 'result.diff.validate[0].policy == "{{ image_policy_2 }}"' - - 'result.diff.validate[1].policy == "{{ image_policy_2 }}"' - - 'result.diff.validate[2].policy == "{{ image_policy_2 }}"' - - 'result.response.attach_policy[0].RETURN_CODE == 200' - - 'result.response.stage[0].RETURN_CODE == 200' - - 'result.response.upgrade[0].RETURN_CODE == 200' - - 'result.response.upgrade[1].RETURN_CODE == 200' - - 'result.response.upgrade[2].RETURN_CODE == 200' - - 'result.response.validate[0].RETURN_CODE == 200' +- name: MERGED - Upgrade all switches using global config. Override policy in switch configs. + cisco.dcnm.dcnm_image_upgrade: &switch_config + state: merged + config: + policy: "{{ image_policy_1}}" + reboot: false + stage: true + validate: true + upgrade: + nxos: true + epld: false + options: + nxos: + mode: disruptive + bios_force: false + epld: + module: ALL + golden: false + reboot: + config_reload: false + write_erase: false + package: + install: false + uninstall: false + switches: + - ip_address: "{{ leaf1 }}" + policy: "{{ image_policy_2 }}" + - ip_address: "{{ leaf2 }}" + policy: "{{ image_policy_2 }}" + - ip_address: "{{ spine1 }}" + policy: "{{ image_policy_2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - 'result.changed == true' + - 'result.failed == false' + - 'result.diff.attach_policy[0].policy_name == "{{ image_policy_2 }}"' + - 'result.diff.attach_policy[1].policy_name == "{{ image_policy_2 }}"' + - 'result.diff.attach_policy[2].policy_name == "{{ image_policy_2 }}"' + - 'result.diff.upgrade[0].devices[0].policyName == "{{ image_policy_2 }}"' + - 'result.diff.upgrade[1].devices[0].policyName == "{{ image_policy_2 }}"' + - 'result.diff.upgrade[2].devices[0].policyName == "{{ image_policy_2 }}"' + - 'result.diff.validate[0].policy == "{{ image_policy_2 }}"' + - 'result.diff.validate[1].policy == "{{ image_policy_2 }}"' + - 'result.diff.validate[2].policy == "{{ image_policy_2 }}"' + - 'result.response.attach_policy[0].RETURN_CODE == 200' + - 'result.response.stage[0].RETURN_CODE == 200' + - 'result.response.upgrade[0].RETURN_CODE == 200' + - 'result.response.upgrade[1].RETURN_CODE == 200' + - 'result.response.upgrade[2].RETURN_CODE == 200' + - 'result.response.validate[0].RETURN_CODE == 200' ################################################################################ # MERGED IDEMPOTENCE switch_config ################################################################################ -- name: MERGED - switch_config - Idempotence PAUSE 15 minutes - ansible.builtin.pause: - minutes: 15 - -- name: MERGED - switch_config - Idempotence - cisco.dcnm.dcnm_image_upgrade: *switch_config - register: result - -- assert: - that: - - 'result.changed == false' - - 'result.failed == false' - - '(result.diff.attach_policy | length) == 0' - - '(result.diff.stage | length) == 0' - - '(result.diff.upgrade | length) == 0' - - '(result.diff.validate | length) == 0' - - '(result.response.attach_policy | length) == 0' - - '(result.response.stage | length) == 0' - - '(result.response.upgrade | length) == 0' - - '(result.response.validate | length) == 0' +- name: MERGED - switch_config - Idempotence PAUSE 15 minutes + ansible.builtin.pause: + minutes: 15 + +- name: MERGED - switch_config - Idempotence + cisco.dcnm.dcnm_image_upgrade: *switch_config + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.failed == false' + - '(result.diff.attach_policy | length) == 0' + - '(result.diff.stage | length) == 0' + - '(result.diff.upgrade | length) == 0' + - '(result.diff.validate | length) == 0' + - '(result.response.attach_policy | length) == 0' + - '(result.response.stage | length) == 0' + - '(result.response.upgrade | length) == 0' + - '(result.response.validate | length) == 0' ################################################################################ # CLEAN-UP ################################################################################ -- name: MERGED - cleanup - Clean up any existing devices - cisco.dcnm.dcnm_inventory: - fabric: "{{ fabric_name }}" - state: deleted \ No newline at end of file +- name: MERGED - cleanup - Clean up any existing devices + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted \ No newline at end of file From 743a1b558baefc30b7db38a812cf7c728ae0ad83 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 19 Jan 2024 13:02:22 -1000 Subject: [PATCH 211/300] IT: Fix jinja2 warning, add "deleted" state test, more... 1. Fixed the following with the merged.yaml role: [WARNING]: conditional statements should not include jinja2 templating delimiters ... 2. Adding deleted.yaml role 3. Fix ImageUpgradeTask detach_policy result (found with integration test for "deleted" state) --- plugins/modules/dcnm_image_upgrade.py | 13 +- .../dcnm_image_upgrade/tests/deleted.yaml | 251 ++++++++++++++++++ .../dcnm_image_upgrade/tests/merged.yaml | 116 ++++---- 3 files changed, 317 insertions(+), 63 deletions(-) create mode 100644 tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index b0130e95a..99216dd24 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -999,7 +999,7 @@ def _attach_or_detach_image_policy(self, action=None) -> None: if action == "attach": self.result.diff_attach_policy = instance.diff_null - elif action == "detach": + if action == "detach": self.result.diff_detach_policy = instance.diff_null return @@ -1008,7 +1008,10 @@ def _attach_or_detach_image_policy(self, action=None) -> None: instance.action = action instance.serial_numbers = value instance.commit() - self.result.response_attach_policy = copy.deepcopy(instance.response) + if action == "attach": + self.result.response_attach_policy = copy.deepcopy(instance.response) + if action == "detach": + self.result.response_detach_policy = copy.deepcopy(instance.response) for diff in instance.diff: msg = ( @@ -1303,9 +1306,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - # collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - # config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - # log.config = config_file + collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml new file mode 100644 index 000000000..4cb664063 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml @@ -0,0 +1,251 @@ +################################################################################ +# RUNTIME +################################################################################ + +# 45 to 50 minutes +# Some previous runtimes where: +# - 44:07.93 +# - 49:23.53 + +################################################################################ +# STEPS +################################################################################ + +# 1. Create a fabric +# 2. Merge switches into fabric +# 3. Upgrade switches using global config +# 4. Detach policies from switches +# 5. Cleanup + +################################################################################ +# REQUIREMENTS +################################################################################ + +# 1. image policies are already configured on the controller: +# - KR5M (Kerry release maintenance 5) +# - NR3F (Niles release maintenance 3) +# The above include both NX-OS and EPLD images. +# +# TODO: Once dcnm_image_policy module is accepted, use that to +# configure the above policies. +# +# Example vars for dcnm_image_upgrade integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: merged +# fabric_name: f1 +# username: admin +# password: "foobar" +# switch_username: admin +# switch_password: "foobar" +# spine1: 172.22.150.114 +# spine2: 172.22.150.115 +# leaf1: 172.22.150.106 +# leaf2: 172.22.150.107 +# leaf3: 172.22.150.108 +# leaf4: 172.22.150.109 +# # for dcnm_image_upgrade role +# test_fabric: "{{ fabric_name }}" +# ansible_switch1: "{{ leaf1 }}" +# ansible_switch2: "{{ leaf2 }}" +# ansible_switch3: "{{ spine1 }}" +# image_policy_1: "KR5M" +# image_policy_2: "NR3F" + +################################################################################ +# SETUP +################################################################################ + +- set_fact: + rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" + +- name: DELETED - Verify if fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_fabric_create }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.response.DATA != None + +- name: DELETED - setup - Clean up any existing devices + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted + +- name: DELETED - Merge switches + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: merged + config: + - seed_ip: "{{ ansible_switch1 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch2 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch3 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: spine + preserve_config: False + register: result + +- assert: + that: + - result.changed == true + +- assert: + that: + - item["RETURN_CODE"] == 200 + loop: '{{ result.response }}' + +################################################################################ +# DELETED - Upgrade all switches using global_config +################################################################################ + +- name: DELETED - Upgrade all switches using global config + cisco.dcnm.dcnm_image_upgrade: &global_config + state: merged + config: + policy: "{{ image_policy_1 }}" + reboot: false + stage: true + validate: true + upgrade: + nxos: true + epld: false + options: + nxos: + mode: disruptive + bios_force: false + epld: + module: ALL + golden: false + reboot: + config_reload: false + write_erase: false + package: + install: false + uninstall: false + switches: + - ip_address: "{{ leaf1 }}" + - ip_address: "{{ leaf2 }}" + - ip_address: "{{ spine1 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - result.diff.attach_policy[0].policy_name == image_policy_1 + - result.diff.attach_policy[1].policy_name == image_policy_1 + - result.diff.attach_policy[2].policy_name == image_policy_1 + - result.diff.upgrade[0].devices[0].policyName == image_policy_1 + - result.diff.upgrade[1].devices[0].policyName == image_policy_1 + - result.diff.upgrade[2].devices[0].policyName == image_policy_1 + - result.diff.validate[0].policy == image_policy_1 + - result.diff.validate[1].policy == image_policy_1 + - result.diff.validate[2].policy == image_policy_1 + - result.response.attach_policy[0].RETURN_CODE == 200 + - result.response.stage[0].RETURN_CODE == 200 + - result.response.upgrade[0].RETURN_CODE == 200 + - result.response.upgrade[1].RETURN_CODE == 200 + - result.response.upgrade[2].RETURN_CODE == 200 + - result.response.validate[0].RETURN_CODE == 200 + +################################################################################ +# DELETED - Detach policies from two switches and verify +################################################################################ + +- name: DELETED - PAUSE 15 minutes for switches to reboot + ansible.builtin.pause: + minutes: 15 + +- name: DELETED - Detach policies from two switches + cisco.dcnm.dcnm_image_upgrade: + state: deleted + config: + policy: "{{ image_policy_1 }}" + switches: + - ip_address: "{{ leaf1 }}" + - ip_address: "{{ leaf2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff.attach_policy | length) == 0 + - (result.diff.detach_policy | length) == 2 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.attach_policy | length) == 0 + - (result.response.detach_policy | length) == 1 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 + + +################################################################################ +# DELETED - Detach policies from remaining switch and verify +################################################################################ + +- name: DELETED - Detach policy from one remaining switch + cisco.dcnm.dcnm_image_upgrade: + state: deleted + config: + policy: "{{ image_policy_1 }}" + switches: + - ip_address: "{{ spine1 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff.attach_policy | length) == 0 + - (result.diff.detach_policy | length) == 1 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.attach_policy | length) == 0 + - (result.response.detach_policy | length) == 1 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 + +################################################################################ +# CLEAN-UP +################################################################################ + +- name: DELETED - cleanup - Remove devices from fabric + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted \ No newline at end of file diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml index 4a2b1bb00..9574c6a86 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml @@ -68,7 +68,7 @@ - assert: that: - - 'result.response.DATA != None' + - result.response.DATA != None - name: MERGED - setup - Clean up any existing devices cisco.dcnm.dcnm_inventory: @@ -105,11 +105,11 @@ - assert: that: - - 'result.changed == true' + - result.changed == true - assert: that: - - 'item["RETURN_CODE"] == 200' + - item["RETURN_CODE"] == 200 loop: '{{ result.response }}' ################################################################################ @@ -151,23 +151,23 @@ - assert: that: - - 'result.changed == true' - - 'result.failed == false' - - 'result.diff.attach_policy[0].policy_name == "{{ image_policy_1 }}"' - - 'result.diff.attach_policy[1].policy_name == "{{ image_policy_1 }}"' - - 'result.diff.attach_policy[2].policy_name == "{{ image_policy_1 }}"' - - 'result.diff.upgrade[0].devices[0].policyName == "{{ image_policy_1 }}"' - - 'result.diff.upgrade[1].devices[0].policyName == "{{ image_policy_1 }}"' - - 'result.diff.upgrade[2].devices[0].policyName == "{{ image_policy_1 }}"' - - 'result.diff.validate[0].policy == "{{ image_policy_1 }}"' - - 'result.diff.validate[1].policy == "{{ image_policy_1 }}"' - - 'result.diff.validate[2].policy == "{{ image_policy_1 }}"' - - 'result.response.attach_policy[0].RETURN_CODE == 200' - - 'result.response.stage[0].RETURN_CODE == 200' - - 'result.response.upgrade[0].RETURN_CODE == 200' - - 'result.response.upgrade[1].RETURN_CODE == 200' - - 'result.response.upgrade[2].RETURN_CODE == 200' - - 'result.response.validate[0].RETURN_CODE == 200' + - result.changed == true + - result.failed == false + - result.diff.attach_policy[0].policy_name == image_policy_1 + - result.diff.attach_policy[1].policy_name == image_policy_1 + - result.diff.attach_policy[2].policy_name == image_policy_1 + - result.diff.upgrade[0].devices[0].policyName == image_policy_1 + - result.diff.upgrade[1].devices[0].policyName == image_policy_1 + - result.diff.upgrade[2].devices[0].policyName == image_policy_1 + - result.diff.validate[0].policy == image_policy_1 + - result.diff.validate[1].policy == image_policy_1 + - result.diff.validate[2].policy == image_policy_1 + - result.response.attach_policy[0].RETURN_CODE == 200 + - result.response.stage[0].RETURN_CODE == 200 + - result.response.upgrade[0].RETURN_CODE == 200 + - result.response.upgrade[1].RETURN_CODE == 200 + - result.response.upgrade[2].RETURN_CODE == 200 + - result.response.validate[0].RETURN_CODE == 200 ################################################################################ # MERGED IDEMPOTENCE global_config @@ -186,16 +186,16 @@ - assert: that: - - 'result.changed == false' - - 'result.failed == false' - - '(result.diff.attach_policy | length) == 0' - - '(result.diff.stage | length) == 0' - - '(result.diff.upgrade | length) == 0' - - '(result.diff.validate | length) == 0' - - '(result.response.attach_policy | length) == 0' - - '(result.response.stage | length) == 0' - - '(result.response.upgrade | length) == 0' - - '(result.response.validate | length) == 0' + - result.changed == false + - result.failed == false + - (result.diff.attach_policy | length) == 0 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.attach_policy | length) == 0 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 ################################################################################ @@ -240,23 +240,23 @@ - assert: that: - - 'result.changed == true' - - 'result.failed == false' - - 'result.diff.attach_policy[0].policy_name == "{{ image_policy_2 }}"' - - 'result.diff.attach_policy[1].policy_name == "{{ image_policy_2 }}"' - - 'result.diff.attach_policy[2].policy_name == "{{ image_policy_2 }}"' - - 'result.diff.upgrade[0].devices[0].policyName == "{{ image_policy_2 }}"' - - 'result.diff.upgrade[1].devices[0].policyName == "{{ image_policy_2 }}"' - - 'result.diff.upgrade[2].devices[0].policyName == "{{ image_policy_2 }}"' - - 'result.diff.validate[0].policy == "{{ image_policy_2 }}"' - - 'result.diff.validate[1].policy == "{{ image_policy_2 }}"' - - 'result.diff.validate[2].policy == "{{ image_policy_2 }}"' - - 'result.response.attach_policy[0].RETURN_CODE == 200' - - 'result.response.stage[0].RETURN_CODE == 200' - - 'result.response.upgrade[0].RETURN_CODE == 200' - - 'result.response.upgrade[1].RETURN_CODE == 200' - - 'result.response.upgrade[2].RETURN_CODE == 200' - - 'result.response.validate[0].RETURN_CODE == 200' + - result.changed == true + - result.failed == false + - result.diff.attach_policy[0].policy_name == image_policy_2 + - result.diff.attach_policy[1].policy_name == image_policy_2 + - result.diff.attach_policy[2].policy_name == image_policy_2 + - result.diff.upgrade[0].devices[0].policyName == image_policy_2 + - result.diff.upgrade[1].devices[0].policyName == image_policy_2 + - result.diff.upgrade[2].devices[0].policyName == image_policy_2 + - result.diff.validate[0].policy == image_policy_2 + - result.diff.validate[1].policy == image_policy_2 + - result.diff.validate[2].policy == image_policy_2 + - result.response.attach_policy[0].RETURN_CODE == 200 + - result.response.stage[0].RETURN_CODE == 200 + - result.response.upgrade[0].RETURN_CODE == 200 + - result.response.upgrade[1].RETURN_CODE == 200 + - result.response.upgrade[2].RETURN_CODE == 200 + - result.response.validate[0].RETURN_CODE == 200 ################################################################################ # MERGED IDEMPOTENCE switch_config @@ -272,22 +272,22 @@ - assert: that: - - 'result.changed == false' - - 'result.failed == false' - - '(result.diff.attach_policy | length) == 0' - - '(result.diff.stage | length) == 0' - - '(result.diff.upgrade | length) == 0' - - '(result.diff.validate | length) == 0' - - '(result.response.attach_policy | length) == 0' - - '(result.response.stage | length) == 0' - - '(result.response.upgrade | length) == 0' - - '(result.response.validate | length) == 0' + - result.changed == false + - result.failed == false + - (result.diff.attach_policy | length) == 0 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.attach_policy | length) == 0 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 ################################################################################ # CLEAN-UP ################################################################################ -- name: MERGED - cleanup - Clean up any existing devices +- name: MERGED - CLEAN-UP - Remove devices from fabric cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: deleted \ No newline at end of file From b0b04f9386e87bccfc10ba2b163ee2cdc059c155 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 19 Jan 2024 15:04:22 -1000 Subject: [PATCH 212/300] skip query state diffs when determining if anything changed --- plugins/module_utils/image_mgmt/image_upgrade_task_result.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py index 075ca9259..47d6f357b 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py @@ -97,6 +97,9 @@ def did_anything_change(self): return True if diffs have been appended to any of the diff lists. """ for key in self.diff_properties.keys(): + # skip query state diffs + if key == "diff_issu_status": + continue if len(self.properties[key]) != 0: return True return False From 6ca13803f9c399203fe253865dec91bf14fc8f42 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 19 Jan 2024 20:47:46 -1000 Subject: [PATCH 213/300] Add "query" state integration test --- .../dcnm_image_upgrade/tests/query.yaml | 381 ++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 tests/integration/targets/dcnm_image_upgrade/tests/query.yaml diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml new file mode 100644 index 000000000..b68f6ab04 --- /dev/null +++ b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml @@ -0,0 +1,381 @@ +################################################################################ +# RUNTIME +################################################################################ + +# 45 to 50 minutes +# Some previous runtimes where: +# - 47:50.79 + +################################################################################ +# STEPS +################################################################################ + +# 1. Create a fabric +# 2. Merge switches into fabric +# 3. Upgrade switches using global config +# 4. Query and verify ISSU status +# 5. Detach image policies from two of the three switches +# 6. Query and verify ISSU status +# 7. Detach image policy from remaining switch +# 8. Query and verify ISSU status +# 9. Cleanup + +################################################################################ +# REQUIREMENTS +################################################################################ + +# 1. image policies are already configured on the controller: +# - KR5M (Kerry release maintenance 5) +# - NR3F (Niles release maintenance 3) +# The above include both NX-OS and EPLD images. +# +# TODO: Once dcnm_image_policy module is accepted, use that to +# configure the above policies. +# +# Example vars for dcnm_image_upgrade integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: merged +# fabric_name: f1 +# username: admin +# password: "foobar" +# switch_username: admin +# switch_password: "foobar" +# spine1: 172.22.150.114 +# spine2: 172.22.150.115 +# leaf1: 172.22.150.106 +# leaf2: 172.22.150.107 +# leaf3: 172.22.150.108 +# leaf4: 172.22.150.109 +# # for dcnm_image_upgrade role +# test_fabric: "{{ fabric_name }}" +# ansible_switch1: "{{ leaf1 }}" +# ansible_switch2: "{{ leaf2 }}" +# ansible_switch3: "{{ spine1 }}" +# image_policy_1: "KR5M" +# image_policy_2: "NR3F" + +################################################################################ +# SETUP +################################################################################ + +- set_fact: + rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" + +- name: QUERY - Verify if fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_fabric_create }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.response.DATA != None + +- name: QUERY - setup - Clean up any existing devices + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted + +- name: QUERY - Merge switches + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: merged + config: + - seed_ip: "{{ ansible_switch1 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch2 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch3 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: spine + preserve_config: False + register: result + +- assert: + that: + - result.changed == true + +- assert: + that: + - item["RETURN_CODE"] == 200 + loop: '{{ result.response }}' + +################################################################################ +# QUERY - Upgrade all switches using global_config +################################################################################ + +- name: QUERY - Upgrade all switches using global config + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + policy: "{{ image_policy_1 }}" + reboot: false + stage: true + validate: true + upgrade: + nxos: true + epld: false + options: + nxos: + mode: disruptive + bios_force: false + epld: + module: ALL + golden: false + reboot: + config_reload: false + write_erase: false + package: + install: false + uninstall: false + switches: + - ip_address: "{{ ansible_switch1 }}" + - ip_address: "{{ ansible_switch2 }}" + - ip_address: "{{ ansible_switch3 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - result.diff.attach_policy[0].policy_name == image_policy_1 + - result.diff.attach_policy[1].policy_name == image_policy_1 + - result.diff.attach_policy[2].policy_name == image_policy_1 + - result.diff.upgrade[0].devices[0].policyName == image_policy_1 + - result.diff.upgrade[1].devices[0].policyName == image_policy_1 + - result.diff.upgrade[2].devices[0].policyName == image_policy_1 + - result.diff.validate[0].policy == image_policy_1 + - result.diff.validate[1].policy == image_policy_1 + - result.diff.validate[2].policy == image_policy_1 + - result.response.attach_policy[0].RETURN_CODE == 200 + - result.response.stage[0].RETURN_CODE == 200 + - result.response.upgrade[0].RETURN_CODE == 200 + - result.response.upgrade[1].RETURN_CODE == 200 + - result.response.upgrade[2].RETURN_CODE == 200 + - result.response.validate[0].RETURN_CODE == 200 + +- name: QUERY - PAUSE 15 minutes for switches to reboot + ansible.builtin.pause: + minutes: 15 + +################################################################################ +# QUERY - Query and verify ISSU status +################################################################################ + +- name: QUERY - Query ISSU Status for three switches + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + switches: + - ip_address: "{{ ansible_switch1 }}" + - ip_address: "{{ ansible_switch2 }}" + - ip_address: "{{ ansible_switch3 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff.issu_status | length) == 3 + - (result.diff.detach_policy | length) == 0 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.issu_status | length) == 1 + - (result.response.attach_policy | length) == 0 + - (result.response.detach_policy | length) == 0 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 + - (result.diff.issu_status[0].ipAddress) == ansible_switch1 + - (result.diff.issu_status[1].ipAddress) == ansible_switch2 + - (result.diff.issu_status[2].ipAddress) == ansible_switch3 + - (result.diff.issu_status[0].policy) == image_policy_1 + - (result.diff.issu_status[1].policy) == image_policy_1 + - (result.diff.issu_status[2].policy) == image_policy_1 + - (result.diff.issu_status[0].statusPercent) == 100 + - (result.diff.issu_status[1].statusPercent) == 100 + - (result.diff.issu_status[2].statusPercent) == 100 + +################################################################################ +# QUERY - Detach policies from two switches and verify +################################################################################ + +- name: QUERY - Detach policies from two switches + cisco.dcnm.dcnm_image_upgrade: + state: deleted + config: + policy: "{{ image_policy_1 }}" + switches: + - ip_address: "{{ ansible_switch1 }}" + - ip_address: "{{ ansible_switch2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff.detach_policy | length) == 2 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.attach_policy | length) == 0 + - (result.response.detach_policy | length) == 1 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 + + +################################################################################ +# QUERY - Query ISSU status again and verify that policy for two switches +# is set to None +################################################################################ + +- name: QUERY - Query ISSU Status (two switches should have no image policy) + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + switches: + - ip_address: "{{ ansible_switch1 }}" + - ip_address: "{{ ansible_switch2 }}" + - ip_address: "{{ ansible_switch3 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff.issu_status | length) == 3 + - (result.diff.detach_policy | length) == 0 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.issu_status | length) == 1 + - (result.response.attach_policy | length) == 0 + - (result.response.detach_policy | length) == 0 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 + - (result.diff.issu_status[0].ipAddress) == ansible_switch1 + - (result.diff.issu_status[1].ipAddress) == ansible_switch2 + - (result.diff.issu_status[2].ipAddress) == ansible_switch3 + - (result.diff.issu_status[0].policy) == "None" + - (result.diff.issu_status[1].policy) == "None" + - (result.diff.issu_status[2].policy) == image_policy_1 + - (result.diff.issu_status[0].statusPercent) == 0 + - (result.diff.issu_status[1].statusPercent) == 0 + - (result.diff.issu_status[2].statusPercent) == 100 + +################################################################################ +# QUERY - Detach policies from remaining switch and verify +################################################################################ + +- name: QUERY - Detach policy from remaining switch + cisco.dcnm.dcnm_image_upgrade: + state: deleted + config: + policy: "{{ image_policy_1 }}" + switches: + - ip_address: "{{ ansible_switch3 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff.attach_policy | length) == 0 + - (result.diff.detach_policy | length) == 1 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.attach_policy | length) == 0 + - (result.response.detach_policy | length) == 1 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 + +################################################################################ +# QUERY - Query ISSU status again and verify that policy for all switches +# is set to None +################################################################################ + +- name: QUERY - Query ISSU Status (all switches should have no image policy) + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + switches: + - ip_address: "{{ ansible_switch1 }}" + - ip_address: "{{ ansible_switch2 }}" + - ip_address: "{{ ansible_switch3 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff.issu_status | length) == 3 + - (result.diff.detach_policy | length) == 0 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.issu_status | length) == 1 + - (result.response.attach_policy | length) == 0 + - (result.response.detach_policy | length) == 0 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 + - (result.diff.issu_status[0].ipAddress) == ansible_switch1 + - (result.diff.issu_status[1].ipAddress) == ansible_switch2 + - (result.diff.issu_status[2].ipAddress) == ansible_switch3 + - (result.diff.issu_status[0].policy) == "None" + - (result.diff.issu_status[1].policy) == "None" + - (result.diff.issu_status[2].policy) == "None" + - (result.diff.issu_status[0].statusPercent) == 0 + - (result.diff.issu_status[1].statusPercent) == 0 + - (result.diff.issu_status[2].statusPercent) == 0 + +################################################################################ +# CLEAN-UP +################################################################################ + +- name: QUERY - cleanup - Remove devices from fabric + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted \ No newline at end of file From 758653e08327f52c0a7b4bc6ae871225ead4737d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 22 Jan 2024 14:46:08 -1000 Subject: [PATCH 214/300] Add loop around dcnm_send to wait for successful response. We've seen cases were a 500 response is returned if install-options is called too early. Add a loop to try for self.timeout seconds with 5 second poll time. Update unit tests to bypass the sleep so that the tests run faster. --- .../image_mgmt/install_options.py | 88 +++++++++++++++++-- ...est_image_upgrade_image_install_options.py | 16 +++- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index c3d37f075..874c80c26 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -21,6 +21,7 @@ import inspect import json import logging +import time from typing import Any, Dict from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -158,14 +159,16 @@ def __init__(self, module) -> None: def _init_properties(self): # self.properties is already initialized in the parent class self.properties["epld"] = False + self.properties["epld_modules"] = None self.properties["issu"] = True - self.properties["response_data"] = None - self.properties["response"] = None - self.properties["result"] = None self.properties["package_install"] = False self.properties["policy_name"] = None + self.properties["response"] = None + self.properties["response_data"] = None + self.properties["result"] = {} self.properties["serial_number"] = None - self.properties["epld_modules"] = None + self.properties["timeout"] = 300 + self.properties["unit_test"] = False def refresh(self) -> None: """ @@ -185,6 +188,10 @@ def refresh(self) -> None: msg += "calling refresh()" self.module.fail_json(msg, **self.failed_result) + msg = f"self.epld {self.epld}, " + msg += f"self.issu {self.issu}, " + msg += f"self.package_install {self.package_install}" + self.log.debug(msg) # At least one of epld, issu, or package_install must be True # before calling refresh() or the controller will return an error. # Mock the response such that the caller knows nothing needs to be @@ -204,11 +211,34 @@ def refresh(self) -> None: self._build_payload() - self.properties["response"] = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(self.payload) - ) - self.properties["response_data"] = self.response.get("DATA", {}) - self.properties["result"] = self._handle_response(self.response, self.verb) + timeout = self.timeout + sleep_time = 5 + self.result["success"] = False + + msg = f"Entering dcnm_send loop. timeout {timeout}, sleep_time {sleep_time}" + self.log.debug(msg) + + while timeout > 0 and self.result.get("success") is False: + msg = "Calling dcnm_send with payload: " + msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.properties["response"] = dcnm_send( + self.module, self.verb, self.path, data=json.dumps(self.payload) + ) + + msg = "Calling dcnm_send DONE" + self.log.debug(msg) + + self.properties["response_data"] = self.response.get("DATA", {}) + self.properties["result"] = self._handle_response(self.response, self.verb) + + msg = f"self.response {self.response}" + self.log.debug(msg) + + if self.result.get("success") is False and self.unit_test is False: + time.sleep(sleep_time) + timeout -= sleep_time if self.result["success"] is False: msg = f"{self.class_name}.{method_name}: " @@ -255,6 +285,9 @@ def _build_payload(self) -> None: self.payload["epld"] = self.epld self.payload["packageInstall"] = self.package_install + msg = f"self.payload {self.payload}" + self.log.debug(msg) + def _get(self, item): return self.make_boolean(self.make_none(self.response_data.get(item))) @@ -287,6 +320,7 @@ def serial_number(self, value): self.properties["serial_number"] = value # Optional properties + @property def issu(self): """ @@ -352,6 +386,42 @@ def package_install(self, value): self.module.fail_json(msg, **self.failed_result) self.properties["package_install"] = value + @property + def timeout(self): + """ + Timeout, in seconds, for retrieving install-options from the controller. + Valid values: int() + Default: 300 + """ + return self.properties.get("timeout") + + @timeout.setter + def timeout(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an int(). Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["timeout"] = value + + @property + def unit_test(self): + """ + Is the class running under a unit test. + Set this to True in unit tests to speed the test up. + Default: False + """ + return self.properties.get("unit_test") + + @unit_test.setter + def unit_test(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be a bool(). Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["unit_test"] = value + # Retrievable properties @property def comp_disp(self): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index ba17fe952..1d6327d42 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -82,8 +82,10 @@ def test_image_mgmt_install_options_00002(image_install_options) -> None: assert instance.properties.get("policy_name") is None assert instance.properties.get("response") is None assert instance.properties.get("response_data") is None - assert instance.properties.get("result") is None + assert instance.properties.get("result") == {} assert instance.properties.get("serial_number") is None + assert instance.properties.get("timeout") == 300 + assert instance.properties.get("unit_test") is False def test_image_mgmt_install_options_00003(image_install_options) -> None: @@ -142,6 +144,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) instance = image_install_options + instance.unit_test = True instance.policy_name = "KRM5" instance.serial_number = "BAR" instance.refresh() @@ -188,6 +191,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: match += "the controller. Controller response:" instance = image_install_options + instance.unit_test = True instance.policy_name = "KRM5" instance.serial_number = "BAR" with pytest.raises(AnsibleFailJson, match=rf"{match}"): @@ -223,6 +227,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance = image_install_options instance.policy_name = "KRM5" instance.serial_number = "FDO21120U5D" + instance.unit_test = True instance.refresh() assert isinstance(instance.response, dict) assert instance.device_name == "leaf1" @@ -279,6 +284,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.epld = True instance.issu = True instance.package_install = False + instance.unit_test = True instance.refresh() assert isinstance(instance.response, dict) assert instance.device_name == "leaf1" @@ -336,6 +342,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.epld = True instance.issu = False instance.package_install = False + instance.unit_test = True instance.refresh() assert isinstance(instance.response, dict) assert instance.device_name is None @@ -397,6 +404,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.epld = True instance.issu = True instance.package_install = True + instance.unit_test = True with pytest.raises(AnsibleFailJson, match=match): instance.refresh() @@ -431,6 +439,7 @@ def test_image_mgmt_install_options_00011(image_install_options) -> None: instance.epld = False instance.issu = False instance.package_install = False + instance.unit_test = True instance.refresh() assert isinstance(instance.response_data, dict) @@ -456,6 +465,7 @@ def test_image_mgmt_install_options_00020(image_install_options) -> None: instance = image_install_options instance.policy_name = "KRM5" instance.serial_number = "BAR" + instance.unit_test = True instance._build_payload() # pylint: disable=protected-access assert instance.payload.get("devices")[0].get("policyName") == "KRM5" assert instance.payload.get("devices")[0].get("serialNumber") == "BAR" @@ -482,6 +492,7 @@ def test_image_mgmt_install_options_00021(image_install_options) -> None: instance.issu = False instance.epld = True instance.package_install = True + instance.unit_test = True instance._build_payload() # pylint: disable=protected-access assert instance.payload.get("devices")[0].get("policyName") == "KRM5" assert instance.payload.get("devices")[0].get("serialNumber") == "BAR" @@ -502,6 +513,7 @@ def test_image_mgmt_install_options_00022(image_install_options) -> None: match += "boolean value" instance = image_install_options + instance.unit_test = True with pytest.raises(AnsibleFailJson, match=match): instance.issu = "FOO" @@ -518,6 +530,7 @@ def test_image_mgmt_install_options_00023(image_install_options) -> None: match += "boolean value" instance = image_install_options + instance.unit_test = True with pytest.raises(AnsibleFailJson, match=match): instance.epld = "FOO" @@ -534,5 +547,6 @@ def test_image_mgmt_install_options_00024(image_install_options) -> None: match += "package_install must be a boolean value" instance = image_install_options + instance.unit_test = True with pytest.raises(AnsibleFailJson, match=match): instance.package_install = "FOO" From b1be8935c2e7de9c4092ea34c137745029fdccba Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 22 Jan 2024 15:10:29 -1000 Subject: [PATCH 215/300] Remove test_image_mgmt_validate_00020 We''re now initializing self.serial_numbers to [] instead of None and silently returning if self.serial_numbers is [] in commit(). Previously, we would fail_json if self.serial_numbers was still set to None in commit(). We no longer need the unit tests that verifies fail_json is called. --- .../module_utils/image_mgmt/image_validate.py | 23 +++++----- .../test_image_upgrade_image_validate.py | 45 +------------------ 2 files changed, 11 insertions(+), 57 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 0f2f3bfcb..244fb662b 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -86,7 +86,9 @@ def __init__(self, module): self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self) -> None: - self.method_name = inspect.stack()[0][3] + """ + Initialize the properties dictionary + """ # self.properties is already initialized in the parent class self.properties["check_interval"] = 10 # seconds @@ -95,14 +97,15 @@ def _init_properties(self) -> None: self.properties["result"] = {} self.properties["response"] = {} self.properties["non_disruptive"] = False - self.properties["serial_numbers"] = None + self.properties["serial_numbers"] = [] def prune_serial_numbers(self) -> None: """ If the image is already validated on a switch, remove that switch's serial number from the list of serial numbers to validate. """ - self.method_name = inspect.stack()[0][3] + msg = f"ENTERED: self.serial_numbers {self.serial_numbers}" + self.log.debug(msg) self.issu_detail.refresh() serial_numbers = copy.copy(self.serial_numbers) @@ -111,6 +114,9 @@ def prune_serial_numbers(self) -> None: if self.issu_detail.validated == "Success": self.serial_numbers.remove(self.issu_detail.serial_number) + msg = f"DONE: self.serial_numbers {self.serial_numbers}" + self.log.debug(msg) + def validate_serial_numbers(self) -> None: """ Log a warning if the validated state for any serial_number @@ -156,16 +162,7 @@ def commit(self) -> None: """ method_name = inspect.stack()[0][3] - msg = "ENTERED commit()" - self.log.debug(msg) - - if self.serial_numbers is None: - msg = f"{self.class_name}.{method_name}: " - msg += "call instance.serial_numbers " - msg += "before calling commit." - self.module.fail_json(msg, **self.failed_result) - - msg = f"self.serial_numbers: {self.serial_numbers}" + msg = f"ENTERED: self.serial_numbers: {self.serial_numbers}" self.log.debug(msg) if len(self.serial_numbers) == 0: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 54f80d2e2..72be7dc72 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -85,7 +85,7 @@ def test_image_mgmt_validate_00002(image_validate) -> None: assert instance.properties.get("response") == {} assert instance.properties.get("result") == {} assert instance.properties.get("non_disruptive") is False - assert instance.properties.get("serial_numbers") is None + assert instance.properties.get("serial_numbers") == [] def test_image_mgmt_validate_00003( @@ -401,49 +401,6 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -MATCH_00020 = "ImageValidate.commit: call instance.serial_numbers " -MATCH_00020 += "before calling commit." - - -@pytest.mark.parametrize( - "serial_numbers_is_set, expected", - [ - (True, does_not_raise()), - (False, pytest.raises(AnsibleFailJson, match=MATCH_00020)), - ], -) -def test_image_mgmt_validate_00020( - monkeypatch, image_validate, serial_numbers_is_set, expected -) -> None: - """ - Function - commit - - Test - - fail_json is called when serial_numbers is None - - fail_json is not called when serial_numbers is set - """ - - def mock_dcnm_send_image_validate(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00020a" - return responses_image_validate(key) - - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00020a" - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_IMAGE_VALIDATE, mock_dcnm_send_image_validate) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - - instance = image_validate - assert instance.class_name == "ImageValidate" - - if serial_numbers_is_set: - instance.serial_numbers = ["FDO21120U5D"] - with expected: - instance.commit() - - def test_image_mgmt_validate_00021(monkeypatch, image_validate) -> None: """ Function From 7137d46568ed58199a05b25c7e00fbdcc5676000 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 22 Jan 2024 15:16:29 -1000 Subject: [PATCH 216/300] ImageUpgradeTaskResult: Remove redundant class definition. --- .../image_mgmt/image_policy_action.py | 2 ++ .../image_mgmt/image_upgrade_task_result.py | 30 +++---------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 87616b0f9..7843dae54 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -112,6 +112,8 @@ def build_payload(self): payload["ipAddr"] = self.switch_issu_details.ip_address payload["platform"] = self.switch_issu_details.platform payload["serialNumber"] = self.switch_issu_details.serial_number + msg = f"payload: {json.dumps(payload, indent=4)}" + self.log.debug(msg) for key, value in payload.items(): if value is None: msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py index 47d6f357b..ecbe1a06c 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py @@ -21,34 +21,12 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.result import \ - Result - -class ImageUpgradeTaskResult(Result): +class ImageUpgradeTaskResult: """ Storage for ImageUpgradeTask result - - Override properties in Result() for use with ImageUpgradeTask module. - - Specifically, this class adds the following properties: - - stage: dict() - represents the image stage result - validate: dict() - represents the image validate result - install: dict() - represents the image install result - - stage, validate and install are added to diff["merged"] like so: - - diff["merged"]["stage"] = stage - diff["merged"]["validate"] = validate - diff["merged"]["install"] = install - - Usage: - """ - -class ImageUpgradeTaskResult: def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module @@ -96,7 +74,7 @@ def did_anything_change(self): """ return True if diffs have been appended to any of the diff lists. """ - for key in self.diff_properties.keys(): + for key in self.diff_properties: # skip query state diffs if key == "diff_issu_status": continue @@ -123,9 +101,9 @@ def failed_result(self): result["failed"] = True result["diff"] = {} result["response"] = {} - for key in self.diff_properties.keys(): + for key in self.diff_properties: result["diff"][key] = [] - for key in self.response_properties.keys(): + for key in self.response_properties: result["response"][key] = [] return result From 6247470b8395258e9b826310999cfc90e6a32b85 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 22 Jan 2024 19:49:26 -1000 Subject: [PATCH 217/300] Break "merged" state integration tests into two files, more... Rename ansible_switchX to ansible_switch_X Update comments and task names to include test stage --- .../dcnm_image_upgrade/tests/deleted.yaml | 83 +++--- ...{merged.yaml => merged_global_config.yaml} | 153 +++++----- .../tests/merged_override_global_config.yaml | 271 ++++++++++++++++++ .../dcnm_image_upgrade/tests/query.yaml | 131 +++++---- 4 files changed, 459 insertions(+), 179 deletions(-) rename tests/integration/targets/dcnm_image_upgrade/tests/{merged.yaml => merged_global_config.yaml} (65%) create mode 100644 tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml index 4cb664063..8ff70c990 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml @@ -2,20 +2,24 @@ # RUNTIME ################################################################################ -# 45 to 50 minutes -# Some previous runtimes where: -# - 44:07.93 -# - 49:23.53 +# 30 to 40 minutes +# Some previous runtimes: +# - 32:06.27 ################################################################################ # STEPS ################################################################################ +# SETUP # 1. Create a fabric # 2. Merge switches into fabric # 3. Upgrade switches using global config -# 4. Detach policies from switches -# 5. Cleanup +# 4. Wait for all switches to complete ISSU +# TEST +# 5. Detach policies from two switches and verify +# 6. Detach policy from remaining switch and verify +# CLEANUP +# 7. Delete devices from fabric ################################################################################ # REQUIREMENTS @@ -48,9 +52,9 @@ # leaf4: 172.22.150.109 # # for dcnm_image_upgrade role # test_fabric: "{{ fabric_name }}" -# ansible_switch1: "{{ leaf1 }}" -# ansible_switch2: "{{ leaf2 }}" -# ansible_switch3: "{{ spine1 }}" +# ansible_switch_1: "{{ leaf1 }}" +# ansible_switch_2: "{{ leaf2 }}" +# ansible_switch_3: "{{ spine1 }}" # image_policy_1: "KR5M" # image_policy_2: "NR3F" @@ -61,7 +65,7 @@ - set_fact: rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" -- name: DELETED - Verify if fabric is deployed. +- name: DELETED - SETUP - Verify if fabric is deployed. cisco.dcnm.dcnm_rest: method: GET path: "{{ rest_fabric_create }}" @@ -74,31 +78,31 @@ that: - result.response.DATA != None -- name: DELETED - setup - Clean up any existing devices +- name: DELETED - SETUP - Clean up any existing devices cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: deleted -- name: DELETED - Merge switches +- name: DELETED - SETUP - Merge switches cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: merged config: - - seed_ip: "{{ ansible_switch1 }}" + - seed_ip: "{{ ansible_switch_1 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" max_hops: 0 role: leaf preserve_config: False - - seed_ip: "{{ ansible_switch2 }}" + - seed_ip: "{{ ansible_switch_2 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" max_hops: 0 role: leaf preserve_config: False - - seed_ip: "{{ ansible_switch3 }}" + - seed_ip: "{{ ansible_switch_3 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" @@ -117,10 +121,10 @@ loop: '{{ result.response }}' ################################################################################ -# DELETED - Upgrade all switches using global_config +# DELETED - SETUP - Upgrade all switches using global_config ################################################################################ -- name: DELETED - Upgrade all switches using global config +- name: DELETED - SETUP - Upgrade all switches using global config cisco.dcnm.dcnm_image_upgrade: &global_config state: merged config: @@ -145,9 +149,9 @@ install: false uninstall: false switches: - - ip_address: "{{ leaf1 }}" - - ip_address: "{{ leaf2 }}" - - ip_address: "{{ spine1 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: @@ -173,22 +177,35 @@ - result.response.upgrade[2].RETURN_CODE == 200 - result.response.validate[0].RETURN_CODE == 200 +- name: DELETED - SETUP - Wait for controller response for all three switches + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + switches: + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" + register: result + until: + - result.diff.issu_status[0].ipAddress == ansible_switch_1 + - result.diff.issu_status[1].ipAddress == ansible_switch_2 + - result.diff.issu_status[2].ipAddress == ansible_switch_3 + retries: 60 + delay: 5 + ignore_errors: yes + ################################################################################ -# DELETED - Detach policies from two switches and verify +# DELETED - TEST - Detach policies from two switches and verify ################################################################################ -- name: DELETED - PAUSE 15 minutes for switches to reboot - ansible.builtin.pause: - minutes: 15 - -- name: DELETED - Detach policies from two switches +- name: DELETED - TEST - Detach policies from two switches cisco.dcnm.dcnm_image_upgrade: state: deleted config: policy: "{{ image_policy_1 }}" switches: - - ip_address: "{{ leaf1 }}" - - ip_address: "{{ leaf2 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" register: result - debug: @@ -211,16 +228,16 @@ ################################################################################ -# DELETED - Detach policies from remaining switch and verify +# DELETED - TEST - Detach policies from remaining switch and verify ################################################################################ -- name: DELETED - Detach policy from one remaining switch +- name: DELETED - TEST - Detach policy from remaining switch cisco.dcnm.dcnm_image_upgrade: state: deleted config: policy: "{{ image_policy_1 }}" switches: - - ip_address: "{{ spine1 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: @@ -245,7 +262,7 @@ # CLEAN-UP ################################################################################ -- name: DELETED - cleanup - Remove devices from fabric +- name: DELETED - CLEANUP - Remove devices from fabric cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" - state: deleted \ No newline at end of file + state: deleted diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml similarity index 65% rename from tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml rename to tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml index 9574c6a86..feeddf359 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml @@ -1,17 +1,41 @@ +################################################################################ +# TESTCASE: +# +# merged_global_config +# +# Description: +# +# This test case verifies Ansible merged state for dcnm_image_upgrade. +# All switches use the same image policy and the configuration specifics +# are all specified in the global config stanza. +# +# To minimize runtime, we use preserve_config: True during SETUP +################################################################################ + ################################################################################ # RUNTIME ################################################################################ -# About 1.5 hours +# Approximately 30 minutes +# Recent run times: +# 29:49.15 +# 33:40.57 +# 28:58.18 +# 31:41.42 ################################################################################ # STEPS ################################################################################ +# SETUP # 1. Create a fabric # 2. Merge switches into fabric -# 3. Upgrade switches using global config -# 4. Upgrade switches using switch config, overriding policy in global config +# TEST +# 3. Upgrade switches using global config and verify +# 4. Wait for all switches to complete ISSU +# 5. Test idempotence +# CLEANUP +# 6. Remove devices from fabric ################################################################################ # REQUIREMENTS @@ -44,9 +68,9 @@ # leaf4: 172.22.150.109 # # for dcnm_image_upgrade role # test_fabric: "{{ fabric_name }}" -# ansible_switch1: "{{ leaf1 }}" -# ansible_switch2: "{{ leaf2 }}" -# ansible_switch3: "{{ spine1 }}" +# ansible_switch_1: "{{ leaf1 }}" +# ansible_switch_2: "{{ leaf2 }}" +# ansible_switch_3: "{{ spine1 }}" # image_policy_1: "KR5M" # image_policy_2: "NR3F" @@ -57,7 +81,7 @@ - set_fact: rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" -- name: MERGED - Verify if fabric is deployed. +- name: MERGED - SETUP - Verify if fabric is deployed. cisco.dcnm.dcnm_rest: method: GET path: "{{ rest_fabric_create }}" @@ -70,31 +94,31 @@ that: - result.response.DATA != None -- name: MERGED - setup - Clean up any existing devices +- name: MERGED - SETUP - Clean up any existing devices cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: deleted -- name: MERGED - Merge switches +- name: MERGED - SETUP - Merge switches cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: merged config: - - seed_ip: "{{ ansible_switch1 }}" + - seed_ip: "{{ ansible_switch_1 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" max_hops: 0 role: leaf preserve_config: False - - seed_ip: "{{ ansible_switch2 }}" + - seed_ip: "{{ ansible_switch_2 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" max_hops: 0 role: leaf preserve_config: False - - seed_ip: "{{ ansible_switch3 }}" + - seed_ip: "{{ ansible_switch_3 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" @@ -113,11 +137,11 @@ loop: '{{ result.response }}' ################################################################################ -# MERGED global_config +# MERGED - TEST - Upgrade all switches using global config ################################################################################ -- name: MERGED - Upgrade all switches using global config - cisco.dcnm.dcnm_image_upgrade: &global_config +- name: MERGED - TEST - Upgrade all switches using global config + cisco.dcnm.dcnm_image_upgrade: state: merged config: policy: "{{ image_policy_1}}" @@ -141,9 +165,9 @@ install: false uninstall: false switches: - - ip_address: "{{ leaf1 }}" - - ip_address: "{{ leaf2 }}" - - ip_address: "{{ spine1 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: @@ -169,41 +193,29 @@ - result.response.upgrade[2].RETURN_CODE == 200 - result.response.validate[0].RETURN_CODE == 200 -################################################################################ -# MERGED IDEMPOTENCE global_config -################################################################################ - -- name: MERGED - global_config - Idempotence PAUSE 15 minutes - ansible.builtin.pause: - minutes: 15 - -- name: MERGED - global_config - Idempotence - cisco.dcnm.dcnm_image_upgrade: *global_config +- name: MERGED - TEST - Wait for controller response for all three switches + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + switches: + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - -- debug: - var: result - -- assert: - that: - - result.changed == false - - result.failed == false - - (result.diff.attach_policy | length) == 0 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.attach_policy | length) == 0 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 - + until: + - result.diff.issu_status[0].ipAddress == ansible_switch_1 + - result.diff.issu_status[1].ipAddress == ansible_switch_2 + - result.diff.issu_status[2].ipAddress == ansible_switch_3 + retries: 60 + delay: 5 + ignore_errors: yes ################################################################################ -# MERGED switch_config +# MERGED - TEST - global_config - IDEMPOTENCE ################################################################################ -- name: MERGED - Upgrade all switches using global config. Override policy in switch configs. - cisco.dcnm.dcnm_image_upgrade: &switch_config +- name: MERGED - TEST - global_config - IDEMPOTENCE + cisco.dcnm.dcnm_image_upgrade: state: merged config: policy: "{{ image_policy_1}}" @@ -227,49 +239,14 @@ install: false uninstall: false switches: - - ip_address: "{{ leaf1 }}" - policy: "{{ image_policy_2 }}" - - ip_address: "{{ leaf2 }}" - policy: "{{ image_policy_2 }}" - - ip_address: "{{ spine1 }}" - policy: "{{ image_policy_2 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: var: result -- assert: - that: - - result.changed == true - - result.failed == false - - result.diff.attach_policy[0].policy_name == image_policy_2 - - result.diff.attach_policy[1].policy_name == image_policy_2 - - result.diff.attach_policy[2].policy_name == image_policy_2 - - result.diff.upgrade[0].devices[0].policyName == image_policy_2 - - result.diff.upgrade[1].devices[0].policyName == image_policy_2 - - result.diff.upgrade[2].devices[0].policyName == image_policy_2 - - result.diff.validate[0].policy == image_policy_2 - - result.diff.validate[1].policy == image_policy_2 - - result.diff.validate[2].policy == image_policy_2 - - result.response.attach_policy[0].RETURN_CODE == 200 - - result.response.stage[0].RETURN_CODE == 200 - - result.response.upgrade[0].RETURN_CODE == 200 - - result.response.upgrade[1].RETURN_CODE == 200 - - result.response.upgrade[2].RETURN_CODE == 200 - - result.response.validate[0].RETURN_CODE == 200 - -################################################################################ -# MERGED IDEMPOTENCE switch_config -################################################################################ - -- name: MERGED - switch_config - Idempotence PAUSE 15 minutes - ansible.builtin.pause: - minutes: 15 - -- name: MERGED - switch_config - Idempotence - cisco.dcnm.dcnm_image_upgrade: *switch_config - register: result - - assert: that: - result.changed == false @@ -287,7 +264,7 @@ # CLEAN-UP ################################################################################ -- name: MERGED - CLEAN-UP - Remove devices from fabric +- name: MERGED - CLEANUP - Remove devices from fabric cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" - state: deleted \ No newline at end of file + state: deleted diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml new file mode 100644 index 000000000..9eb4b722d --- /dev/null +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml @@ -0,0 +1,271 @@ +################################################################################ +# TESTCASE: +# merged_override_global_config +# +# Description: +# +# This test case verifies Ansible merged state for dcnm_image_upgrade. +# All switches override the image policy specified in the global config +# stanza, by specifying a different image policy in their individual +# switch config stanzas. +# All other upgrade options are specified in the global config stanza. +################################################################################ + +################################################################################ +# RUNTIME +################################################################################ + +# About 30 minutes +# Recent run times: +# 33:18.99 +# 27:36.11 + +################################################################################ +# STEPS +################################################################################ + +# SETUP +# 1. Create a fabric +# 2. Merge switches into fabric +# TEST +# 3. Upgrade switches using global config, overriding image policy in switch config +# 4. Verify the upgrade is successful. +# 5. Wait for all switches to complete ISSU +# 6. Test idempotence +# CLEANUP +# 7. Remove devices from fabric + +################################################################################ +# REQUIREMENTS +################################################################################ + +# 1. Recommended to use a simple fabric type +# e.g. LAN Classic or Enhanced LAN Classic +# 2. image policies are already configured on the controller: +# - KR5M (Kerry release maintenance 5) +# - NR3F (Niles release maintenance 3) +# The above include both NX-OS and EPLD images. +# +# TODO: Once dcnm_image_policy module is accepted, use that to +# configure the above policies. +# +# Example vars for dcnm_image_upgrade integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: merged +# fabric_name: f1 +# username: admin +# password: "foobar" +# switch_username: admin +# switch_password: "foobar" +# spine1: 172.22.150.114 +# spine2: 172.22.150.115 +# leaf1: 172.22.150.106 +# leaf2: 172.22.150.107 +# leaf3: 172.22.150.108 +# leaf4: 172.22.150.109 +# # for dcnm_image_upgrade role +# test_fabric: "{{ fabric_name }}" +# ansible_switch_1: "{{ leaf1 }}" +# ansible_switch_2: "{{ leaf2 }}" +# ansible_switch_3: "{{ spine1 }}" +# image_policy_1: "KR5M" +# image_policy_2: "NR3F" + +################################################################################ +# SETUP +################################################################################ + +- set_fact: + rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" + +- name: MERGED - SETUP - Verify if fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_fabric_create }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.response.DATA != None + +- name: MERGED - SETUP - Clean up any existing devices + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted + +- name: MERGED - SETUP - Merge switches + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: merged + config: + - seed_ip: "{{ ansible_switch_1 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch_2 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: leaf + preserve_config: False + - seed_ip: "{{ ansible_switch_3 }}" + auth_proto: MD5 + user_name: "{{ switch_username }}" + password: "{{ switch_password }}" + max_hops: 0 + role: spine + preserve_config: False + register: result + +- assert: + that: + - item["RETURN_CODE"] == 200 + loop: '{{ result.response }}' + +################################################################################ +# MERGED - TEST - Override global image policy in switch configs +################################################################################ + +- name: MERGED - TEST - Upgrade all switches using global config. Override policy in switch configs. + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + policy: "{{ image_policy_1}}" + reboot: false + stage: true + validate: true + upgrade: + nxos: true + epld: false + options: + nxos: + mode: disruptive + bios_force: false + epld: + module: ALL + golden: false + reboot: + config_reload: false + write_erase: false + package: + install: false + uninstall: false + switches: + - ip_address: "{{ ansible_switch_1 }}" + policy: "{{ image_policy_2 }}" + - ip_address: "{{ ansible_switch_2 }}" + policy: "{{ image_policy_2 }}" + - ip_address: "{{ ansible_switch_3 }}" + policy: "{{ image_policy_2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - result.diff.attach_policy[0].policy_name == image_policy_2 + - result.diff.attach_policy[1].policy_name == image_policy_2 + - result.diff.attach_policy[2].policy_name == image_policy_2 + - result.diff.upgrade[0].devices[0].policyName == image_policy_2 + - result.diff.upgrade[1].devices[0].policyName == image_policy_2 + - result.diff.upgrade[2].devices[0].policyName == image_policy_2 + - result.diff.validate[0].policy == image_policy_2 + - result.diff.validate[1].policy == image_policy_2 + - result.diff.validate[2].policy == image_policy_2 + - result.response.attach_policy[0].RETURN_CODE == 200 + - result.response.stage[0].RETURN_CODE == 200 + - result.response.upgrade[0].RETURN_CODE == 200 + - result.response.upgrade[1].RETURN_CODE == 200 + - result.response.upgrade[2].RETURN_CODE == 200 + - result.response.validate[0].RETURN_CODE == 200 + +- name: MERGED - TEST - Wait for controller response for all three switches + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + switches: + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" + register: result + until: + - result.diff.issu_status[0].ipAddress == ansible_switch_1 + - result.diff.issu_status[1].ipAddress == ansible_switch_2 + - result.diff.issu_status[2].ipAddress == ansible_switch_3 + retries: 60 + delay: 5 + ignore_errors: yes + +################################################################################ +# MERGED - TEST - IDEMPOTENCE switch_config +# +# Anchor and Alias didn't work for this. I copied the entire config from above +################################################################################ + +- name: MERGED - TEST - switch_config - Idempotence + cisco.dcnm.dcnm_image_upgrade: + state: merged + config: + policy: "{{ image_policy_1}}" + reboot: false + stage: true + validate: true + upgrade: + nxos: true + epld: false + options: + nxos: + mode: disruptive + bios_force: false + epld: + module: ALL + golden: false + reboot: + config_reload: false + write_erase: false + package: + install: false + uninstall: false + switches: + - ip_address: "{{ ansible_switch_1 }}" + policy: "{{ image_policy_2 }}" + - ip_address: "{{ ansible_switch_2 }}" + policy: "{{ image_policy_2 }}" + - ip_address: "{{ ansible_switch_3 }}" + policy: "{{ image_policy_2 }}" + register: result + +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff.attach_policy | length) == 0 + - (result.diff.stage | length) == 0 + - (result.diff.upgrade | length) == 0 + - (result.diff.validate | length) == 0 + - (result.response.attach_policy | length) == 0 + - (result.response.stage | length) == 0 + - (result.response.upgrade | length) == 0 + - (result.response.validate | length) == 0 + +################################################################################ +# CLEAN-UP +################################################################################ + +- name: MERGED - CLEANUP - Remove devices from fabric + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name }}" + state: deleted diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml index b68f6ab04..229b408db 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml @@ -2,23 +2,27 @@ # RUNTIME ################################################################################ -# 45 to 50 minutes +# 30 minutes # Some previous runtimes where: -# - 47:50.79 +# - 26:19.11 +# - 26:32.97 ################################################################################ # STEPS ################################################################################ -# 1. Create a fabric +# SETUP +# 1. Verify fabric is deployed # 2. Merge switches into fabric # 3. Upgrade switches using global config -# 4. Query and verify ISSU status +# TEST +# 4. Query and verify ISSU status image_policy_1 attached to all switches # 5. Detach image policies from two of the three switches -# 6. Query and verify ISSU status +# 6. Query and verify ISSU status image_policy_1 removed from two switches # 7. Detach image policy from remaining switch -# 8. Query and verify ISSU status -# 9. Cleanup +# 8. Query and verify ISSU status image_policy_1 removed from all switches +# CLEANUP +# 9. Delete devices from fabric ################################################################################ # REQUIREMENTS @@ -51,9 +55,9 @@ # leaf4: 172.22.150.109 # # for dcnm_image_upgrade role # test_fabric: "{{ fabric_name }}" -# ansible_switch1: "{{ leaf1 }}" -# ansible_switch2: "{{ leaf2 }}" -# ansible_switch3: "{{ spine1 }}" +# ansible_switch_1: "{{ leaf1 }}" +# ansible_switch_2: "{{ leaf2 }}" +# ansible_switch_3: "{{ spine1 }}" # image_policy_1: "KR5M" # image_policy_2: "NR3F" @@ -64,7 +68,7 @@ - set_fact: rest_fabric_create: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ fabric_name }}" -- name: QUERY - Verify if fabric is deployed. +- name: QUERY - SETUP - Verify if fabric is deployed. cisco.dcnm.dcnm_rest: method: GET path: "{{ rest_fabric_create }}" @@ -77,31 +81,31 @@ that: - result.response.DATA != None -- name: QUERY - setup - Clean up any existing devices +- name: QUERY - SETUP - Clean up any existing devices cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: deleted -- name: QUERY - Merge switches +- name: QUERY - SETUP - Merge switches cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: merged config: - - seed_ip: "{{ ansible_switch1 }}" + - seed_ip: "{{ ansible_switch_1 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" max_hops: 0 role: leaf preserve_config: False - - seed_ip: "{{ ansible_switch2 }}" + - seed_ip: "{{ ansible_switch_2 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" max_hops: 0 role: leaf preserve_config: False - - seed_ip: "{{ ansible_switch3 }}" + - seed_ip: "{{ ansible_switch_3 }}" auth_proto: MD5 user_name: "{{ switch_username }}" password: "{{ switch_password }}" @@ -120,10 +124,10 @@ loop: '{{ result.response }}' ################################################################################ -# QUERY - Upgrade all switches using global_config +# QUERY - SETUP - Upgrade all switches using global_config ################################################################################ -- name: QUERY - Upgrade all switches using global config +- name: QUERY - SETUP - Upgrade all switches using global config cisco.dcnm.dcnm_image_upgrade: state: merged config: @@ -148,9 +152,9 @@ install: false uninstall: false switches: - - ip_address: "{{ ansible_switch1 }}" - - ip_address: "{{ ansible_switch2 }}" - - ip_address: "{{ ansible_switch3 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: @@ -176,22 +180,35 @@ - result.response.upgrade[2].RETURN_CODE == 200 - result.response.validate[0].RETURN_CODE == 200 -- name: QUERY - PAUSE 15 minutes for switches to reboot - ansible.builtin.pause: - minutes: 15 +- name: QUERY - SETUP - Wait for controller response for all three switches + cisco.dcnm.dcnm_image_upgrade: + state: query + config: + switches: + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" + register: result + until: + - result.diff.issu_status[0].ipAddress == ansible_switch_1 + - result.diff.issu_status[1].ipAddress == ansible_switch_2 + - result.diff.issu_status[2].ipAddress == ansible_switch_3 + retries: 60 + delay: 5 + ignore_errors: yes ################################################################################ -# QUERY - Query and verify ISSU status +# QUERY - TEST - Verify image_policy_1 attached to all switches ################################################################################ -- name: QUERY - Query ISSU Status for three switches +- name: QUERY - TEST - Verify image_policy_1 attached to all switches cisco.dcnm.dcnm_image_upgrade: state: query config: switches: - - ip_address: "{{ ansible_switch1 }}" - - ip_address: "{{ ansible_switch2 }}" - - ip_address: "{{ ansible_switch3 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: @@ -212,9 +229,9 @@ - (result.response.stage | length) == 0 - (result.response.upgrade | length) == 0 - (result.response.validate | length) == 0 - - (result.diff.issu_status[0].ipAddress) == ansible_switch1 - - (result.diff.issu_status[1].ipAddress) == ansible_switch2 - - (result.diff.issu_status[2].ipAddress) == ansible_switch3 + - (result.diff.issu_status[0].ipAddress) == ansible_switch_1 + - (result.diff.issu_status[1].ipAddress) == ansible_switch_2 + - (result.diff.issu_status[2].ipAddress) == ansible_switch_3 - (result.diff.issu_status[0].policy) == image_policy_1 - (result.diff.issu_status[1].policy) == image_policy_1 - (result.diff.issu_status[2].policy) == image_policy_1 @@ -223,17 +240,17 @@ - (result.diff.issu_status[2].statusPercent) == 100 ################################################################################ -# QUERY - Detach policies from two switches and verify +# QUERY - TEST - Detach policies from two switches and verify ################################################################################ -- name: QUERY - Detach policies from two switches +- name: QUERY - TEST - Detach policies from two switches cisco.dcnm.dcnm_image_upgrade: state: deleted config: policy: "{{ image_policy_1 }}" switches: - - ip_address: "{{ ansible_switch1 }}" - - ip_address: "{{ ansible_switch2 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" register: result - debug: @@ -255,18 +272,17 @@ ################################################################################ -# QUERY - Query ISSU status again and verify that policy for two switches -# is set to None +# QUERY - TEST - Verify image_policy_1 removed from two switches ################################################################################ -- name: QUERY - Query ISSU Status (two switches should have no image policy) +- name: QUERY - TEST - Verify image_policy_1 removed from two switches cisco.dcnm.dcnm_image_upgrade: state: query config: switches: - - ip_address: "{{ ansible_switch1 }}" - - ip_address: "{{ ansible_switch2 }}" - - ip_address: "{{ ansible_switch3 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: @@ -287,9 +303,9 @@ - (result.response.stage | length) == 0 - (result.response.upgrade | length) == 0 - (result.response.validate | length) == 0 - - (result.diff.issu_status[0].ipAddress) == ansible_switch1 - - (result.diff.issu_status[1].ipAddress) == ansible_switch2 - - (result.diff.issu_status[2].ipAddress) == ansible_switch3 + - (result.diff.issu_status[0].ipAddress) == ansible_switch_1 + - (result.diff.issu_status[1].ipAddress) == ansible_switch_2 + - (result.diff.issu_status[2].ipAddress) == ansible_switch_3 - (result.diff.issu_status[0].policy) == "None" - (result.diff.issu_status[1].policy) == "None" - (result.diff.issu_status[2].policy) == image_policy_1 @@ -298,16 +314,16 @@ - (result.diff.issu_status[2].statusPercent) == 100 ################################################################################ -# QUERY - Detach policies from remaining switch and verify +# QUERY - TEST - Detach policies from remaining switch and verify ################################################################################ -- name: QUERY - Detach policy from remaining switch +- name: QUERY - TEST - Detach policy from remaining switch cisco.dcnm.dcnm_image_upgrade: state: deleted config: policy: "{{ image_policy_1 }}" switches: - - ip_address: "{{ ansible_switch3 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: @@ -329,18 +345,17 @@ - (result.response.validate | length) == 0 ################################################################################ -# QUERY - Query ISSU status again and verify that policy for all switches -# is set to None +# QUERY - TEST - Verify image_policy_1 removed from all switches ################################################################################ -- name: QUERY - Query ISSU Status (all switches should have no image policy) +- name: QUERY - TEST - Verify image_policy_1 removed from all switches cisco.dcnm.dcnm_image_upgrade: state: query config: switches: - - ip_address: "{{ ansible_switch1 }}" - - ip_address: "{{ ansible_switch2 }}" - - ip_address: "{{ ansible_switch3 }}" + - ip_address: "{{ ansible_switch_1 }}" + - ip_address: "{{ ansible_switch_2 }}" + - ip_address: "{{ ansible_switch_3 }}" register: result - debug: @@ -361,9 +376,9 @@ - (result.response.stage | length) == 0 - (result.response.upgrade | length) == 0 - (result.response.validate | length) == 0 - - (result.diff.issu_status[0].ipAddress) == ansible_switch1 - - (result.diff.issu_status[1].ipAddress) == ansible_switch2 - - (result.diff.issu_status[2].ipAddress) == ansible_switch3 + - (result.diff.issu_status[0].ipAddress) == ansible_switch_1 + - (result.diff.issu_status[1].ipAddress) == ansible_switch_2 + - (result.diff.issu_status[2].ipAddress) == ansible_switch_3 - (result.diff.issu_status[0].policy) == "None" - (result.diff.issu_status[1].policy) == "None" - (result.diff.issu_status[2].policy) == "None" @@ -375,7 +390,7 @@ # CLEAN-UP ################################################################################ -- name: QUERY - cleanup - Remove devices from fabric +- name: QUERY - CLEANUP - Remove devices from fabric cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: deleted \ No newline at end of file From a37a01fd363beea904304e40f7a27462a5cc3d54 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 22 Jan 2024 19:57:11 -1000 Subject: [PATCH 218/300] ImageUpgradeTask: Fix idempotence for merged state Fix an intermittent idempotence issue found by the integration tests. have.status was sometimes reported as "Not-In-Sync" which caused upgrade.nxos to be set to True, which cause unnecessary upgrade. The fix was to remove have.status from consideration. We now consider only have.reason, have.policy, and have.upgrade. --- plugins/modules/dcnm_image_upgrade.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 99216dd24..f488dd3b4 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -599,25 +599,33 @@ def _build_idempotent_want(self, want) -> None: # if the image is already validated, don't validate it again if self.have.validated == "Success": self.idempotent_want["validate"] = False + + msg = f"self.have.reason: {self.have.reason}, " + msg += f"self.have.policy: {self.have.policy}, " + msg += f"idempotent_want[policy]: {self.idempotent_want['policy']}, " + msg += f"self.have.upgrade: {self.have.upgrade}" + self.log.debug(msg) + # if the image is already upgraded, don't upgrade it again if ( - self.have.status == "In-Sync" - and self.have.reason == "Upgrade" - and self.have.policy == want["policy"] + self.have.reason == "Upgrade" + and self.have.policy == self.idempotent_want["policy"] # If upgrade is other than Success, we need to try to upgrade # again. So only change upgrade.nxos if upgrade is Success. and self.have.upgrade == "Success" ): + msg = "Set upgrade nxos to False" + self.log.debug(msg) self.idempotent_want["upgrade"]["nxos"] = False # Get relevant install options from the controller - # based on the options in our want item + # based on the options in our idempotent_want item instance = ImageInstallOptions(self.module) - instance.policy_name = want["policy"] + instance.policy_name = self.idempotent_want["policy"] instance.serial_number = self.have.serial_number instance.epld = want.get("upgrade", {}).get("epld", False) - instance.issu = want.get("upgrade", {}).get("nxos", False) + instance.issu = self.idempotent_want.get("upgrade", {}).get("nxos", False) instance.package_install = ( want.get("options", {}).get("package", {}).get("install", False) ) @@ -1306,9 +1314,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - log.config = config_file + # collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + # config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + # log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) From 5baa10d8d63637c68a473968083a8c985dfffed3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 23 Jan 2024 19:59:40 -1000 Subject: [PATCH 219/300] Move timeout and unit_test properties to ImageUpgradeCommon As a first step, in preparation for moving safe_send() to ImageUpgradeCommon, move two properties that safe_send() relies on. --- .../image_mgmt/image_upgrade_common.py | 38 ++++++++++ .../image_mgmt/install_options.py | 74 +++++++++---------- 2 files changed, 75 insertions(+), 37 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 58a67a7ff..012bcfadd 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -53,6 +53,8 @@ def __init__(self, module): self.properties["diff"] = [] self.properties["failed"] = False self.properties["response"] = [] + self.properties["timeout"] = 300 + self.properties["unit_test"] = False def _handle_response(self, response, verb): """ @@ -213,3 +215,39 @@ def failed(self, value): msg += f"failed must be a bool. Got {value}" self.module.fail_json(msg) self.properties["failed"] = value + + @property + def timeout(self): + """ + Timeout, in seconds, for retrieving responses from the controller. + Valid values: int() + Default: 300 + """ + return self.properties.get("timeout") + + @timeout.setter + def timeout(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an int(). Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["timeout"] = value + + @property + def unit_test(self): + """ + Is the class running under a unit test. + Set this to True in unit tests to speed the test up. + Default: False + """ + return self.properties.get("unit_test") + + @unit_test.setter + def unit_test(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be a bool(). Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["unit_test"] = value diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 874c80c26..5071f50ea 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -386,43 +386,43 @@ def package_install(self, value): self.module.fail_json(msg, **self.failed_result) self.properties["package_install"] = value - @property - def timeout(self): - """ - Timeout, in seconds, for retrieving install-options from the controller. - Valid values: int() - Default: 300 - """ - return self.properties.get("timeout") - - @timeout.setter - def timeout(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be an int(). Got {value}." - self.module.fail_json(msg, **self.failed_result) - self.properties["timeout"] = value - - @property - def unit_test(self): - """ - Is the class running under a unit test. - Set this to True in unit tests to speed the test up. - Default: False - """ - return self.properties.get("unit_test") - - @unit_test.setter - def unit_test(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be a bool(). Got {value}." - self.module.fail_json(msg, **self.failed_result) - self.properties["unit_test"] = value - - # Retrievable properties + # @property + # def timeout(self): + # """ + # Timeout, in seconds, for retrieving responses from the controller. + # Valid values: int() + # Default: 300 + # """ + # return self.properties.get("timeout") + + # @timeout.setter + # def timeout(self, value): + # method_name = inspect.stack()[0][3] + # if not isinstance(value, int): + # msg = f"{self.class_name}.{method_name}: " + # msg += f"{method_name} must be an int(). Got {value}." + # self.module.fail_json(msg, **self.failed_result) + # self.properties["timeout"] = value + + # @property + # def unit_test(self): + # """ + # Is the class running under a unit test. + # Set this to True in unit tests to speed the test up. + # Default: False + # """ + # return self.properties.get("unit_test") + + # @unit_test.setter + # def unit_test(self, value): + # method_name = inspect.stack()[0][3] + # if not isinstance(value, bool): + # msg = f"{self.class_name}.{method_name}: " + # msg += f"{method_name} must be a bool(). Got {value}." + # self.module.fail_json(msg, **self.failed_result) + # self.properties["unit_test"] = value + + # Getter properties @property def comp_disp(self): """ From 33bebb429d5c728bed9a05d2376a3e56d6085594 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 23 Jan 2024 20:05:41 -1000 Subject: [PATCH 220/300] Add safe_send() method to call dcnm_send with a retry and timeout mechanism We were experiencing timeouts and intermittent 500 responses when using dcnm_send() directly. Hence, adding a safe_send() method to retry dcnm_send() until we get a 200 response or until a timeout is exceeded. Will move this to ImageUpgradeCommon later. --- .../image_mgmt/image_policy_action.py | 82 ++++++++++++++----- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 7843dae54..2e1f29bbe 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -22,6 +22,7 @@ import inspect import json import logging +from time import sleep from typing import Any, Dict from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -79,12 +80,13 @@ def __init__(self, module): self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.module) self.valid_actions = {"attach", "detach", "query"} self.verb = None + self.send_interval = 5 # interval between dcnm_send retries def _init_properties(self): # self.properties is already initialized in the parent class self.properties["action"] = None - self.properties["response"] = None - self.properties["result"] = None + self.properties["response"] = {} + self.properties["result"] = {} self.properties["policy_name"] = None self.properties["query_result"] = None self.properties["serial_numbers"] = None @@ -203,6 +205,60 @@ def commit(self): msg += f"Unknown action {self.action}." self.module.fail_json(msg, **self.failed_result) + def safe_send(self, payload=None): + """ + Call dcnm_send() with retries until successful response or timeout is exceeded. + + Properties read: + self.send_interval: interval between retries (set in ImageUpgradeCommon) + self.timeout: timeout in seconds (set in ImageUpgradeCommon) + self.verb: HTTP verb (set in the calling class's commit() method) + self.path: HTTP path (set in the calling class's commit() method) + payload: + - (optionally) passed directly to this function. + - Normally only used when verb is POST or PUT. + + Properties written: + self.properties["response"]: raw response from the controller + # self.properties["response_data"]: response["DATA"] + self.properties["result"]: result from self._handle_response() method + """ + caller = inspect.stack()[1][3] + try: + timeout = self.timeout + except AttributeError: + timeout = 300 + + success = False + msg = f"{caller}: Entering safe_send loop. timeout {timeout}, send_interval {self.send_interval}" + self.log.debug(msg) + + while timeout > 0 and success is False: + if payload is None: + msg = f"{caller}: Calling dcnm_send with no payload" + self.log.debug(msg) + response = dcnm_send( + self.module, self.verb, self.path + ) + else: + msg = f"{caller}: Calling dcnm_send with payload: " + msg += f"{json.dumps(payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + response = dcnm_send( + self.module, self.verb, self.path, data=json.dumps(payload) + ) + + msg = f"{caller}: response {response}" + self.log.debug(msg) + + self.properties["response"] = response + self.properties["result"] = self._handle_response(response, self.verb) + success = self.properties["result"]["success"] + + if success is False and self.unit_test is False: + sleep(self.send_interval) + timeout -= self.send_interval + def _attach_policy(self): """ Attach policy_name to the switch(es) associated with serial_numbers @@ -225,20 +281,14 @@ def _attach_policy(self): payload: Dict[str, Any] = {} payload["mappingList"] = self.payloads - response = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(payload) - ) - result = self._handle_response(response, self.verb) + self.safe_send(payload) - if not result["success"]: + if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " msg += f"to switch {payload['ipAddr']}." self.module.fail_json(msg, **self.failed_result) - self.properties["result"] = result - self.properties["response"] = response - for payload in self.payloads: diff: Dict[str, Any] = {} diff["action"] = self.action @@ -266,14 +316,7 @@ def _detach_policy(self): query_params = ",".join(self.serial_numbers) self.path += f"?serialNumber={query_params}" - self.properties["response"] = dcnm_send(self.module, self.verb, self.path) - self.properties["result"] = self._handle_response(self.response, self.verb) - - msg = f"result: {json.dumps(self.result, indent=4)}" - self.log.debug(msg) - - msg = f"response: {json.dumps(self.response, indent=4)}" - self.log.debug(msg) + self.safe_send() if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " @@ -305,8 +348,7 @@ def _query_policy(self): self.path = self.path.replace("__POLICY_NAME__", self.policy_name) - self.properties["response"] = dcnm_send(self.module, self.verb, self.path) - self.properties["result"] = self._handle_response(self.response, self.verb) + self.safe_send() if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " From 795094cd96e73a4b097c5864593677180d9753af Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 23 Jan 2024 20:06:48 -1000 Subject: [PATCH 221/300] Add recent runtimes to integration tests --- .../targets/dcnm_image_upgrade/tests/deleted.yaml | 5 +++-- .../dcnm_image_upgrade/tests/merged_global_config.yaml | 4 ++-- .../tests/merged_override_global_config.yaml | 9 +++++---- .../targets/dcnm_image_upgrade/tests/query.yaml | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml index 8ff70c990..137f071f6 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml @@ -2,9 +2,10 @@ # RUNTIME ################################################################################ -# 30 to 40 minutes -# Some previous runtimes: +# Recent run times (MM:SS.ms): # - 32:06.27 +# - 29:10.63 +# - 30:39.32 ################################################################################ # STEPS diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml index feeddf359..1336f0c16 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml @@ -16,12 +16,12 @@ # RUNTIME ################################################################################ -# Approximately 30 minutes -# Recent run times: +# Recent run times (MM:SS.ms): # 29:49.15 # 33:40.57 # 28:58.18 # 31:41.42 +# 31:28.12 ################################################################################ # STEPS diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml index 9eb4b722d..0d86c773b 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml @@ -15,10 +15,11 @@ # RUNTIME ################################################################################ -# About 30 minutes -# Recent run times: -# 33:18.99 -# 27:36.11 +# Recent run times (MM:SS.ms): +# - 33:18.99 +# - 27:36.11 +# - 36:10.94 +# - 34:14.59 ################################################################################ # STEPS diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml index 229b408db..d00e11413 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml @@ -2,10 +2,10 @@ # RUNTIME ################################################################################ -# 30 minutes -# Some previous runtimes where: +# Recent run times (MM:SS.ms): # - 26:19.11 # - 26:32.97 +# - 28:16.01 ################################################################################ # STEPS From 56ae8fbf84ba15d843c1d6362826c1cd9baf1285 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 23 Jan 2024 20:10:53 -1000 Subject: [PATCH 222/300] Forgot to commit changes to unit test. --- .../test_image_upgrade_image_policy_action.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 2d151c9bd..35708d3e8 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -88,8 +88,8 @@ def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: instance = image_policy_action assert isinstance(instance.properties, dict) assert instance.properties.get("action") is None - assert instance.properties.get("response") is None - assert instance.properties.get("result") is None + assert instance.properties.get("response") == {} + assert instance.properties.get("result") == {} assert instance.properties.get("policy_name") is None assert instance.properties.get("query_result") is None assert instance.properties.get("serial_numbers") is None From c3e3e05e2b03f43dae4a5cfd2f3cd1fa784540e1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 23 Jan 2024 20:16:35 -1000 Subject: [PATCH 223/300] Fix PEP8 issue with inline comment --- plugins/module_utils/image_mgmt/image_policy_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 2e1f29bbe..e799c38ac 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -80,7 +80,7 @@ def __init__(self, module): self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.module) self.valid_actions = {"attach", "detach", "query"} self.verb = None - self.send_interval = 5 # interval between dcnm_send retries + self.send_interval = 5 # interval between dcnm_send retries def _init_properties(self): # self.properties is already initialized in the parent class From 83cc254114983d762e98b2b3a3bc1517513a9cad Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 24 Jan 2024 10:33:46 -1000 Subject: [PATCH 224/300] Cleanup result names Be explicit with result names. 1. ImageUpgradeTask: rename self.result to self.task_result and update related unit test. 2. ImageUpgradeCommon.failed_result: No need to instantiate ImageUpgradeTaskResult since we're only using one of its properties. 3. Move module_utils/common/result.py to ImagePolicy module. It wasn't really a good idea to try to develop a generic result class given the differences in results between the different modules. common/result.py is currently specific to ImagePolicy --- plugins/module_utils/common/result.py | 197 ------------------ .../image_mgmt/image_upgrade_common.py | 7 +- plugins/modules/dcnm_image_upgrade.py | 46 ++-- .../test_image_upgrade_image_upgrade_task.py | 2 +- 4 files changed, 27 insertions(+), 225 deletions(-) delete mode 100644 plugins/module_utils/common/result.py diff --git a/plugins/module_utils/common/result.py b/plugins/module_utils/common/result.py deleted file mode 100644 index 469692b77..000000000 --- a/plugins/module_utils/common/result.py +++ /dev/null @@ -1,197 +0,0 @@ -# -# Copyright (c) 2024 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__ = "Allen Robel" - -import inspect -import logging -from typing import Any, Dict - - -class Result: - """ - Storage for module result - - Usage: - - NOTES: - 1. Assumes deleted and merged are class instances with diff properties - that return the diff for the deleted and merged states. - 2. diff must be a dict() - 3. result.deleted, etc do not overwrite the existing value. They append - to it. So, for example: - result.deleted = {"foo": "bar"} - result.deleted = {"baz": "qux"} - print(result.deleted) - Output: [{"foo": "bar"}, {"baz": "qux"}] - 4. result.response is a list of dicts. Each dict represents a response - from the controller. - - result = Result(ansible_module) - result.deleted = deleted.diff # Appends to deleted-state changes - result.merged = merged.diff # Appends to merged-state changes - # If a class doesn't have a diff property, then just append the dict - # that represents the changes for a given state. - result.overridden = {"foo": "bar"} - etc for other states - # If you want to append a response from the controller, then do this: - result.response = response - result = result.result - - print(result) - - # output will be a dict with the following structure: - { - "changed": True, # or False - "diff": [ - { - "deleted": [], - "merged": [], - "overridden": [], - "query": [], - "replaced": [] - } - ] - "response": [] - } - """ - - def __init__(self, ansible_module): - self.class_name = self.__class__.__name__ - self.ansible_module = ansible_module - - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED Result()") - - self.states = ["deleted", "merged", "overridden", "query", "replaced"] - self._build_properties() - - def _build_properties(self): - """ - self.properties holds property values for the class - """ - self.properties: Dict[str, Any] = {} - for state in self.states: - self.properties[state] = [] - self.properties["changed"] = False - self.properties["response"] = [] - - def did_anything_change(self): - """ - return True if things have been appended to any of the state lists. - """ - for state in self.states: - if state == "query": - continue - if len(self.properties[state]) != 0: - return True - return False - - @property - def deleted(self): - """ - return changes for deleted state - """ - return self.properties["deleted"] - - @deleted.setter - def deleted(self, value): - self._verify_is_dict(value) - self.properties["deleted"].append(value) - - @property - def merged(self): - """ - return changes for merged state - """ - return self.properties["merged"] - - @merged.setter - def merged(self, value): - self._verify_is_dict(value) - self.properties["merged"].append(value) - - @property - def overridden(self): - """ - return changes for overridden state - """ - return self.properties["overridden"] - - @overridden.setter - def overridden(self, value): - self._verify_is_dict(value) - self.properties["overridden"].append(value) - - @property - def query(self): - """ - return changes for query state - """ - return self.properties["query"] - - @query.setter - def query(self, value): - self._verify_is_dict(value) - self.properties["query"].append(value) - - def _verify_is_dict(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, dict): - msg = f"{self.class_name}.{method_name}: " - msg += "value must be a dict. " - msg += f"got {type(value).__name__} for " - msg += f"value {value}" - self.ansible_module.fail_json(msg, **self.result) - - @property - def replaced(self): - """ - return changes for replaced state - """ - return self.properties["replaced"] - - @replaced.setter - def replaced(self, value): - self._verify_is_dict(value) - self.properties["replaced"].append(value) - - @property - def result(self): - """ - return a result that AnsibleModule can use - """ - result = {} - result["changed"] = self.did_anything_change() - result["diff"] = {} - for state in self.states: - result["diff"][state] = self.properties[state] - result["response"] = self.properties["response"] - return result - - @property - def response(self): - """ - return the controller response(s) - """ - return self.properties["response"] - - @response.setter - def response(self, value): - self._verify_is_dict(value) - self.properties["response"].append(value) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 012bcfadd..2d37dc5ad 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -23,7 +23,7 @@ # Using only for its failed_result property from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ - ImageUpgradeTaskResult as Result + ImageUpgradeTaskResult class ImageUpgradeCommon: @@ -161,10 +161,9 @@ def make_none(self, value): @property def failed_result(self): """ - return a result for a failed task with no changes + Return a result for a failed task with no changes """ - result = Result(self.module) - return result.failed_result + return ImageUpgradeTaskResult(None).failed_result @property def changed(self): diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index f488dd3b4..4909088e8 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -483,8 +483,8 @@ def __init__(self, module): self.want = [] self.need = [] - self.result = ImageUpgradeTaskResult(self.module) - self.result.changed = False + self.task_result = ImageUpgradeTaskResult(self.module) + self.task_result.changed = False self.switch_details = SwitchDetails(self.module) self.image_policies = ImagePolicies(self.module) @@ -526,8 +526,8 @@ def get_want(self) -> None: self.log.debug(msg) if len(self.want) == 0: - self.result.result["changed"] = False - self.module.exit_json(**self.result.result) + self.task_result.result["changed"] = False + self.module.exit_json(**self.task_result.result) def _build_idempotent_want(self, want) -> None: """ @@ -1006,9 +1006,9 @@ def _attach_or_detach_image_policy(self, action=None) -> None: self.log.debug(msg) if action == "attach": - self.result.diff_attach_policy = instance.diff_null + self.task_result.diff_attach_policy = instance.diff_null if action == "detach": - self.result.diff_detach_policy = instance.diff_null + self.task_result.diff_detach_policy = instance.diff_null return for key, value in serial_numbers_to_update.items(): @@ -1017,9 +1017,9 @@ def _attach_or_detach_image_policy(self, action=None) -> None: instance.serial_numbers = value instance.commit() if action == "attach": - self.result.response_attach_policy = copy.deepcopy(instance.response) + self.task_result.response_attach_policy = copy.deepcopy(instance.response) if action == "detach": - self.result.response_detach_policy = copy.deepcopy(instance.response) + self.task_result.response_detach_policy = copy.deepcopy(instance.response) for diff in instance.diff: msg = ( @@ -1027,9 +1027,9 @@ def _attach_or_detach_image_policy(self, action=None) -> None: ) self.log.debug(msg) if action == "attach": - self.result.diff_attach_policy = copy.deepcopy(diff) + self.task_result.diff_attach_policy = copy.deepcopy(diff) elif action == "detach": - self.result.diff_detach_policy = copy.deepcopy(diff) + self.task_result.diff_detach_policy = copy.deepcopy(diff) def _stage_images(self, serial_numbers) -> None: """ @@ -1046,9 +1046,9 @@ def _stage_images(self, serial_numbers) -> None: instance.serial_numbers = serial_numbers instance.commit() for diff in instance.diff: - self.result.diff_stage = copy.deepcopy(diff) + self.task_result.diff_stage = copy.deepcopy(diff) instance.response.pop("DATA", None) - self.result.response_stage = instance.response + self.task_result.response_stage = instance.response def _validate_images(self, serial_numbers) -> None: """ @@ -1064,9 +1064,9 @@ def _validate_images(self, serial_numbers) -> None: instance.serial_numbers = serial_numbers instance.commit() for diff in instance.diff: - self.result.diff_validate = copy.deepcopy(diff) + self.task_result.diff_validate = copy.deepcopy(diff) instance.response.pop("DATA", None) - self.result.response_validate = instance.response + self.task_result.response_validate = instance.response def _verify_install_options(self, devices) -> None: """ @@ -1200,9 +1200,9 @@ def _upgrade_images(self, devices) -> None: upgrade.devices = devices upgrade.commit() for diff in upgrade.diff: - self.result.diff_upgrade = diff + self.task_result.diff_upgrade = diff for response in upgrade.response: - self.result.response_upgrade = response + self.task_result.response_upgrade = response def handle_merged_state(self) -> None: """ @@ -1272,12 +1272,12 @@ def handle_query_state(self) -> None: instance.refresh() response = copy.deepcopy(instance.response) response.pop("DATA") - self.result.response_issu_status = copy.deepcopy(response) + self.task_result.response_issu_status = copy.deepcopy(response) for switch in self.need: instance.filter = switch.get("ip_address") if instance.filtered_data is None: continue - self.result.diff_issu_status = instance.filtered_data + self.task_result.diff_issu_status = instance.filtered_data def _failure(self, resp) -> None: """ @@ -1331,15 +1331,15 @@ def main(): elif ansible_module.params["state"] == "query": task_module.get_need_query() - task_module.result.changed = False + task_module.task_result.changed = False if len(task_module.need) == 0: - ansible_module.exit_json(**task_module.result.module_result) + ansible_module.exit_json(**task_module.task_result.module_result) if ansible_module.check_mode: - ansible_module.exit_json(**task_module.result.module_result) + ansible_module.exit_json(**task_module.task_result.module_result) if ansible_module.params["state"] in ["merged", "deleted"]: - task_module.result.changed = True + task_module.task_result.changed = True if ansible_module.params["state"] == "merged": task_module.handle_merged_state() @@ -1348,7 +1348,7 @@ def main(): elif ansible_module.params["state"] == "query": task_module.handle_query_state() - ansible_module.exit_json(**task_module.result.module_result) + ansible_module.exit_json(**task_module.task_result.module_result) if __name__ == "__main__": diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index c9eb6096d..d607f0083 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -89,7 +89,7 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: assert instance.validated == {} assert instance.want == [] assert instance.need == [] - assert instance.result.module_result == { + assert instance.task_result.module_result == { "changed": False, "diff": { "attach_policy": [], From 15af4d3e81090ba779a991289b91fdddf177385f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 24 Jan 2024 13:19:07 -1000 Subject: [PATCH 225/300] Move ImagePolicyAction.safe_send to ImageUpgradeCommon.dcnm_send_with_retry Moving to dcnm_send_with_retry to ImageUpgradeCommon so that other classes can leverage it. --- .../image_mgmt/image_policy_action.py | 64 +-------------- .../image_mgmt/image_upgrade_common.py | 78 +++++++++++++++++++ .../test_image_upgrade_image_policy_action.py | 6 +- 3 files changed, 84 insertions(+), 64 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index e799c38ac..380d9e8dd 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -22,7 +22,6 @@ import inspect import json import logging -from time import sleep from typing import Any, Dict from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -33,8 +32,6 @@ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ - dcnm_send class ImagePolicyAction(ImageUpgradeCommon): @@ -80,7 +77,6 @@ def __init__(self, module): self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.module) self.valid_actions = {"attach", "detach", "query"} self.verb = None - self.send_interval = 5 # interval between dcnm_send retries def _init_properties(self): # self.properties is already initialized in the parent class @@ -205,60 +201,6 @@ def commit(self): msg += f"Unknown action {self.action}." self.module.fail_json(msg, **self.failed_result) - def safe_send(self, payload=None): - """ - Call dcnm_send() with retries until successful response or timeout is exceeded. - - Properties read: - self.send_interval: interval between retries (set in ImageUpgradeCommon) - self.timeout: timeout in seconds (set in ImageUpgradeCommon) - self.verb: HTTP verb (set in the calling class's commit() method) - self.path: HTTP path (set in the calling class's commit() method) - payload: - - (optionally) passed directly to this function. - - Normally only used when verb is POST or PUT. - - Properties written: - self.properties["response"]: raw response from the controller - # self.properties["response_data"]: response["DATA"] - self.properties["result"]: result from self._handle_response() method - """ - caller = inspect.stack()[1][3] - try: - timeout = self.timeout - except AttributeError: - timeout = 300 - - success = False - msg = f"{caller}: Entering safe_send loop. timeout {timeout}, send_interval {self.send_interval}" - self.log.debug(msg) - - while timeout > 0 and success is False: - if payload is None: - msg = f"{caller}: Calling dcnm_send with no payload" - self.log.debug(msg) - response = dcnm_send( - self.module, self.verb, self.path - ) - else: - msg = f"{caller}: Calling dcnm_send with payload: " - msg += f"{json.dumps(payload, indent=4, sort_keys=True)}" - self.log.debug(msg) - response = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(payload) - ) - - msg = f"{caller}: response {response}" - self.log.debug(msg) - - self.properties["response"] = response - self.properties["result"] = self._handle_response(response, self.verb) - success = self.properties["result"]["success"] - - if success is False and self.unit_test is False: - sleep(self.send_interval) - timeout -= self.send_interval - def _attach_policy(self): """ Attach policy_name to the switch(es) associated with serial_numbers @@ -281,7 +223,7 @@ def _attach_policy(self): payload: Dict[str, Any] = {} payload["mappingList"] = self.payloads - self.safe_send(payload) + self.dcnm_send_with_retry(payload) if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " @@ -316,7 +258,7 @@ def _detach_policy(self): query_params = ",".join(self.serial_numbers) self.path += f"?serialNumber={query_params}" - self.safe_send() + self.dcnm_send_with_retry() if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " @@ -348,7 +290,7 @@ def _query_policy(self): self.path = self.path.replace("__POLICY_NAME__", self.policy_name) - self.safe_send() + self.dcnm_send_with_retry() if not self.result["success"]: msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 2d37dc5ad..cff1a5f9b 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -19,8 +19,13 @@ __author__ = "Allen Robel" import inspect +import json import logging +from time import sleep + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send # Using only for its failed_result property from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ ImageUpgradeTaskResult @@ -53,9 +58,64 @@ def __init__(self, module): self.properties["diff"] = [] self.properties["failed"] = False self.properties["response"] = [] + self.properties["send_interval"] = 5 self.properties["timeout"] = 300 self.properties["unit_test"] = False + def dcnm_send_with_retry(self, payload=None): + """ + Call dcnm_send() with retries until successful response or timeout is exceeded. + + Properties read: + self.send_interval: interval between retries (set in ImageUpgradeCommon) + self.timeout: timeout in seconds (set in ImageUpgradeCommon) + self.verb: HTTP verb (set in the calling class's commit() method) + self.path: HTTP path (set in the calling class's commit() method) + payload: + - (optionally) passed directly to this function. + - Normally only used when verb is POST or PUT. + + Properties written: + self.properties["response"]: raw response from the controller + # self.properties["response_data"]: response["DATA"] + self.properties["result"]: result from self._handle_response() method + """ + caller = inspect.stack()[1][3] + try: + timeout = self.timeout + except AttributeError: + timeout = 300 + + success = False + msg = f"{caller}: Entering dcnm_send_with_retry loop. timeout {timeout}, send_interval {self.send_interval}" + self.log.debug(msg) + + while timeout > 0 and success is False: + if payload is None: + msg = f"{caller}: Calling dcnm_send with no payload" + self.log.debug(msg) + response = dcnm_send( + self.module, self.verb, self.path + ) + else: + msg = f"{caller}: Calling dcnm_send with payload: " + msg += f"{json.dumps(payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + response = dcnm_send( + self.module, self.verb, self.path, data=json.dumps(payload) + ) + + msg = f"{caller}: response {response}" + self.log.debug(msg) + + self.properties["response"] = response + self.properties["result"] = self._handle_response(response, self.verb) + success = self.properties["result"]["success"] + + if success is False and self.unit_test is False: + sleep(self.send_interval) + timeout -= self.send_interval + def _handle_response(self, response, verb): """ Call the appropriate handler for response based on verb @@ -215,6 +275,24 @@ def failed(self, value): self.module.fail_json(msg) self.properties["failed"] = value + @property + def send_interval(self): + """ + Send interval, in seconds, for retrying responses from the controller. + Valid values: int() + Default: 5 + """ + return self.properties.get("send_interval") + + @send_interval.setter + def send_interval(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an int(). Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["send_interval"] = value + @property def timeout(self): """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 35708d3e8..fa71a011f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -51,7 +51,7 @@ PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" -DCNM_SEND_IMAGE_POLICY_ACTION = PATCH_IMAGE_MGMT + "image_policy_action.dcnm_send" +DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.dcnm_send" DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" @@ -415,12 +415,12 @@ def mock_dcnm_send_switch_details(*args) -> Dict[str, Any]: def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_policy_action(*args) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: return responses_image_policy_action(key) monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) monkeypatch.setattr( - DCNM_SEND_IMAGE_POLICY_ACTION, mock_dcnm_send_image_policy_action + DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_common ) monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) monkeypatch.setattr( From 7f171b5fd867c80831103c64dcf9d65162be03be Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 24 Jan 2024 15:16:35 -1000 Subject: [PATCH 226/300] ImagePolicies() - initialize response data to {}, more... Also, update some comments throughout. --- plugins/module_utils/image_mgmt/image_policies.py | 2 +- .../module_utils/image_mgmt/image_upgrade_common.py | 1 - .../targets/dcnm_image_upgrade/tests/deleted.yaml | 7 ++++--- .../dcnm_image_upgrade/tests/merged_global_config.yaml | 1 + .../tests/merged_override_global_config.yaml | 10 ++++++---- .../targets/dcnm_image_upgrade/tests/query.yaml | 7 ++++--- .../test_image_upgrade_image_policies.py | 2 +- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index fb6568d34..97966ec99 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -70,7 +70,7 @@ def _init_properties(self): # self.properties is already initialized in the parent class self.properties["all_policies"] = None self.properties["policy_name"] = None - self.properties["response_data"] = None + self.properties["response_data"] = {} self.properties["response"] = None self.properties["result"] = None diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index cff1a5f9b..a0fa2ce08 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -77,7 +77,6 @@ def dcnm_send_with_retry(self, payload=None): Properties written: self.properties["response"]: raw response from the controller - # self.properties["response_data"]: response["DATA"] self.properties["result"]: result from self._handle_response() method """ caller = inspect.stack()[1][3] diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml index 137f071f6..fa4a2de1a 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml @@ -3,9 +3,10 @@ ################################################################################ # Recent run times (MM:SS.ms): -# - 32:06.27 -# - 29:10.63 -# - 30:39.32 +# 32:06.27 +# 29:10.63 +# 30:39.32 +# 32:36.36 ################################################################################ # STEPS diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml index 1336f0c16..f913b9d6c 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml @@ -22,6 +22,7 @@ # 28:58.18 # 31:41.42 # 31:28.12 +# 29:18.47 ################################################################################ # STEPS diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml index 0d86c773b..c7e5c58f3 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml @@ -16,10 +16,12 @@ ################################################################################ # Recent run times (MM:SS.ms): -# - 33:18.99 -# - 27:36.11 -# - 36:10.94 -# - 34:14.59 +# 33:18.99 +# 27:36.11 +# 36:10.94 +# 34:14.59 +# 34:17.54 +# 30:40.84 ################################################################################ # STEPS diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml index d00e11413..fdf42977e 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml @@ -3,9 +3,10 @@ ################################################################################ # Recent run times (MM:SS.ms): -# - 26:19.11 -# - 26:32.97 -# - 28:16.01 +# 26:19.11 +# 26:32.97 +# 28:16.01 +# 38:33.19 ################################################################################ # STEPS diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index 53aaecadd..8e0e5874c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -70,7 +70,7 @@ def test_image_mgmt_image_policies_00002(image_policies) -> None: instance = image_policies assert isinstance(image_policies.properties, dict) assert instance.properties.get("policy_name") is None - assert instance.properties.get("response_data") is None + assert instance.properties.get("response_data") == {} assert instance.properties.get("response") is None assert instance.properties.get("result") is None From 4325d2ecd61a90181c9e51c705cb1675a1af55e8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 26 Jan 2024 09:31:25 -1000 Subject: [PATCH 227/300] Use dcnm_send_with_retry in more classes Use ImageUpgradeCommon().dcnm_send_with_retry() in two more classes. 1. ImageUpgrade() 2. ImagePolicyAction() - Update ImageUpgradeTask() to use ImagePolicyAction().response_current - Update unit tests for ImageUpgrade() and ImagePolicyAction - ImageUpgrade: remove result and response properties and inherit from ImageUpgradeCommon. - ImageUpgrade: move devices verification from commit() to _validate_devices() - ImageUpgradeTaskResult: minor docstring edit to fix typo - ImageUpgradeCommon: add response_current and result_current properties. - ImagePolicyAction: Remove local response and result property definitions and inherit from ImageUpgradeCommmon - ImagePolicyAction: Use response_current and result_current as appropriate - ImagePolicyAction: Add a setter for query_result property --- .../image_mgmt/image_policy_action.py | 46 +++---- .../module_utils/image_mgmt/image_upgrade.py | 84 +++++------- .../image_mgmt/image_upgrade_common.py | 121 ++++++++++++++++-- .../image_mgmt/image_upgrade_task_result.py | 2 +- plugins/modules/dcnm_image_upgrade.py | 6 +- .../test_image_upgrade_image_policy_action.py | 20 +-- .../test_image_upgrade_image_upgrade.py | 106 ++++++++------- 7 files changed, 234 insertions(+), 151 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index 380d9e8dd..ec05dd5d7 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -81,8 +81,6 @@ def __init__(self, module): def _init_properties(self): # self.properties is already initialized in the parent class self.properties["action"] = None - self.properties["response"] = {} - self.properties["result"] = {} self.properties["policy_name"] = None self.properties["query_result"] = None self.properties["serial_numbers"] = None @@ -225,7 +223,7 @@ def _attach_policy(self): payload["mappingList"] = self.payloads self.dcnm_send_with_retry(payload) - if not self.result["success"]: + if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " msg += f"to switch {payload['ipAddr']}." @@ -260,7 +258,7 @@ def _detach_policy(self): self.dcnm_send_with_retry() - if not self.result["success"]: + if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when detaching policy {self.policy_name} " msg += f"from the following device(s): {','.join(sorted(self.serial_numbers))}." @@ -292,13 +290,13 @@ def _query_policy(self): self.dcnm_send_with_retry() - if not self.result["success"]: + if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when querying image policy {self.policy_name}." self.module.fail_json(msg, **self.failed_result) - self.properties["query_result"] = self.response.get("DATA") - self.diff = self.response + self.query_result = self.response_current.get("DATA") + self.diff = self.response_current @property def diff_null(self): @@ -320,6 +318,16 @@ def query_result(self): """ return self.properties.get("query_result") + @query_result.setter + def query_result(self, value): + method_name = inspect.stack()[0][3] + if isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.query_result must be a dict. " + msg += f"Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["query_result"] = value + @property def action(self): """ @@ -334,38 +342,14 @@ def action(self): @action.setter def action(self, value): method_name = inspect.stack()[0][3] - if value not in self.valid_actions: msg = f"{self.class_name}.{method_name}: " msg += "instance.action must be one of " msg += f"{','.join(sorted(self.valid_actions))}. " msg += f"Got {value}." self.module.fail_json(msg, **self.failed_result) - self.properties["action"] = value - @property - def response(self): - """ - Return the raw response from the controller. - - Assumes that commit() has been called. - - In the case of attach, this is a list of responses. - """ - return self.properties.get("response") - - @property - def result(self): - """ - Return the raw result. - - Assumes that commit() has been called. - - In the case of attach, this is a list of results. - """ - return self.properties.get("result") - @property def policy_name(self): """ diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index c0f5c4456..59223447d 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -173,8 +173,6 @@ def _init_properties(self) -> None: self.properties["epld_upgrade"] = False self.properties["force_non_disruptive"] = False self.properties["response_data"] = [] - self.properties["result"] = [] - self.properties["response"] = [] self.properties["non_disruptive"] = False self.properties["package_install"] = False self.properties["package_uninstall"] = False @@ -192,11 +190,22 @@ def _init_properties(self) -> None: def _validate_devices(self) -> None: """ - 1. Perform any pre-upgrade validations (currently none) + 1. Perform any pre-upgrade validations + a. Verify that self.devices is set 2. Populate self.ip_addresses with the ip_address of all switches which can be upgraded. This is used in _wait_for_current_actions_to_complete """ + method_name = inspect.stack()[0][3] + + msg = f"self.devices: {json.dumps(self.devices, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if self.devices is None: + msg = f"{self.class_name}.{method_name}: " + msg += "call instance.devices before calling commit." + self.module.fail_json(msg, **self.failed_result) + for device in self.devices: self.issu_detail.filter = device.get("ip_address") self.issu_detail.refresh() @@ -444,14 +453,6 @@ def commit(self) -> None: """ method_name = inspect.stack()[0][3] - msg = f"self.devices: {json.dumps(self.devices, indent=4, sort_keys=True)}" - self.log.debug(msg) - - if self.devices is None: - msg = f"{self.class_name}.{method_name}: " - msg += "call instance.devices before calling commit." - self.module.fail_json(msg, **self.failed_result) - self._validate_devices() self._wait_for_current_actions_to_complete() @@ -470,24 +471,30 @@ def commit(self) -> None: msg = f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - response = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(self.payload) - ) - self.properties["result"] = self._handle_response(response, self.verb) - - msg = f"response: {json.dumps(response, indent=4, sort_keys=True)}" - self.log.debug(msg) + self.dcnm_send_with_retry(self.payload) - if not self.result["success"]: + if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " - msg += f"failed: {self.result}. " - msg += f"Controller response: {response}" + msg += f"failed: {self.result_current}. " + msg += f"Controller response: {self.response_current}" self.module.fail_json(msg, **self.failed_result) - response_data = response.get("DATA") + self.response_data = self.response_current.get("DATA") + + msg = f"self.result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"self.result: {json.dumps(self.result, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = ( + f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" + ) + self.log.debug(msg) + msg = f"self.response_data: {json.dumps(self.response_data, indent=4, sort_keys=True)}" + self.log.debug(msg) - self.response = copy.deepcopy(response) - self.response_data = response_data # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(self.payload) @@ -887,8 +894,6 @@ def check_timeout(self, value): self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value - # getter properties - @property def response_data(self): """ @@ -903,30 +908,3 @@ def response_data(self): @response_data.setter def response_data(self, value): self.properties["response_data"].append(value) - - @property - def result(self): - """ - Return the POST result. - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("result") - - @property - def response(self): - """ - Return the POST response from the controller - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("response") - - @response.setter - def response(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, dict): - msg = f"{self.class_name}.{method_name}: " - msg += "instance.response must be a dict." - self.module.fail_json(msg, **self.failed_result) - self.properties["response"].append(value) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index a0fa2ce08..bdc312f03 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -18,17 +18,17 @@ __metaclass__ = type __author__ = "Allen Robel" +import copy import inspect import json import logging from time import sleep - -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ - dcnm_send # Using only for its failed_result property from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ ImageUpgradeTaskResult +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send class ImageUpgradeCommon: @@ -58,6 +58,9 @@ def __init__(self, module): self.properties["diff"] = [] self.properties["failed"] = False self.properties["response"] = [] + self.properties["response_current"] = {} + self.properties["result"] = [] + self.properties["result_current"] = {} self.properties["send_interval"] = 5 self.properties["timeout"] = 300 self.properties["unit_test"] = False @@ -93,9 +96,7 @@ def dcnm_send_with_retry(self, payload=None): if payload is None: msg = f"{caller}: Calling dcnm_send with no payload" self.log.debug(msg) - response = dcnm_send( - self.module, self.verb, self.path - ) + response = dcnm_send(self.module, self.verb, self.path) else: msg = f"{caller}: Calling dcnm_send with payload: " msg += f"{json.dumps(payload, indent=4, sort_keys=True)}" @@ -104,17 +105,35 @@ def dcnm_send_with_retry(self, payload=None): self.module, self.verb, self.path, data=json.dumps(payload) ) - msg = f"{caller}: response {response}" - self.log.debug(msg) + self.response_current = copy.deepcopy(response) + self.response = copy.deepcopy(response) + + self.result_current = self._handle_response(response, self.verb) + self.result = copy.deepcopy(self.result_current) - self.properties["response"] = response - self.properties["result"] = self._handle_response(response, self.verb) - success = self.properties["result"]["success"] + success = self.result_current["success"] if success is False and self.unit_test is False: sleep(self.send_interval) timeout -= self.send_interval + msg = f"{caller}: Exiting dcnm_send_with_retry loop. success {success}. " + self.log.debug(msg) + + msg = f"{caller}: self.response_current {json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{caller}: self.response {json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{caller}: self.result_current {json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = ( + f"{caller}: self.result {json.dumps(self.result, indent=4, sort_keys=True)}" + ) + self.log.debug(msg) + def _handle_response(self, response, verb): """ Call the appropriate handler for response based on verb @@ -274,6 +293,86 @@ def failed(self, value): self.module.fail_json(msg) self.properties["failed"] = value + @property + def response_current(self): + """ + Return the current POST response from the controller + instance.commit() must be called first. + + This is a dict of the current response from the controller. + """ + return self.properties.get("response_current") + + @response_current.setter + def response_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response_current must be a dict. " + msg += f"Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["response_current"] = value + + @property + def response(self): + """ + Return the aggregated POST response from the controller + instance.commit() must be called first. + + This is a list of responses from the controller. + """ + return self.properties.get("response") + + @response.setter + def response(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response must be a dict. " + msg += f"Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["response"].append(value) + + @property + def result(self): + """ + Return the aggregated result from the controller + instance.commit() must be called first. + + This is a list of results from the controller. + """ + return self.properties.get("result") + + @result.setter + def result(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.result must be a dict. " + msg += f"Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["result"].append(value) + + @property + def result_current(self): + """ + Return the current result from the controller + instance.commit() must be called first. + + This is a dict containing the current result. + """ + return self.properties.get("result_current") + + @result_current.setter + def result_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.result_current must be a dict. " + msg += f"Got {value}." + self.module.fail_json(msg, **self.failed_result) + self.properties["result_current"] = value + @property def send_interval(self): """ diff --git a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py index ecbe1a06c..0d5e28105 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_task_result.py @@ -294,7 +294,7 @@ def response_stage(self, value): @property def response_upgrade(self): """ - Getter for diff_upgrade property + Getter for response_upgrade property """ return self.properties["response_upgrade"] diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 4909088e8..9cb147e50 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1017,9 +1017,9 @@ def _attach_or_detach_image_policy(self, action=None) -> None: instance.serial_numbers = value instance.commit() if action == "attach": - self.task_result.response_attach_policy = copy.deepcopy(instance.response) + self.task_result.response_attach_policy = copy.deepcopy(instance.response_current) if action == "detach": - self.task_result.response_detach_policy = copy.deepcopy(instance.response) + self.task_result.response_detach_policy = copy.deepcopy(instance.response_current) for diff in instance.diff: msg = ( @@ -1202,6 +1202,8 @@ def _upgrade_images(self, devices) -> None: for diff in upgrade.diff: self.task_result.diff_upgrade = diff for response in upgrade.response: + msg = f"adding response to response_upgrade: {json.dumps(response, indent=4, sort_keys=True)}" + self.log.debug(msg) self.task_result.response_upgrade = response def handle_merged_state(self) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index fa71a011f..eb4a466a6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -88,8 +88,10 @@ def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: instance = image_policy_action assert isinstance(instance.properties, dict) assert instance.properties.get("action") is None - assert instance.properties.get("response") == {} - assert instance.properties.get("result") == {} + assert instance.properties.get("response") == [] + assert instance.properties.get("response_current") == {} + assert instance.properties.get("result") == [] + assert instance.properties.get("result_current") == {} assert instance.properties.get("policy_name") is None assert instance.properties.get("query_result") is None assert instance.properties.get("serial_numbers") is None @@ -433,15 +435,15 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: instance.action = "detach" instance.commit() - assert isinstance(instance.response, dict) - assert instance.response.get("RETURN_CODE") == 200 - assert instance.response.get("METHOD") == "DELETE" - assert instance.response.get("MESSAGE") == "OK" + assert isinstance(instance.response_current, dict) + assert instance.response_current.get("RETURN_CODE") == 200 + assert instance.response_current.get("METHOD") == "DELETE" + assert instance.response_current.get("MESSAGE") == "OK" assert ( - instance.response.get("DATA") == "Successfully detach the policy from device." + instance.response_current.get("DATA") == "Successfully detach the policy from device." ) - assert instance.result.get("success") is True - assert instance.result.get("changed") is True + assert instance.result_current.get("success") is True + assert instance.result_current.get("changed") is True MATCH_00060 = "ImagePolicyAction.action: instance.action must be " diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 408fc0ab3..e18abce76 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -46,11 +46,10 @@ PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -DCNM_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.dcnm_send" +DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" - def test_image_mgmt_upgrade_00001(image_upgrade) -> None: """ Function @@ -153,8 +152,9 @@ def test_image_mgmt_upgrade_00005(image_upgrade) -> None: """ instance = image_upgrade - match = "ImageUpgrade.commit: call instance.devices before calling commit." + match = "ImageUpgrade._validate_devices: call instance.devices before calling commit." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -189,7 +189,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): @@ -200,7 +200,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -229,6 +229,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): ] match = r"ImageUpgrade._build_payload_issu_upgrade: upgrade.nxos must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -269,7 +270,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): @@ -280,7 +281,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -309,6 +310,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] + instance.unit_test = True instance.commit() assert instance.payload == payloads_image_upgrade(key) @@ -347,7 +349,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): @@ -358,7 +360,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -387,7 +389,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - + instance.unit_test = True instance.commit() assert instance.payload == payloads_image_upgrade(key) @@ -424,7 +426,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): @@ -435,7 +437,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -469,6 +471,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += "options.nxos.mode must be one of " match += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " match += "Got FOO." + instance.unit_test = True with pytest.raises(AnsibleFailJson, match=match): instance.commit() @@ -501,7 +504,7 @@ def test_image_mgmt_upgrade_00022(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00022a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -518,7 +521,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -548,6 +551,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] + instance.unit_test = True instance.commit() assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False assert instance.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is False @@ -582,7 +586,7 @@ def test_image_mgmt_upgrade_00023(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00023a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -599,7 +603,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -628,6 +632,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + instance.unit_test = True instance.commit() assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False assert instance.payload["issuUpgradeOptions1"]["forceNonDisruptive"] is True @@ -660,7 +665,7 @@ def test_image_mgmt_upgrade_00024(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00024a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -677,7 +682,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -709,6 +714,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade._build_payload_issu_options_2: " match += r"options.nxos.bios_force must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -738,7 +744,7 @@ def test_image_mgmt_upgrade_00025(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00025a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -755,7 +761,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -790,6 +796,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += "all other upgrade options, e.g. upgrade.nxos, " match += "must be False." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -818,7 +825,7 @@ def test_image_mgmt_upgrade_00026(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00026a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -835,7 +842,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -868,6 +875,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += "options.epld.module must either be 'ALL' " match += r"or an integer. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -896,7 +904,7 @@ def test_image_mgmt_upgrade_00027(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00027a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -913,7 +921,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -945,6 +953,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade._build_payload_epld: " match += r"options.epld.golden must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -973,7 +982,7 @@ def test_image_mgmt_upgrade_00028(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00028a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -990,7 +999,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1022,6 +1031,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade._build_payload_reboot: " match += r"reboot must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -1051,7 +1061,7 @@ def test_image_mgmt_upgrade_00029(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00029a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -1068,7 +1078,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1100,6 +1110,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade._build_payload_reboot_options: " match += r"options.reboot.config_reload must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -1129,7 +1140,7 @@ def test_image_mgmt_upgrade_00030(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00030a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -1146,7 +1157,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1178,6 +1189,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade._build_payload_reboot_options: " match += r"options.reboot.write_erase must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -1212,7 +1224,7 @@ def test_image_mgmt_upgrade_00031(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00031a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -1229,7 +1241,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1261,6 +1273,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageUpgrade._build_payload_package: " match += r"options.package.uninstall must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -1279,7 +1292,7 @@ def test_image_mgmt_upgrade_00032(monkeypatch, image_upgrade) -> None: device has not yet been upgraded to the desired version - Methods called by commit that wait for current actions, and image upgrade, to complete are mocked to do nothing - - ImageUpgrade response (mock_dcnm_send_image_upgrade) is set + - ImageUpgrade response (mock_dcnm_send_image_upgrade_commit) is set to return RESULT_CODE 500 with MESSAGE "Internal Server Error" Expected results: @@ -1291,7 +1304,7 @@ def test_image_mgmt_upgrade_00032(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00032a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -1308,7 +1321,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1346,6 +1359,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += "imagemanagement/rest/imageupgrade/upgrade-image', " match += r"'RETURN_CODE': 500\}" with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -1375,7 +1389,7 @@ def test_image_mgmt_upgrade_00033(monkeypatch, image_upgrade) -> None: key = "test_image_mgmt_upgrade_00033a" - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -1392,7 +1406,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1422,6 +1436,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match = "ImageInstallOptions.epld: " match += r"epld must be a boolean value. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): + instance.unit_test = True instance.commit() @@ -1475,7 +1490,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): @@ -1486,7 +1501,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1515,6 +1530,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + instance.unit_test = True instance.commit() assert instance.response_data == [121] @@ -1548,7 +1564,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): @@ -1559,7 +1575,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1588,8 +1604,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] + instance.unit_test = True instance.commit() - assert instance.result == {"success": True, "changed": True} + assert instance.result_current == {"success": True, "changed": True} def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: @@ -1621,7 +1638,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): @@ -1632,7 +1649,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE, mock_dcnm_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1661,6 +1678,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] + instance.unit_test = True instance.commit() print(f"instance.response: {instance.response}") assert isinstance(instance.response, list) From 0eccb5995aee45cb8cc6409755935272512375f0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 26 Jan 2024 09:35:36 -1000 Subject: [PATCH 228/300] Fix PEP8 error (need two black lines) --- .../dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index e18abce76..1ff26c30d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -50,6 +50,7 @@ DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" + def test_image_mgmt_upgrade_00001(image_upgrade) -> None: """ Function From 8fe798e9f3f82b3a8722e585bb578d5aacabb403 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 26 Jan 2024 09:52:54 -1000 Subject: [PATCH 229/300] Remove unused dcnm_send import --- plugins/module_utils/image_mgmt/image_upgrade.py | 2 -- plugins/modules/dcnm_image_upgrade.py | 10 ++++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 59223447d..2bdf63116 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -33,8 +33,6 @@ ImageInstallOptions from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsByIpAddress -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ - dcnm_send class ImageUpgrade(ImageUpgradeCommon): diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 9cb147e50..30358ca22 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -436,8 +436,6 @@ SwitchDetails from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsByIpAddress -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ - dcnm_send class ImageUpgradeTask(ImageUpgradeCommon): @@ -1017,9 +1015,13 @@ def _attach_or_detach_image_policy(self, action=None) -> None: instance.serial_numbers = value instance.commit() if action == "attach": - self.task_result.response_attach_policy = copy.deepcopy(instance.response_current) + self.task_result.response_attach_policy = copy.deepcopy( + instance.response_current + ) if action == "detach": - self.task_result.response_detach_policy = copy.deepcopy(instance.response_current) + self.task_result.response_detach_policy = copy.deepcopy( + instance.response_current + ) for diff in instance.diff: msg = ( From a258131c1969dfd95399d4ea288d71e76af29836 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 28 Jan 2024 08:16:58 -1000 Subject: [PATCH 230/300] Add class RestSend(), more... Replacing ImageUpgradeCommon.dcnm_send_with_retry() with RestSend() class. The problem with the former has to do with pytest patching. For some tests, we need to patch dcnm_send() to mock different responses for each of the subclasses of ImageUpgradeCommon. Since the patch path was identical, there was no way to do this. The solution is to move this to a separate class (RestSend) which is then imported into subclasses of ImageUpgradeCommon. This provides the different patch paths that we need to provide appropriate mock responses to each subclass. Other changes: 1. The above required extensive changes for unit tests of ImageUpgrade (the first subclass we've modified to use RestSend). Other subclasses will be similarly modified later. 2. Run test_image_upgrade_image_policy_action.py through black/isort 3. InstallOptions: Move validation of refresh parameters out of refresh() and into a separate method. Modify unit tests appropriately. 4. InstallOptions: Fix use of self.result, self.result_current, self.response, self.response_current 5. InstallOptions: Remove timeout and unit_test properties (these are moved to RestSend). Remove response and result properties (these are inherited from ImageUpgradeCommon). Fix raw_response property to alias response_current. 6. ImageUprade: Use RestSend. 7. ImageUpgradeCommon: Added a lot of debugging to dcnm_send_with_retry() --- plugins/module_utils/common/mock_rest_send.py | 397 ++++++++++++++++++ .../image_mgmt/image_policy_action.py | 6 +- .../module_utils/image_mgmt/image_upgrade.py | 50 +-- .../image_mgmt/image_upgrade_common.py | 30 +- .../image_mgmt/install_options.py | 104 ++--- plugins/module_utils/image_mgmt/rest_send.py | 375 +++++++++++++++++ .../image_upgrade_payloads_ImageUpgrade.json | 2 +- ...est_image_upgrade_image_install_options.py | 36 +- .../test_image_upgrade_image_policy_action.py | 3 +- .../test_image_upgrade_image_upgrade.py | 262 +++++++----- 10 files changed, 1028 insertions(+), 237 deletions(-) create mode 100644 plugins/module_utils/common/mock_rest_send.py create mode 100644 plugins/module_utils/image_mgmt/rest_send.py diff --git a/plugins/module_utils/common/mock_rest_send.py b/plugins/module_utils/common/mock_rest_send.py new file mode 100644 index 000000000..8831fdb74 --- /dev/null +++ b/plugins/module_utils/common/mock_rest_send.py @@ -0,0 +1,397 @@ +# +# Copyright (c) 2024 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__ = "Allen Robel" + +import copy +import inspect +import json +import logging +from time import sleep + +# Using only for its failed_result property +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ + ImageUpgradeTaskResult +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.fixture import \ + load_fixture + + +class MockRestSend: + """ + Send REST requests to the controller with retries, and handle responses. + + Usage (where ansible_module is an instance of AnsibleModule): + + send_rest = RestSend(ansible_module) + send_rest.path = "/rest/top-down/fabrics" + send_rest.verb = "GET" + send_rest.commit() + + response = send_rest.response + result = send_rest.result + """ + + def __init__(self, ansible_module): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED SendRest()" + self.log.debug(msg) + + self.ansible_module = ansible_module + self.params = ansible_module.params + + self.properties = {} + self.properties["response"] = [] + self.properties["response_current"] = {} + self.properties["result"] = [] + self.properties["result_current"] = {} + self.properties["send_interval"] = 5 + self.properties["timeout"] = 300 + self.properties["unit_test"] = False + self.properties["verb"] = None + self.properties["path"] = None + self.properties["key"] = None + self.properties["file"] = None + self.properties["payload"] = None + + def _verify_commit_parameters(self): + if self.verb is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "verb must be set before calling commit()." + self.ansible_module.fail_json(msg, **self.failed_result) + if self.path is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "path must be set before calling commit()." + self.ansible_module.fail_json(msg, **self.failed_result) + + def commit(self): + """ + Call dcnm_send() with retries until successful response or timeout is exceeded. + + Properties read: + self.send_interval: interval between retries (set in ImageUpgradeCommon) + self.timeout: timeout in seconds (set in ImageUpgradeCommon) + self.verb: HTTP verb e.g. GET, POST, PUT, DELETE + self.path: HTTP path e.g. http://controller_ip/path/to/endpoint + self.payload: Optional HTTP payload + + Properties written: + self.properties["response"]: raw response from the controller + self.properties["result"]: result from self._handle_response() method + """ + caller = inspect.stack()[1][3] + + self._verify_commit_parameters() + try: + timeout = self.timeout + except AttributeError: + timeout = 300 + + success = False + msg = f"{caller}: Entering commit loop. " + msg += f"timeout {timeout}, send_interval {self.send_interval}" + self.log.debug(msg) + msg = f"verb {self.verb}, path {self.path}," + self.log.debug(msg) + msg = f"payload {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + while timeout > 0 and success is False: + + if self.payload is None: + msg = f"{caller}: Calling load_fixture: file {self.file}, key {self.key} verb {self.verb}, path {self.path}" + self.log.debug(msg) + response = load_fixture(self.file).get(self.key) + else: + msg = f"{caller}: Calling load_fixture: " + msg += f"file {self.file}, key {self.key} " + msg += f"verb {self.verb}, path {self.path}, " + msg += f"payload {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + response = load_fixture(self.file).get(self.key) + + self.response_current = copy.deepcopy(response) + self.result_current = self._handle_response(response) + + success = self.result_current["success"] + + if success is False and self.unit_test is False: + sleep(self.send_interval) + timeout -= self.send_interval + + self.response = copy.deepcopy(response) + self.result = copy.deepcopy(self.result_current) + + msg = f"{caller}: Exiting commit while loop. success {success}. verb {self.verb}, path {self.path}." + self.log.debug(msg) + + msg = f"{caller}: self.response_current {json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{caller}: self.response {json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{caller}: self.result_current {json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = ( + f"{caller}: self.result {json.dumps(self.result, indent=4, sort_keys=True)}" + ) + self.log.debug(msg) + + def _handle_response(self, response): + """ + Call the appropriate handler for response based on verb + """ + if self.verb == "GET": + return self._handle_get_response(response) + if self.verb in {"POST", "PUT", "DELETE"}: + return self._handle_post_put_delete_response(response) + return self._handle_unknown_request_verbs(response) + + def _handle_unknown_request_verbs(self, response): + method_name = inspect.stack()[0][3] + + msg = f"{self.class_name}.{method_name}: " + msg += f"Unknown request verb ({self.verb}) for response {response}." + self.ansible_module.fail_json(msg) + + def _handle_get_response(self, response): + """ + Caller: + - self._handle_response() + Handle controller responses to GET requests + Returns: dict() with the following keys: + - found: + - False, if request error was "Not found" and RETURN_CODE == 404 + - True otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + success_return_codes = {200, 404} + if ( + response.get("RETURN_CODE") == 404 + and response.get("MESSAGE") == "Not Found" + ): + result["found"] = False + result["success"] = True + return result + if ( + response.get("RETURN_CODE") not in success_return_codes + or response.get("MESSAGE") != "OK" + ): + result["found"] = False + result["success"] = False + return result + result["found"] = True + result["success"] = True + return result + + def _handle_post_put_delete_response(self, response): + """ + Caller: + - self.self._handle_response() + + Handle POST, PUT responses from the controller. + + Returns: dict() with the following keys: + - changed: + - True if changes were made to by the controller + - False otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + if response.get("ERROR") is not None: + result["success"] = False + result["changed"] = False + return result + if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: + result["success"] = False + result["changed"] = False + return result + result["success"] = True + result["changed"] = True + return result + + @property + def failed_result(self): + """ + Return a result for a failed task with no changes + """ + return ImageUpgradeTaskResult(None).failed_result + + @property + def file(self): + """ + JSON file containing the simulated response. + """ + return self.properties.get("file") + + @file.setter + def file(self, value): + self.properties["file"] = value + + @property + def key(self): + """ + key within file from which to pull simulated response. + """ + return self.properties.get("key") + + @key.setter + def key(self, value): + self.properties["key"] = value + + @property + def response_current(self): + """ + Return the current POST response from the controller + instance.commit() must be called first. + + This is a dict of the current response from the controller. + """ + return self.properties.get("response_current") + + @response_current.setter + def response_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response_current must be a dict. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["response_current"] = value + + @property + def response(self): + """ + Return the aggregated POST response from the controller + instance.commit() must be called first. + + This is a list of responses from the controller. + """ + return self.properties.get("response") + + @response.setter + def response(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response must be a dict. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["response"].append(value) + + @property + def result(self): + """ + Return the aggregated result from the controller + instance.commit() must be called first. + + This is a list of results from the controller. + """ + return self.properties.get("result") + + @result.setter + def result(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.result must be a dict. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["result"].append(value) + + @property + def result_current(self): + """ + Return the current result from the controller + instance.commit() must be called first. + + This is a dict containing the current result. + """ + return self.properties.get("result_current") + + @result_current.setter + def result_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.result_current must be a dict. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["result_current"] = value + + @property + def send_interval(self): + """ + Send interval, in seconds, for retrying responses from the controller. + Valid values: int() + Default: 5 + """ + return self.properties.get("send_interval") + + @send_interval.setter + def send_interval(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an int(). Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["send_interval"] = value + + @property + def timeout(self): + """ + Timeout, in seconds, for retrieving responses from the controller. + Valid values: int() + Default: 300 + """ + return self.properties.get("timeout") + + @timeout.setter + def timeout(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an int(). Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["timeout"] = value + + @property + def unit_test(self): + """ + Is the class running under a unit test. + Set this to True in unit tests to speed the test up. + Default: False + """ + return self.properties.get("unit_test") + + @unit_test.setter + def unit_test(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be a bool(). Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["unit_test"] = value diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_mgmt/image_policy_action.py index ec05dd5d7..3702f649e 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_mgmt/image_policy_action.py @@ -221,7 +221,7 @@ def _attach_policy(self): payload: Dict[str, Any] = {} payload["mappingList"] = self.payloads - self.dcnm_send_with_retry(payload) + self.dcnm_send_with_retry(self.verb, self.path, payload) if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " @@ -256,7 +256,7 @@ def _detach_policy(self): query_params = ",".join(self.serial_numbers) self.path += f"?serialNumber={query_params}" - self.dcnm_send_with_retry() + self.dcnm_send_with_retry(self.verb, self.path) if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " @@ -288,7 +288,7 @@ def _query_policy(self): self.path = self.path.replace("__POLICY_NAME__", self.policy_name) - self.dcnm_send_with_retry() + self.dcnm_send_with_retry(self.verb, self.path) if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 2bdf63116..6568c5dc7 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -31,6 +31,8 @@ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ ImageInstallOptions +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsByIpAddress @@ -140,8 +142,8 @@ def __init__(self, module): self.ipv4_done = set() self.ipv4_todo = set() self.payload: Dict[str, Any] = {} - self.path = None - self.verb = None + self.path = self.endpoints.image_upgrade.get("path") + self.verb = self.endpoints.image_upgrade.get("verb") self._init_properties() self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) @@ -224,6 +226,9 @@ def _build_payload(self, device) -> None: """ # issu_detail.refresh() has already been called in _validate_devices() # so no need to call it here. + msg = f"ENTERED _build_payload: device {device}" + self.log.debug(msg) + self.issu_detail.filter = device.get("ip_address") self.install_options.serial_number = self.issu_detail.serial_number @@ -257,6 +262,9 @@ def _build_payload(self, device) -> None: self._build_payload_reboot_options(device) self._build_payload_package(device) + msg = f"EXITING _build_payload: payload {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + def _build_payload_issu_upgrade(self, device) -> None: """ Build the issuUpgrade portion of the payload. @@ -454,11 +462,9 @@ def commit(self) -> None: self._validate_devices() self._wait_for_current_actions_to_complete() - self.path: str = self.endpoints.image_upgrade.get("path") - self.verb: str = self.endpoints.image_upgrade.get("verb") - - msg = f"self.verb {self.verb}, self.path: {self.path}" - self.log.debug(msg) + self.rest_send = RestSend(self.module) + self.rest_send.verb = self.verb + self.rest_send.path = self.path for device in self.devices: msg = f"device: {json.dumps(device, indent=4, sort_keys=True)}" @@ -466,32 +472,22 @@ def commit(self) -> None: self._build_payload(device) - msg = f"payload : {json.dumps(self.payload, indent=4, sort_keys=True)}" + msg = f"calling rest_send: " + msg += f"verb {self.verb}, path: {self.path} " + msg += f"payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - self.dcnm_send_with_retry(self.payload) + self.rest_send.payload = self.payload + self.rest_send.commit() - if not self.result_current["success"]: + if not self.rest_send.result_current["success"]: msg = f"{self.class_name}.{method_name}: " - msg += f"failed: {self.result_current}. " - msg += f"Controller response: {self.response_current}" + msg += f"failed: {self.rest_send.result_current}. " + msg += f"Controller response: {self.rest_send.response_current}" self.module.fail_json(msg, **self.failed_result) - self.response_data = self.response_current.get("DATA") - - msg = f"self.result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}" - self.log.debug(msg) - msg = f"self.result: {json.dumps(self.result, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" - self.log.debug(msg) - msg = ( - f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" - ) - self.log.debug(msg) - msg = f"self.response_data: {json.dumps(self.response_data, indent=4, sort_keys=True)}" - self.log.debug(msg) + self.response_data = self.rest_send.response_current.get("DATA") + self.response = self.rest_send.response_current # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(self.payload) diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index bdc312f03..7878e02e9 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -65,15 +65,15 @@ def __init__(self, module): self.properties["timeout"] = 300 self.properties["unit_test"] = False - def dcnm_send_with_retry(self, payload=None): + def dcnm_send_with_retry(self, verb: str, path: str, payload=None): """ Call dcnm_send() with retries until successful response or timeout is exceeded. Properties read: self.send_interval: interval between retries (set in ImageUpgradeCommon) self.timeout: timeout in seconds (set in ImageUpgradeCommon) - self.verb: HTTP verb (set in the calling class's commit() method) - self.path: HTTP path (set in the calling class's commit() method) + verb: HTTP verb (set in the calling class's commit() method) + path: HTTP path (set in the calling class's commit() method) payload: - (optionally) passed directly to this function. - Normally only used when verb is POST or PUT. @@ -89,27 +89,24 @@ def dcnm_send_with_retry(self, payload=None): timeout = 300 success = False - msg = f"{caller}: Entering dcnm_send_with_retry loop. timeout {timeout}, send_interval {self.send_interval}" + msg = f"{caller}: Entering dcnm_send_with_retry loop. timeout {timeout}, send_interval {self.send_interval}, verb {verb}, path {path}" self.log.debug(msg) while timeout > 0 and success is False: if payload is None: - msg = f"{caller}: Calling dcnm_send with no payload" + msg = f"{caller}: Calling dcnm_send: verb {verb}, path {path}" self.log.debug(msg) - response = dcnm_send(self.module, self.verb, self.path) + response = dcnm_send(self.module, verb, path) else: - msg = f"{caller}: Calling dcnm_send with payload: " + msg = ( + f"{caller}: Calling dcnm_send: verb {verb}, path {path}, payload: " + ) msg += f"{json.dumps(payload, indent=4, sort_keys=True)}" self.log.debug(msg) - response = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(payload) - ) + response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) self.response_current = copy.deepcopy(response) - self.response = copy.deepcopy(response) - - self.result_current = self._handle_response(response, self.verb) - self.result = copy.deepcopy(self.result_current) + self.result_current = self._handle_response(response, verb) success = self.result_current["success"] @@ -117,7 +114,10 @@ def dcnm_send_with_retry(self, payload=None): sleep(self.send_interval) timeout -= self.send_interval - msg = f"{caller}: Exiting dcnm_send_with_retry loop. success {success}. " + self.response = copy.deepcopy(response) + self.result = copy.deepcopy(self.result_current) + + msg = f"{caller}: Exiting dcnm_send_with_retry loop. success {success}. verb {verb}, path {path}." self.log.debug(msg) msg = f"{caller}: self.response_current {json.dumps(self.response_current, indent=4, sort_keys=True)}" diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_mgmt/install_options.py index 5071f50ea..cd874caeb 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_mgmt/install_options.py @@ -18,6 +18,7 @@ __metaclass__ = type __author__ = "Allen Robel" +import copy import inspect import json import logging @@ -150,6 +151,7 @@ def __init__(self, module) -> None: self.path = self.endpoints.install_options.get("path") self.verb = self.endpoints.install_options.get("verb") + self.payload: Dict[str, Any] = {} self.compatibility_status = {} @@ -163,19 +165,18 @@ def _init_properties(self): self.properties["issu"] = True self.properties["package_install"] = False self.properties["policy_name"] = None - self.properties["response"] = None self.properties["response_data"] = None - self.properties["result"] = {} self.properties["serial_number"] = None self.properties["timeout"] = 300 self.properties["unit_test"] = False - def refresh(self) -> None: + def _validate_refresh_parameters(self) -> None: """ - Refresh self.response_data with current install-options from the controller + Ensure parameters are set correctly for a refresh() call. + + fail_json if not. """ method_name = inspect.stack()[0][3] - if self.policy_name is None: msg = f"{self.class_name}.{method_name}: " msg += "instance.policy_name must be set before " @@ -188,6 +189,14 @@ def refresh(self) -> None: msg += "calling refresh()" self.module.fail_json(msg, **self.failed_result) + def refresh(self) -> None: + """ + Refresh self.response_data with current install-options from the controller + """ + method_name = inspect.stack()[0][3] + + self._validate_refresh_parameters() + msg = f"self.epld {self.epld}, " msg += f"self.issu {self.issu}, " msg += f"self.package_install {self.package_install}" @@ -213,37 +222,29 @@ def refresh(self) -> None: timeout = self.timeout sleep_time = 5 - self.result["success"] = False + self.result_current["success"] = False - msg = f"Entering dcnm_send loop. timeout {timeout}, sleep_time {sleep_time}" - self.log.debug(msg) - - while timeout > 0 and self.result.get("success") is False: - msg = "Calling dcnm_send with payload: " + while timeout > 0 and self.result_current.get("success") is False: + msg = f"Calling dcnm_send: verb {self.verb} path {self.path} payload: " msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - self.properties["response"] = dcnm_send( + response = dcnm_send( self.module, self.verb, self.path, data=json.dumps(self.payload) ) - msg = "Calling dcnm_send DONE" - self.log.debug(msg) + self.properties["response_data"] = response.get("DATA", {}) + self.result_current = self._handle_response(response, self.verb) + self.response_current = copy.deepcopy(response) - self.properties["response_data"] = self.response.get("DATA", {}) - self.properties["result"] = self._handle_response(self.response, self.verb) - - msg = f"self.response {self.response}" - self.log.debug(msg) - - if self.result.get("success") is False and self.unit_test is False: + if self.result_current.get("success") is False and self.unit_test is False: time.sleep(sleep_time) timeout -= sleep_time - if self.result["success"] is False: + if self.result_current["success"] is False: msg = f"{self.class_name}.{method_name}: " msg += "Bad result when retrieving install-options from " - msg += f"the controller. Controller response: {self.response}. " + msg += f"the controller. Controller response: {self.response_current}. " if self.response_data.get("error", None) is None: self.module.fail_json(msg, **self.failed_result) if "does not have package to continue" in self.response_data.get( @@ -254,6 +255,7 @@ def refresh(self) -> None: msg += f"True in the playbook for device {self.serial_number}." self.module.fail_json(msg, **self.failed_result) + self.response = copy.deepcopy(self.response_current) if self.response_data.get("compatibilityStatusList") is None: self.compatibility_status = {} else: @@ -386,42 +388,6 @@ def package_install(self, value): self.module.fail_json(msg, **self.failed_result) self.properties["package_install"] = value - # @property - # def timeout(self): - # """ - # Timeout, in seconds, for retrieving responses from the controller. - # Valid values: int() - # Default: 300 - # """ - # return self.properties.get("timeout") - - # @timeout.setter - # def timeout(self, value): - # method_name = inspect.stack()[0][3] - # if not isinstance(value, int): - # msg = f"{self.class_name}.{method_name}: " - # msg += f"{method_name} must be an int(). Got {value}." - # self.module.fail_json(msg, **self.failed_result) - # self.properties["timeout"] = value - - # @property - # def unit_test(self): - # """ - # Is the class running under a unit test. - # Set this to True in unit tests to speed the test up. - # Default: False - # """ - # return self.properties.get("unit_test") - - # @unit_test.setter - # def unit_test(self, value): - # method_name = inspect.stack()[0][3] - # if not isinstance(value, bool): - # msg = f"{self.class_name}.{method_name}: " - # msg += f"{method_name} must be a bool(). Got {value}." - # self.module.fail_json(msg, **self.failed_result) - # self.properties["unit_test"] = value - # Getter properties @property def comp_disp(self): @@ -502,22 +468,6 @@ def response_data(self) -> Dict[str, Any]: """ return self.properties.get("response_data", {}) - @property - def response(self) -> Dict[str, Any]: - """ - Return the controller response. - Return empty dict otherwise - """ - return self.properties.get("response", {}) - - @property - def result(self): - """ - Return the query result. - Return empty dict otherwise - """ - return self.properties.get("result", {}) - @property def os_type(self): """ @@ -556,9 +506,9 @@ def raw_data(self): def raw_response(self): """ Return the raw response, if it exists. - Alias for self.response + Alias for self.response_current """ - return self.response + return self.response_current @property def rep_status(self): diff --git a/plugins/module_utils/image_mgmt/rest_send.py b/plugins/module_utils/image_mgmt/rest_send.py new file mode 100644 index 000000000..0ddc464c0 --- /dev/null +++ b/plugins/module_utils/image_mgmt/rest_send.py @@ -0,0 +1,375 @@ +# +# Copyright (c) 2024 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__ = "Allen Robel" + +import copy +import inspect +import json +import logging +from time import sleep + +# Using only for its failed_result property +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ + ImageUpgradeTaskResult +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send + + +class RestSend: + """ + Send REST requests to the controller with retries, and handle responses. + + Usage (where ansible_module is an instance of AnsibleModule): + + send_rest = RestSend(ansible_module) + send_rest.path = "/rest/top-down/fabrics" + send_rest.verb = "GET" + send_rest.commit() + + response = send_rest.response + result = send_rest.result + """ + + def __init__(self, ansible_module): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED SendRest()" + self.log.debug(msg) + + self.ansible_module = ansible_module + self.params = ansible_module.params + + self.properties = {} + self.properties["response"] = [] + self.properties["response_current"] = {} + self.properties["result"] = [] + self.properties["result_current"] = {} + self.properties["send_interval"] = 5 + self.properties["timeout"] = 300 + self.properties["unit_test"] = False + self.properties["verb"] = None + self.properties["path"] = None + self.properties["payload"] = None + + def _verify_commit_parameters(self): + if self.verb is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "verb must be set before calling commit()." + self.ansible_module.fail_json(msg, **self.failed_result) + if self.path is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "path must be set before calling commit()." + self.ansible_module.fail_json(msg, **self.failed_result) + + def commit(self): + """ + Call dcnm_send() with retries until successful response or timeout is exceeded. + + Properties read: + self.send_interval: interval between retries (set in ImageUpgradeCommon) + self.timeout: timeout in seconds (set in ImageUpgradeCommon) + self.verb: HTTP verb e.g. GET, POST, PUT, DELETE + self.path: HTTP path e.g. http://controller_ip/path/to/endpoint + self.payload: Optional HTTP payload + + Properties written: + self.properties["response"]: raw response from the controller + self.properties["result"]: result from self._handle_response() method + """ + caller = inspect.stack()[1][3] + + self._verify_commit_parameters() + try: + timeout = self.timeout + except AttributeError: + timeout = 300 + + success = False + msg = f"{caller}: Entering commit loop. " + msg += f"timeout {timeout}, send_interval {self.send_interval}" + self.log.debug(msg) + msg = f"verb {self.verb}, path {self.path}," + self.log.debug(msg) + msg = f"payload {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + while timeout > 0 and success is False: + if self.payload is None: + msg = f"{caller}: Calling dcnm_send: verb {self.verb}, path {self.path}" + self.log.debug(msg) + response = dcnm_send(self.ansible_module, self.verb, self.path) + else: + msg = f"{caller}: Calling dcnm_send: verb {self.verb}, path {self.path}, payload: " + msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + response = dcnm_send( + self.ansible_module, + self.verb, + self.path, + data=json.dumps(self.payload), + ) + + self.response_current = copy.deepcopy(response) + self.result_current = self._handle_response(response) + + success = self.result_current["success"] + + if success is False and self.unit_test is False: + sleep(self.send_interval) + timeout -= self.send_interval + + self.response = copy.deepcopy(response) + self.result = copy.deepcopy(self.result_current) + + msg = f"{caller}: Exiting dcnm_send_with_retry loop. success {success}. verb {self.verb}, path {self.path}." + self.log.debug(msg) + + msg = f"{caller}: self.response_current {json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{caller}: self.response {json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{caller}: self.result_current {json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = ( + f"{caller}: self.result {json.dumps(self.result, indent=4, sort_keys=True)}" + ) + self.log.debug(msg) + + def _handle_response(self, response): + """ + Call the appropriate handler for response based on verb + """ + if self.verb == "GET": + return self._handle_get_response(response) + if self.verb in {"POST", "PUT", "DELETE"}: + return self._handle_post_put_delete_response(response) + return self._handle_unknown_request_verbs(response) + + def _handle_unknown_request_verbs(self, response): + method_name = inspect.stack()[0][3] + + msg = f"{self.class_name}.{method_name}: " + msg += f"Unknown request verb ({self.verb}) for response {response}." + self.ansible_module.fail_json(msg) + + def _handle_get_response(self, response): + """ + Caller: + - self._handle_response() + Handle controller responses to GET requests + Returns: dict() with the following keys: + - found: + - False, if request error was "Not found" and RETURN_CODE == 404 + - True otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + success_return_codes = {200, 404} + if ( + response.get("RETURN_CODE") == 404 + and response.get("MESSAGE") == "Not Found" + ): + result["found"] = False + result["success"] = True + return result + if ( + response.get("RETURN_CODE") not in success_return_codes + or response.get("MESSAGE") != "OK" + ): + result["found"] = False + result["success"] = False + return result + result["found"] = True + result["success"] = True + return result + + def _handle_post_put_delete_response(self, response): + """ + Caller: + - self.self._handle_response() + + Handle POST, PUT responses from the controller. + + Returns: dict() with the following keys: + - changed: + - True if changes were made to by the controller + - False otherwise + - success: + - False if RETURN_CODE != 200 or MESSAGE != "OK" + - True otherwise + """ + result = {} + if response.get("ERROR") is not None: + result["success"] = False + result["changed"] = False + return result + if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: + result["success"] = False + result["changed"] = False + return result + result["success"] = True + result["changed"] = True + return result + + @property + def failed_result(self): + """ + Return a result for a failed task with no changes + """ + return ImageUpgradeTaskResult(None).failed_result + + @property + def response_current(self): + """ + Return the current POST response from the controller + instance.commit() must be called first. + + This is a dict of the current response from the controller. + """ + return copy.deepcopy(self.properties.get("response_current")) + + @response_current.setter + def response_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response_current must be a dict. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["response_current"] = value + + @property + def response(self): + """ + Return the aggregated POST response from the controller + instance.commit() must be called first. + + This is a list of responses from the controller. + """ + return copy.deepcopy(self.properties.get("response")) + + @response.setter + def response(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response must be a dict. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["response"].append(value) + + @property + def result(self): + """ + Return the aggregated result from the controller + instance.commit() must be called first. + + This is a list of results from the controller. + """ + return copy.deepcopy(self.properties.get("result")) + + @result.setter + def result(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.result must be a dict. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["result"].append(value) + + @property + def result_current(self): + """ + Return the current result from the controller + instance.commit() must be called first. + + This is a dict containing the current result. + """ + return copy.deepcopy(self.properties.get("result_current")) + + @result_current.setter + def result_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.result_current must be a dict. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["result_current"] = value + + @property + def send_interval(self): + """ + Send interval, in seconds, for retrying responses from the controller. + Valid values: int() + Default: 5 + """ + return self.properties.get("send_interval") + + @send_interval.setter + def send_interval(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an int(). Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["send_interval"] = value + + @property + def timeout(self): + """ + Timeout, in seconds, for retrieving responses from the controller. + Valid values: int() + Default: 300 + """ + return self.properties.get("timeout") + + @timeout.setter + def timeout(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an int(). Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["timeout"] = value + + @property + def unit_test(self): + """ + Is the class running under a unit test. + Set this to True in unit tests to speed the test up. + Default: False + """ + return self.properties.get("unit_test") + + @unit_test.setter + def unit_test(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be a bool(). Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["unit_test"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json index 14324084e..3bbd1d77b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json @@ -8,7 +8,7 @@ ], "epldOptions": { "golden": true, - "moduleNumber": 27 + "moduleNumber": 1 }, "epldUpgrade": true, "issuUpgrade": false, diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index 1d6327d42..d134d9d42 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -80,9 +80,11 @@ def test_image_mgmt_install_options_00002(image_install_options) -> None: assert instance.properties.get("issu") is True assert instance.properties.get("package_install") is False assert instance.properties.get("policy_name") is None - assert instance.properties.get("response") is None + assert instance.properties.get("response") == [] + assert instance.properties.get("response_current") == {} assert instance.properties.get("response_data") is None - assert instance.properties.get("result") == {} + assert instance.properties.get("result") == [] + assert instance.properties.get("result_current") == {} assert instance.properties.get("serial_number") is None assert instance.properties.get("timeout") == 300 assert instance.properties.get("unit_test") is False @@ -100,7 +102,7 @@ def test_image_mgmt_install_options_00003(image_install_options) -> None: """ instance = image_install_options instance.serial_number = "FOO" - match = "ImageInstallOptions.refresh: " + match = "ImageInstallOptions._validate_refresh_parameters: " match += "instance.policy_name must be set before " match += r"calling refresh\(\)" with pytest.raises(AnsibleFailJson, match=match): @@ -116,7 +118,7 @@ def test_image_mgmt_install_options_00004(image_install_options) -> None: - fail_json is called because serial_number is not set when refresh is called - fail_json error message is matched """ - match = "ImageInstallOptions.refresh: " + match = "ImageInstallOptions._validate_refresh_parameters: " match += "instance.serial_number must be set before " match += r"calling refresh\(\)" @@ -148,7 +150,8 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.policy_name = "KRM5" instance.serial_number = "BAR" instance.refresh() - assert isinstance(instance.response, dict) + assert isinstance(instance.response, list) + assert isinstance(instance.response_current, dict) assert instance.device_name == "cvd-1314-leaf" assert instance.err_message is None assert instance.epld_modules is None @@ -168,7 +171,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.version == "10.2.5" comp_disp = "show install all impact nxos bootflash:nxos64-cs.10.2.5.M.bin" assert instance.comp_disp == comp_disp - assert instance.result.get("success") is True + assert instance.result_current.get("success") is True def test_image_mgmt_install_options_00006(monkeypatch, image_install_options) -> None: @@ -229,7 +232,10 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.serial_number = "FDO21120U5D" instance.unit_test = True instance.refresh() - assert isinstance(instance.response, dict) + assert isinstance(instance.response_current, dict) + assert isinstance(instance.response, list) + assert isinstance(instance.result_current, dict) + assert isinstance(instance.result, list) assert instance.device_name == "leaf1" assert instance.err_message is None assert instance.epld_modules is None @@ -249,7 +255,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.version == "10.2.5" assert instance.version_check == "Compatibility status skipped." assert instance.comp_disp == "Compatibility status skipped." - assert instance.result.get("success") is True + assert instance.result_current.get("success") is True def test_image_mgmt_install_options_00008(monkeypatch, image_install_options) -> None: @@ -286,7 +292,10 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.package_install = False instance.unit_test = True instance.refresh() - assert isinstance(instance.response, dict) + assert isinstance(instance.response_current, dict) + assert isinstance(instance.response, list) + assert isinstance(instance.result_current, dict) + assert isinstance(instance.result, list) assert instance.device_name == "leaf1" assert instance.err_message is None assert isinstance(instance.epld_modules, dict) @@ -307,7 +316,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.version == "10.2.5" assert instance.version_check == "Compatibility status skipped." assert instance.comp_disp == "Compatibility status skipped." - assert instance.result.get("success") is True + assert instance.result_current.get("success") is True def test_image_mgmt_install_options_00009(monkeypatch, image_install_options) -> None: @@ -344,7 +353,10 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.package_install = False instance.unit_test = True instance.refresh() - assert isinstance(instance.response, dict) + assert isinstance(instance.response_current, dict) + assert isinstance(instance.response, list) + assert isinstance(instance.result_current, dict) + assert isinstance(instance.result, list) assert instance.device_name is None assert instance.err_message is None assert isinstance(instance.epld_modules, dict) @@ -365,7 +377,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.version is None assert instance.version_check is None assert instance.comp_disp is None - assert instance.result.get("success") is True + assert instance.result_current.get("success") is True def test_image_mgmt_install_options_00010(monkeypatch, image_install_options) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index eb4a466a6..48dfa4ad7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -440,7 +440,8 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: assert instance.response_current.get("METHOD") == "DELETE" assert instance.response_current.get("MESSAGE") == "OK" assert ( - instance.response_current.get("DATA") == "Successfully detach the policy from device." + instance.response_current.get("DATA") + == "Successfully detach the policy from device." ) assert instance.result_current.get("success") is True assert instance.result_current.get("changed") is True diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 1ff26c30d..3d1be00c1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -28,15 +28,21 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import logging from typing import Any, Dict import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.mock_rest_send import \ + MockRestSend as MockRestSendImageUpgrade +from ansible_collections.cisco.dcnm.plugins.module_utils.common.mock_rest_send import \ + MockRestSend as MockRestSendInstallOptions from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ ImageUpgrade -from .image_upgrade_utils import (does_not_raise, image_upgrade_fixture, +from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, + image_upgrade_fixture, issu_details_by_ip_address_fixture, payloads_image_upgrade, responses_image_install_options, @@ -46,6 +52,7 @@ PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +REST_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.RestSend" DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" @@ -65,8 +72,11 @@ def test_image_mgmt_upgrade_00001(image_upgrade) -> None: assert isinstance(instance.ipv4_todo, set) assert isinstance(instance.payload, dict) assert instance.class_name == "ImageUpgrade" - assert instance.path is None - assert instance.verb is None + assert ( + instance.path + == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image" + ) + assert instance.verb == "POST" def test_image_mgmt_upgrade_00003(image_upgrade) -> None: @@ -153,7 +163,9 @@ def test_image_mgmt_upgrade_00005(image_upgrade) -> None: """ instance = image_upgrade - match = "ImageUpgrade._validate_devices: call instance.devices before calling commit." + match = ( + "ImageUpgrade._validate_devices: call instance.devices before calling commit." + ) with pytest.raises(AnsibleFailJson, match=match): instance.unit_test = True instance.commit() @@ -162,7 +174,7 @@ def test_image_mgmt_upgrade_00005(image_upgrade) -> None: def test_image_mgmt_upgrade_00018(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - upgrade.nxos set to invalid value @@ -201,7 +213,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr( + DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit + ) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -234,36 +248,43 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.commit() -def test_image_mgmt_upgrade_00019(monkeypatch, image_upgrade) -> None: +def test_image_mgmt_upgrade_00019(monkeypatch, image_upgrade, caplog) -> None: """ Function - - commit + - ImageUpgrade._build_payload Test - non-default values are set for several options - policy_changed is set to False + - Verify that payload is built correctly Setup - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded. - - The methods called by commit are mocked to simulate that the - the image has already been staged and validated and the device - has already been upgraded to the desired version. - - Methods called by commit that wait for current actions, and - image upgrade, to complete are mocked to do nothing. + - commit -> _build_payload -> issu_details is mocked to simulate + that the the image has already been staged and validated and the + device has already been upgraded to the desired version. + - commit -> _build_payload -> install_options is mocked to simulate + that the the image EPLD does not need upgrade. + - The following methods, called by commit() are mocked to do nothing: + - _wait_for_current_actions_to_complete + - _wait_for_image_upgrade_to_complete + - RestSend is mocked to return a successful response Expected results: - 1. instance.payload will equal a payload previously obtained by - running ansible-playbook against the controller for this - scenario which verifies that the non-default values are - included in the payload. + 1. instance.payload (built by instance._build_payload and based on + instance.devices) will equal a payload previously obtained by running + ansible-playbook against the controller for this scenario, which verifies + that the non-default values are included in the payload. """ + caplog.set_level(logging.DEBUG) instance = image_upgrade key = "test_image_mgmt_upgrade_00019a" + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -271,9 +292,6 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -282,7 +300,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -303,7 +321,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "options": { "nxos": {"mode": "disruptive", "bios_force": True}, "package": {"install": True, "uninstall": False}, - "epld": {"module": 27, "golden": True}, + "epld": {"module": 1, "golden": True}, "reboot": {"config_reload": True, "write_erase": False}, }, "validate": True, @@ -311,16 +329,19 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + instance.unit_test = True instance.commit() assert instance.payload == payloads_image_upgrade(key) -def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade) -> None: +def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade, caplog) -> None: """ Function - - commit + - ImageUpgrade.commit Test - User explicitely sets default values for several options @@ -329,10 +350,15 @@ def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade) -> None: Setup: - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - - The methods called by commit are mocked to simulate that the - device has not yet been upgraded to the desired version - - Methods called by commit that wait for current actions, and - image upgrade, to complete are mocked to do nothing + - commit -> _build_payload -> issu_details is mocked to simulate + that the the image has already been staged and validated and the + device has already been upgraded to the desired version. + - commit -> _build_payload -> install_options is mocked to simulate + that the the image EPLD does not need upgrade. + - The following methods, called by commit() are mocked to do nothing: + - _wait_for_current_actions_to_complete + - _wait_for_image_upgrade_to_complete + - RestSend is mocked to return a successful response Expected results: @@ -340,9 +366,11 @@ def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade) -> None: 1. instance.payload will equal a payload previously obtained by running ansible-playbook against the controller for this scenario """ + caplog.set_level(logging.DEBUG) instance = image_upgrade key = "test_image_mgmt_upgrade_00020a" + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -350,9 +378,6 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -361,7 +386,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -390,9 +415,12 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + instance.unit_test = True instance.commit() - assert instance.payload == payloads_image_upgrade(key) @@ -420,6 +448,7 @@ def test_image_mgmt_upgrade_00021(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00021a" + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -427,9 +456,6 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -438,7 +464,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -468,6 +494,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageUpgrade._build_payload_issu_options_1: " match += "options.nxos.mode must be one of " match += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " @@ -504,9 +533,7 @@ def test_image_mgmt_upgrade_00022(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00022a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -522,7 +549,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -552,6 +579,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + instance.unit_test = True instance.commit() assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False @@ -586,9 +616,7 @@ def test_image_mgmt_upgrade_00023(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00023a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -604,7 +632,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -633,6 +661,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + instance.unit_test = True instance.commit() assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False @@ -665,9 +697,7 @@ def test_image_mgmt_upgrade_00024(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00024a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -683,7 +713,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -712,6 +742,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageUpgrade._build_payload_issu_options_2: " match += r"options.nxos.bios_force must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -744,9 +778,7 @@ def test_image_mgmt_upgrade_00025(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00025a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -762,7 +794,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -791,6 +823,8 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file match = "ImageUpgrade._build_payload_epld: Invalid configuration for " match += "172.22.150.102. If options.epld.golden is True " @@ -825,9 +859,7 @@ def test_image_mgmt_upgrade_00026(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00026a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -843,7 +875,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -872,6 +904,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageUpgrade._build_payload_epld: " match += "options.epld.module must either be 'ALL' " match += r"or an integer. Got FOO\." @@ -904,9 +940,7 @@ def test_image_mgmt_upgrade_00027(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00027a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -922,7 +956,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -951,6 +985,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageUpgrade._build_payload_epld: " match += r"options.epld.golden must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -982,9 +1019,7 @@ def test_image_mgmt_upgrade_00028(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00028a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1000,7 +1035,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1029,6 +1064,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageUpgrade._build_payload_reboot: " match += r"reboot must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1061,9 +1100,7 @@ def test_image_mgmt_upgrade_00029(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00029a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1079,7 +1116,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1108,6 +1145,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageUpgrade._build_payload_reboot_options: " match += r"options.reboot.config_reload must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1140,9 +1181,7 @@ def test_image_mgmt_upgrade_00030(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00030a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1158,7 +1197,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1187,6 +1226,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageUpgrade._build_payload_reboot_options: " match += r"options.reboot.write_erase must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1224,9 +1267,7 @@ def test_image_mgmt_upgrade_00031(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00031a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1242,7 +1283,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1271,6 +1312,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageUpgrade._build_payload_package: " match += r"options.package.uninstall must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1304,9 +1349,7 @@ def test_image_mgmt_upgrade_00032(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00032a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1322,7 +1365,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1351,6 +1394,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + MockRestSendImageUpgrade.unit_test = True + match = "ImageUpgrade.commit: failed: " match += r"\{'success': False, 'changed': False\}. " match += r"Controller response: \{'DATA': 123, " @@ -1360,7 +1407,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): match += "imagemanagement/rest/imageupgrade/upgrade-image', " match += r"'RETURN_CODE': 500\}" with pytest.raises(AnsibleFailJson, match=match): - instance.unit_test = True instance.commit() @@ -1389,9 +1435,7 @@ def test_image_mgmt_upgrade_00033(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00033a" - - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1407,7 +1451,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1426,7 +1470,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "upgrade": {"nxos": True, "epld": "FOO"}, "options": { "package": { - "uninstall": "FOO", + "uninstall": False, } }, "validate": True, @@ -1434,6 +1478,10 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + match = "ImageInstallOptions.epld: " match += r"epld must be a boolean value. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1441,7 +1489,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.commit() -# getters +# test getter properties def test_image_mgmt_upgrade_00043(image_upgrade) -> None: @@ -1484,6 +1532,7 @@ def test_image_mgmt_upgrade_00045(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00045a" + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1502,7 +1551,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1531,7 +1580,11 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - instance.unit_test = True + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + + # instance.unit_test = True instance.commit() assert instance.response_data == [121] @@ -1539,6 +1592,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00046(monkeypatch, image_upgrade) -> None: """ Function + - result_current - result Setup: @@ -1553,11 +1607,13 @@ def test_image_mgmt_upgrade_00046(monkeypatch, image_upgrade) -> None: Expected results: - 1. instance.result == {'success': True, 'changed': True} + 1. instance.rest_send.result_current == {'success': True, 'changed': True} + 1. instance.rest_send.result == [{'success': True, 'changed': True}] """ instance = image_upgrade key = "test_image_mgmt_upgrade_00046a" + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1565,9 +1621,6 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1576,7 +1629,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1605,9 +1658,14 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + instance.unit_test = True instance.commit() - assert instance.result_current == {"success": True, "changed": True} + assert instance.rest_send.result_current == {"success": True, "changed": True} + assert instance.rest_send.result == [{"success": True, "changed": True}] def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: @@ -1632,6 +1690,7 @@ def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00047a" + image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1639,9 +1698,6 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass @@ -1650,7 +1706,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit) + monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1679,7 +1735,11 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] - instance.unit_test = True + + MockRestSendImageUpgrade.key = key + MockRestSendImageUpgrade.file = image_upgrade_file + + # instance.unit_test = True instance.commit() print(f"instance.response: {instance.response}") assert isinstance(instance.response, list) From d0a09f0ceedf0027a8a82ad3681d77ed382d13a8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 28 Jan 2024 08:50:02 -1000 Subject: [PATCH 231/300] Fix PEP8 whitespace error --- plugins/module_utils/common/mock_rest_send.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/common/mock_rest_send.py b/plugins/module_utils/common/mock_rest_send.py index 8831fdb74..1be4abb49 100644 --- a/plugins/module_utils/common/mock_rest_send.py +++ b/plugins/module_utils/common/mock_rest_send.py @@ -113,7 +113,6 @@ def commit(self): self.log.debug(msg) while timeout > 0 and success is False: - if self.payload is None: msg = f"{caller}: Calling load_fixture: file {self.file}, key {self.key} verb {self.verb}, path {self.path}" self.log.debug(msg) From b1843567d96cb66c25765a728ebfe7b301334b67 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 28 Jan 2024 09:02:47 -1000 Subject: [PATCH 232/300] Fix PEP8 import handling error --- plugins/module_utils/common/mock_rest_send.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/common/mock_rest_send.py b/plugins/module_utils/common/mock_rest_send.py index 1be4abb49..a9be657e2 100644 --- a/plugins/module_utils/common/mock_rest_send.py +++ b/plugins/module_utils/common/mock_rest_send.py @@ -22,14 +22,19 @@ import inspect import json import logging +import sys from time import sleep # Using only for its failed_result property from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ ImageUpgradeTaskResult -from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.fixture import \ - load_fixture - +try: + from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.fixture import \ + load_fixture +except ImportError as error: + print("ImportError: load_fixture() not found. Exiting.") + print(f"Error detail: {error}") + sys.exit(1) class MockRestSend: """ From 50028d4ab87309cd2af8dda2e2f345d93beeae90 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 28 Jan 2024 09:14:10 -1000 Subject: [PATCH 233/300] Fix PEP8 linespace error, move MockRestSend to image_mgmt for now --- plugins/module_utils/{common => image_mgmt}/mock_rest_send.py | 1 + .../dcnm_image_upgrade/test_image_upgrade_image_upgrade.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) rename plugins/module_utils/{common => image_mgmt}/mock_rest_send.py (99%) diff --git a/plugins/module_utils/common/mock_rest_send.py b/plugins/module_utils/image_mgmt/mock_rest_send.py similarity index 99% rename from plugins/module_utils/common/mock_rest_send.py rename to plugins/module_utils/image_mgmt/mock_rest_send.py index a9be657e2..abe2ff6b5 100644 --- a/plugins/module_utils/common/mock_rest_send.py +++ b/plugins/module_utils/image_mgmt/mock_rest_send.py @@ -36,6 +36,7 @@ print(f"Error detail: {error}") sys.exit(1) + class MockRestSend: """ Send REST requests to the controller with retries, and handle responses. diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 3d1be00c1..53500e805 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -34,10 +34,8 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.common.mock_rest_send import \ +from plugins.module_utils.image_mgmt.mock_rest_send import \ MockRestSend as MockRestSendImageUpgrade -from ansible_collections.cisco.dcnm.plugins.module_utils.common.mock_rest_send import \ - MockRestSend as MockRestSendInstallOptions from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ ImageUpgrade From 1ff362b1d6376df226f1f358d33c378502949636 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 28 Jan 2024 09:35:54 -1000 Subject: [PATCH 234/300] Move MockRestSend into the image management unit test directory --- .../modules/dcnm/dcnm_image_upgrade}/mock_rest_send.py | 9 ++------- .../test_image_upgrade_image_upgrade.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) rename {plugins/module_utils/image_mgmt => tests/unit/modules/dcnm/dcnm_image_upgrade}/mock_rest_send.py (98%) diff --git a/plugins/module_utils/image_mgmt/mock_rest_send.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/mock_rest_send.py similarity index 98% rename from plugins/module_utils/image_mgmt/mock_rest_send.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/mock_rest_send.py index abe2ff6b5..74541c15b 100644 --- a/plugins/module_utils/image_mgmt/mock_rest_send.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/mock_rest_send.py @@ -28,13 +28,8 @@ # Using only for its failed_result property from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ ImageUpgradeTaskResult -try: - from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.fixture import \ - load_fixture -except ImportError as error: - print("ImportError: load_fixture() not found. Exiting.") - print(f"Error detail: {error}") - sys.exit(1) +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.fixture import \ + load_fixture class MockRestSend: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 53500e805..af73fbf59 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -34,7 +34,7 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from plugins.module_utils.image_mgmt.mock_rest_send import \ +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.mock_rest_send import \ MockRestSend as MockRestSendImageUpgrade from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ ImageUpgrade From 4b68149db24e072c3e2138ab2642d146eee3132f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 28 Jan 2024 09:41:26 -1000 Subject: [PATCH 235/300] Fix f-string error --- plugins/module_utils/image_mgmt/image_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 6568c5dc7..91154ead6 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -472,7 +472,7 @@ def commit(self) -> None: self._build_payload(device) - msg = f"calling rest_send: " + msg = "Calling rest_send: " msg += f"verb {self.verb}, path: {self.path} " msg += f"payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) From 577cc8c2eff2d318fcd7288fc9242c07e86a7182 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 28 Jan 2024 16:55:01 -1000 Subject: [PATCH 236/300] Remove need for MockRestSend Figured out a working patch path. --- .../dcnm/dcnm_image_upgrade/mock_rest_send.py | 397 ------------------ .../test_image_upgrade_image_upgrade.py | 207 ++++----- 2 files changed, 108 insertions(+), 496 deletions(-) delete mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/mock_rest_send.py diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/mock_rest_send.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/mock_rest_send.py deleted file mode 100644 index 74541c15b..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/mock_rest_send.py +++ /dev/null @@ -1,397 +0,0 @@ -# -# Copyright (c) 2024 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__ = "Allen Robel" - -import copy -import inspect -import json -import logging -import sys -from time import sleep - -# Using only for its failed_result property -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ - ImageUpgradeTaskResult -from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.fixture import \ - load_fixture - - -class MockRestSend: - """ - Send REST requests to the controller with retries, and handle responses. - - Usage (where ansible_module is an instance of AnsibleModule): - - send_rest = RestSend(ansible_module) - send_rest.path = "/rest/top-down/fabrics" - send_rest.verb = "GET" - send_rest.commit() - - response = send_rest.response - result = send_rest.result - """ - - def __init__(self, ansible_module): - self.class_name = self.__class__.__name__ - - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED SendRest()" - self.log.debug(msg) - - self.ansible_module = ansible_module - self.params = ansible_module.params - - self.properties = {} - self.properties["response"] = [] - self.properties["response_current"] = {} - self.properties["result"] = [] - self.properties["result_current"] = {} - self.properties["send_interval"] = 5 - self.properties["timeout"] = 300 - self.properties["unit_test"] = False - self.properties["verb"] = None - self.properties["path"] = None - self.properties["key"] = None - self.properties["file"] = None - self.properties["payload"] = None - - def _verify_commit_parameters(self): - if self.verb is None: - msg = f"{self.class_name}._verify_commit_parameters: " - msg += "verb must be set before calling commit()." - self.ansible_module.fail_json(msg, **self.failed_result) - if self.path is None: - msg = f"{self.class_name}._verify_commit_parameters: " - msg += "path must be set before calling commit()." - self.ansible_module.fail_json(msg, **self.failed_result) - - def commit(self): - """ - Call dcnm_send() with retries until successful response or timeout is exceeded. - - Properties read: - self.send_interval: interval between retries (set in ImageUpgradeCommon) - self.timeout: timeout in seconds (set in ImageUpgradeCommon) - self.verb: HTTP verb e.g. GET, POST, PUT, DELETE - self.path: HTTP path e.g. http://controller_ip/path/to/endpoint - self.payload: Optional HTTP payload - - Properties written: - self.properties["response"]: raw response from the controller - self.properties["result"]: result from self._handle_response() method - """ - caller = inspect.stack()[1][3] - - self._verify_commit_parameters() - try: - timeout = self.timeout - except AttributeError: - timeout = 300 - - success = False - msg = f"{caller}: Entering commit loop. " - msg += f"timeout {timeout}, send_interval {self.send_interval}" - self.log.debug(msg) - msg = f"verb {self.verb}, path {self.path}," - self.log.debug(msg) - msg = f"payload {json.dumps(self.payload, indent=4, sort_keys=True)}" - self.log.debug(msg) - - while timeout > 0 and success is False: - if self.payload is None: - msg = f"{caller}: Calling load_fixture: file {self.file}, key {self.key} verb {self.verb}, path {self.path}" - self.log.debug(msg) - response = load_fixture(self.file).get(self.key) - else: - msg = f"{caller}: Calling load_fixture: " - msg += f"file {self.file}, key {self.key} " - msg += f"verb {self.verb}, path {self.path}, " - msg += f"payload {json.dumps(self.payload, indent=4, sort_keys=True)}" - self.log.debug(msg) - response = load_fixture(self.file).get(self.key) - - self.response_current = copy.deepcopy(response) - self.result_current = self._handle_response(response) - - success = self.result_current["success"] - - if success is False and self.unit_test is False: - sleep(self.send_interval) - timeout -= self.send_interval - - self.response = copy.deepcopy(response) - self.result = copy.deepcopy(self.result_current) - - msg = f"{caller}: Exiting commit while loop. success {success}. verb {self.verb}, path {self.path}." - self.log.debug(msg) - - msg = f"{caller}: self.response_current {json.dumps(self.response_current, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = f"{caller}: self.response {json.dumps(self.response, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = f"{caller}: self.result_current {json.dumps(self.result_current, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = ( - f"{caller}: self.result {json.dumps(self.result, indent=4, sort_keys=True)}" - ) - self.log.debug(msg) - - def _handle_response(self, response): - """ - Call the appropriate handler for response based on verb - """ - if self.verb == "GET": - return self._handle_get_response(response) - if self.verb in {"POST", "PUT", "DELETE"}: - return self._handle_post_put_delete_response(response) - return self._handle_unknown_request_verbs(response) - - def _handle_unknown_request_verbs(self, response): - method_name = inspect.stack()[0][3] - - msg = f"{self.class_name}.{method_name}: " - msg += f"Unknown request verb ({self.verb}) for response {response}." - self.ansible_module.fail_json(msg) - - def _handle_get_response(self, response): - """ - Caller: - - self._handle_response() - Handle controller responses to GET requests - Returns: dict() with the following keys: - - found: - - False, if request error was "Not found" and RETURN_CODE == 404 - - True otherwise - - success: - - False if RETURN_CODE != 200 or MESSAGE != "OK" - - True otherwise - """ - result = {} - success_return_codes = {200, 404} - if ( - response.get("RETURN_CODE") == 404 - and response.get("MESSAGE") == "Not Found" - ): - result["found"] = False - result["success"] = True - return result - if ( - response.get("RETURN_CODE") not in success_return_codes - or response.get("MESSAGE") != "OK" - ): - result["found"] = False - result["success"] = False - return result - result["found"] = True - result["success"] = True - return result - - def _handle_post_put_delete_response(self, response): - """ - Caller: - - self.self._handle_response() - - Handle POST, PUT responses from the controller. - - Returns: dict() with the following keys: - - changed: - - True if changes were made to by the controller - - False otherwise - - success: - - False if RETURN_CODE != 200 or MESSAGE != "OK" - - True otherwise - """ - result = {} - if response.get("ERROR") is not None: - result["success"] = False - result["changed"] = False - return result - if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: - result["success"] = False - result["changed"] = False - return result - result["success"] = True - result["changed"] = True - return result - - @property - def failed_result(self): - """ - Return a result for a failed task with no changes - """ - return ImageUpgradeTaskResult(None).failed_result - - @property - def file(self): - """ - JSON file containing the simulated response. - """ - return self.properties.get("file") - - @file.setter - def file(self, value): - self.properties["file"] = value - - @property - def key(self): - """ - key within file from which to pull simulated response. - """ - return self.properties.get("key") - - @key.setter - def key(self, value): - self.properties["key"] = value - - @property - def response_current(self): - """ - Return the current POST response from the controller - instance.commit() must be called first. - - This is a dict of the current response from the controller. - """ - return self.properties.get("response_current") - - @response_current.setter - def response_current(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, dict): - msg = f"{self.class_name}.{method_name}: " - msg += "instance.response_current must be a dict. " - msg += f"Got {value}." - self.ansible_module.fail_json(msg, **self.failed_result) - self.properties["response_current"] = value - - @property - def response(self): - """ - Return the aggregated POST response from the controller - instance.commit() must be called first. - - This is a list of responses from the controller. - """ - return self.properties.get("response") - - @response.setter - def response(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, dict): - msg = f"{self.class_name}.{method_name}: " - msg += "instance.response must be a dict. " - msg += f"Got {value}." - self.ansible_module.fail_json(msg, **self.failed_result) - self.properties["response"].append(value) - - @property - def result(self): - """ - Return the aggregated result from the controller - instance.commit() must be called first. - - This is a list of results from the controller. - """ - return self.properties.get("result") - - @result.setter - def result(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, dict): - msg = f"{self.class_name}.{method_name}: " - msg += "instance.result must be a dict. " - msg += f"Got {value}." - self.ansible_module.fail_json(msg, **self.failed_result) - self.properties["result"].append(value) - - @property - def result_current(self): - """ - Return the current result from the controller - instance.commit() must be called first. - - This is a dict containing the current result. - """ - return self.properties.get("result_current") - - @result_current.setter - def result_current(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, dict): - msg = f"{self.class_name}.{method_name}: " - msg += "instance.result_current must be a dict. " - msg += f"Got {value}." - self.ansible_module.fail_json(msg, **self.failed_result) - self.properties["result_current"] = value - - @property - def send_interval(self): - """ - Send interval, in seconds, for retrying responses from the controller. - Valid values: int() - Default: 5 - """ - return self.properties.get("send_interval") - - @send_interval.setter - def send_interval(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be an int(). Got {value}." - self.ansible_module.fail_json(msg, **self.failed_result) - self.properties["send_interval"] = value - - @property - def timeout(self): - """ - Timeout, in seconds, for retrieving responses from the controller. - Valid values: int() - Default: 300 - """ - return self.properties.get("timeout") - - @timeout.setter - def timeout(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be an int(). Got {value}." - self.ansible_module.fail_json(msg, **self.failed_result) - self.properties["timeout"] = value - - @property - def unit_test(self): - """ - Is the class running under a unit test. - Set this to True in unit tests to speed the test up. - Default: False - """ - return self.properties.get("unit_test") - - @unit_test.setter - def unit_test(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be a bool(). Got {value}." - self.ansible_module.fail_json(msg, **self.failed_result) - self.properties["unit_test"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index af73fbf59..75fec9bf9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -34,12 +34,10 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_upgrade.mock_rest_send import \ - MockRestSend as MockRestSendImageUpgrade from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ ImageUpgrade -from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, +from .image_upgrade_utils import (does_not_raise, image_upgrade_fixture, issu_details_by_ip_address_fixture, payloads_image_upgrade, @@ -50,6 +48,10 @@ PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT = PATCH_IMAGE_MGMT + "image_upgrade.RestSend.commit" +PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT = PATCH_IMAGE_MGMT + "image_upgrade.RestSend.response_current" +PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT = PATCH_IMAGE_MGMT + "image_upgrade.RestSend.result_current" + REST_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.RestSend" DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" @@ -282,7 +284,6 @@ def test_image_mgmt_upgrade_00019(monkeypatch, image_upgrade, caplog) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00019a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -296,9 +297,15 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True}) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) + monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -327,8 +334,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": False, } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file instance.unit_test = True instance.commit() @@ -368,7 +373,6 @@ def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade, caplog) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00020a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -382,9 +386,15 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True}) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) + monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -414,9 +424,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - instance.unit_test = True instance.commit() assert instance.payload == payloads_image_upgrade(key) @@ -446,7 +453,6 @@ def test_image_mgmt_upgrade_00021(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00021a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -460,9 +466,13 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -492,9 +502,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - match = "ImageUpgrade._build_payload_issu_options_1: " match += "options.nxos.mode must be one of " match += r"\['disruptive', 'force_non_disruptive', 'non_disruptive'\]. " @@ -531,7 +538,6 @@ def test_image_mgmt_upgrade_00022(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00022a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -545,9 +551,14 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True}) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -577,9 +588,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - instance.unit_test = True instance.commit() assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False @@ -614,7 +622,6 @@ def test_image_mgmt_upgrade_00023(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00023a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -628,9 +635,14 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True}) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -660,9 +672,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - instance.unit_test = True instance.commit() assert instance.payload["issuUpgradeOptions1"]["disruptive"] is False @@ -695,7 +704,6 @@ def test_image_mgmt_upgrade_00024(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00024a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -709,9 +717,13 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -741,9 +753,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - match = "ImageUpgrade._build_payload_issu_options_2: " match += r"options.nxos.bios_force must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -776,7 +785,6 @@ def test_image_mgmt_upgrade_00025(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00025a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -790,9 +798,13 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -821,8 +833,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file match = "ImageUpgrade._build_payload_epld: Invalid configuration for " match += "172.22.150.102. If options.epld.golden is True " @@ -857,7 +867,6 @@ def test_image_mgmt_upgrade_00026(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00026a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -871,9 +880,13 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -903,9 +916,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - match = "ImageUpgrade._build_payload_epld: " match += "options.epld.module must either be 'ALL' " match += r"or an integer. Got FOO\." @@ -952,9 +962,16 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -983,8 +1000,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file match = "ImageUpgrade._build_payload_epld: " match += r"options.epld.golden must be a boolean. Got FOO\." @@ -1017,7 +1032,6 @@ def test_image_mgmt_upgrade_00028(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00028a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1031,9 +1045,13 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1063,9 +1081,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - match = "ImageUpgrade._build_payload_reboot: " match += r"reboot must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1098,7 +1113,6 @@ def test_image_mgmt_upgrade_00029(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00029a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1112,9 +1126,13 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1144,9 +1162,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - match = "ImageUpgrade._build_payload_reboot_options: " match += r"options.reboot.config_reload must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1179,7 +1194,6 @@ def test_image_mgmt_upgrade_00030(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00030a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1193,9 +1207,13 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1225,9 +1243,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - match = "ImageUpgrade._build_payload_reboot_options: " match += r"options.reboot.write_erase must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1265,7 +1280,6 @@ def test_image_mgmt_upgrade_00031(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00031a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1279,9 +1293,13 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1311,9 +1329,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - match = "ImageUpgrade._build_payload_package: " match += r"options.package.uninstall must be a boolean. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1347,7 +1362,6 @@ def test_image_mgmt_upgrade_00032(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00032a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1361,9 +1375,15 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key)) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {'success': False, 'changed': False}) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1392,9 +1412,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): "policy_changed": True, } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - MockRestSendImageUpgrade.unit_test = True match = "ImageUpgrade.commit: failed: " match += r"\{'success': False, 'changed': False\}. " @@ -1433,7 +1450,6 @@ def test_image_mgmt_upgrade_00033(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00033a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1449,7 +1465,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1477,9 +1492,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - match = "ImageInstallOptions.epld: " match += r"epld must be a boolean value. Got FOO\." with pytest.raises(AnsibleFailJson, match=match): @@ -1530,7 +1542,6 @@ def test_image_mgmt_upgrade_00045(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00045a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1538,18 +1549,21 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key)) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {'success': True, 'changed': True}) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1579,10 +1593,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - - # instance.unit_test = True instance.commit() assert instance.response_data == [121] @@ -1590,7 +1600,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def test_image_mgmt_upgrade_00046(monkeypatch, image_upgrade) -> None: """ Function - - result_current - result Setup: @@ -1605,13 +1614,11 @@ def test_image_mgmt_upgrade_00046(monkeypatch, image_upgrade) -> None: Expected results: - 1. instance.rest_send.result_current == {'success': True, 'changed': True} - 1. instance.rest_send.result == [{'success': True, 'changed': True}] + 1. instance.result is a list: [{'success': True, 'changed': True}] """ instance = image_upgrade key = "test_image_mgmt_upgrade_00046a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1625,9 +1632,14 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {'success': True, 'changed': True}) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1657,13 +1669,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - instance.unit_test = True instance.commit() - assert instance.rest_send.result_current == {"success": True, "changed": True} - assert instance.rest_send.result == [{"success": True, "changed": True}] + assert instance.result == [{"success": True, "changed": True}] def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: @@ -1688,7 +1696,6 @@ def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_mgmt_upgrade_00047a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1702,9 +1709,15 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): pass + def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: + return responses_image_upgrade(key) + + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key)) + monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {'success': True, 'changed': True}) + monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr(REST_SEND_IMAGE_UPGRADE, MockRestSendImageUpgrade) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", @@ -1734,10 +1747,6 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): } ] - MockRestSendImageUpgrade.key = key - MockRestSendImageUpgrade.file = image_upgrade_file - - # instance.unit_test = True instance.commit() print(f"instance.response: {instance.response}") assert isinstance(instance.response, list) From 6bc8074e02857da4ce2baf5fab42fb8dda73e670 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 28 Jan 2024 17:02:13 -1000 Subject: [PATCH 237/300] ImageUpgrade: instantiate RestSend in __init__, more... ImageUpgrade.commit(): populate self.result, self.result_current, self.response, self.response_current, self.response_data with data received from RestSend. --- .../module_utils/image_mgmt/image_upgrade.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 91154ead6..a6709a497 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -145,6 +145,8 @@ def __init__(self, module): self.path = self.endpoints.image_upgrade.get("path") self.verb = self.endpoints.image_upgrade.get("verb") + self.rest_send = RestSend(self.module) + self._init_properties() self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) self.install_options = ImageInstallOptions(self.module) @@ -462,7 +464,6 @@ def commit(self) -> None: self._validate_devices() self._wait_for_current_actions_to_complete() - self.rest_send = RestSend(self.module) self.rest_send.verb = self.verb self.rest_send.path = self.path @@ -472,7 +473,7 @@ def commit(self) -> None: self._build_payload(device) - msg = "Calling rest_send: " + msg = "Calling rest_send.commit(): " msg += f"verb {self.verb}, path: {self.path} " msg += f"payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) @@ -480,15 +481,22 @@ def commit(self) -> None: self.rest_send.payload = self.payload self.rest_send.commit() + msg = "DONE rest_send.commit()" + self.log.debug(msg) + + self.response = self.rest_send.response_current + self.response_current = self.rest_send.response_current + self.response_data = self.rest_send.response_current.get("DATA") + + self.result_current = self.rest_send.result_current + self.result = self.rest_send.result_current + if not self.rest_send.result_current["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.rest_send.result_current}. " msg += f"Controller response: {self.rest_send.response_current}" self.module.fail_json(msg, **self.failed_result) - self.response_data = self.rest_send.response_current.get("DATA") - self.response = self.rest_send.response_current - # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(self.payload) From deb31c67354ac798151e59040dd9388e58e92d89 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 30 Jan 2024 14:44:20 -1000 Subject: [PATCH 238/300] ImageStage: Leverage RestSend(), more... - ImageStage: remove response, response_data, result properties (inherit from ImageUpgradeCommon) - ImageStage: modify debug statements to use json.dumps for prettier output - ImageStage: Use RestSend() class - ImageUpgradeCommon: Add response_data getter/setter properties - ImageUpgradeTask: harden handling of ImageStage.response - test_image_upgrade_image_stage.py: modify unit tests to reflect the above changes --- .../module_utils/image_mgmt/image_stage.py | 61 +++++++------------ .../image_mgmt/image_upgrade_common.py | 12 ++++ plugins/modules/dcnm_image_upgrade.py | 6 +- .../test_image_upgrade_image_stage.py | 24 +++++--- 4 files changed, 52 insertions(+), 51 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 6bff94b9d..ab6aaecd6 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -30,10 +30,10 @@ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ - dcnm_send class ImageStage(ImageUpgradeCommon): @@ -107,6 +107,7 @@ def __init__(self, module): self.log.debug("ENTERED ImageStage()") self.endpoints = ApiEndpoints() + self.rest_send = RestSend(self.module) self._init_properties() self.serial_numbers_done = set() self.controller_version = None @@ -118,9 +119,6 @@ def __init__(self, module): def _init_properties(self): # self.properties is already initialized in the parent class self.properties["serial_numbers"] = None - self.properties["response_data"] = None - self.properties["result"] = None - self.properties["response"] = None self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds @@ -210,25 +208,32 @@ def commit(self): else: self.payload["serialNumbers"] = self.serial_numbers - self.properties["response"] = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(self.payload) - ) - self.properties["result"] = self._handle_response(self.response, self.verb) + self.rest_send.verb = self.verb + self.rest_send.path = self.path + self.rest_send.payload = self.payload - msg = f"payload: {self.payload}" + self.rest_send.commit() + + self.response = self.rest_send.response_current + self.response_current = self.rest_send.response_current + self.response_data = self.response_current.get("DATA", "No Stage DATA") + + self.result = self.rest_send.result_current + self.result_current = self.rest_send.result_current + + msg = f"payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"response: {self.response}" + msg = f"response: {json.dumps(self.response, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"result: {self.result}" + msg = f"result: {json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) - if not self.result["success"]: + if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " - msg = f"failed: {self.result}. " - msg += f"Controller response: {self.response}" + msg = f"failed: {self.result_current}. " + msg += f"Controller response: {self.response_current}" self.module.fail_json(msg, **self.failed_result) - self.properties["response_data"] = self.response.get("DATA", "No Stage DATA") self._wait_for_image_stage_to_complete() for serial_number in self.serial_numbers_done: @@ -347,30 +352,6 @@ def serial_numbers(self, value): self.module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value - @property - def response_data(self): - """ - Return the result of the image staging request - for serial_numbers. - - instance.serial_numbers must be set first. - """ - return self.properties.get("response_data") - - @property - def result(self): - """ - Return the POST result from the controller - """ - return self.properties.get("result") - - @property - def response(self): - """ - Return the POST response from the controller - """ - return self.properties.get("response") - @property def check_interval(self): """ diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_mgmt/image_upgrade_common.py index 7878e02e9..c90b15a13 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_mgmt/image_upgrade_common.py @@ -59,6 +59,7 @@ def __init__(self, module): self.properties["failed"] = False self.properties["response"] = [] self.properties["response_current"] = {} + self.properties["response_data"] = [] self.properties["result"] = [] self.properties["result_current"] = {} self.properties["send_interval"] = 5 @@ -333,6 +334,17 @@ def response(self, value): self.module.fail_json(msg, **self.failed_result) self.properties["response"].append(value) + @property + def response_data(self): + """ + Return the contents of the DATA key within current_response. + """ + return self.properties.get("response_data") + + @response_data.setter + def response_data(self, value): + self.properties["response_data"].append(value) + @property def result(self): """ diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 30358ca22..76dc696fe 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1049,8 +1049,10 @@ def _stage_images(self, serial_numbers) -> None: instance.commit() for diff in instance.diff: self.task_result.diff_stage = copy.deepcopy(diff) - instance.response.pop("DATA", None) - self.task_result.response_stage = instance.response + for response in instance.response: + if "DATA" in response: + response.pop("DATA") + self.task_result.response_stage = copy.deepcopy(response) def _validate_images(self, serial_numbers) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 51f9e7ea7..49da259a3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -49,6 +49,8 @@ PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." PATCH_COMMON = PATCH_MODULE_UTILS + "common." +PATCH_IMAGE_STAGE_REST_SEND_COMMIT = PATCH_IMAGE_MGMT + "image_stage.RestSend.commit" +PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT = PATCH_IMAGE_MGMT + "image_stage.RestSend.result_current" DCNM_SEND_CONTROLLER_VERSION = PATCH_COMMON + "controller_version.dcnm_send" DCNM_SEND_IMAGE_STAGE = PATCH_IMAGE_MGMT + "image_stage.dcnm_send" @@ -86,9 +88,9 @@ def test_image_mgmt_stage_00002(image_stage) -> None: """ instance = image_stage assert isinstance(instance.properties, dict) - assert instance.properties.get("response_data") is None - assert instance.properties.get("response") is None - assert instance.properties.get("result") is None + assert instance.properties.get("response_data") == [] + assert instance.properties.get("response") == [] + assert instance.properties.get("result") == [] assert instance.properties.get("serial_numbers") is None assert instance.properties.get("check_interval") == 10 assert instance.properties.get("check_timeout") == 1800 @@ -232,7 +234,7 @@ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00006a" return responses_controller_version(key) - def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00006a" return responses_image_stage(key) @@ -241,8 +243,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) - monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) instance = image_stage if serial_numbers_is_set: @@ -267,7 +270,7 @@ def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: return responses_controller_version(key) # Needed only for the 200 return code - def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00007a" return responses_image_stage(key) @@ -276,9 +279,11 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) - monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) + module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" module_path += "stagingmanagement/stage-image" @@ -321,7 +326,7 @@ def mock_controller_version(*args) -> None: controller_version_patch += "ImageStage._populate_controller_version" monkeypatch.setattr(controller_version_patch, mock_controller_version) - def mock_dcnm_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_stage_00008a" return responses_image_stage(key) @@ -329,8 +334,9 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00008a" return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_IMAGE_STAGE, mock_dcnm_send_image_stage) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) instance.serial_numbers = ["FDO21120U5D"] instance.commit() From eb76baaacefbae26ed1f600b55266636eefd746c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 31 Jan 2024 13:05:51 -1000 Subject: [PATCH 239/300] Add unit test for ImageStage for len(serial_num) == 0 --- .../module_utils/image_mgmt/image_stage.py | 20 ++++-- ...e_upgrade_responses_SwitchIssuDetails.json | 28 ++++++-- .../test_image_upgrade_image_stage.py | 66 ++++++++++++++++--- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index ab6aaecd6..e23c1061e 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -186,10 +186,20 @@ def commit(self): self.log.debug(msg) if len(self.serial_numbers) == 0: - msg = "No serial numbers to stage." - self.properties["response"] = {"response": msg} - self.properties["response_data"] = {"response": msg} - self.properties["result"] = {"success": True} + msg = "No files to stage." + response_current = { + "DATA": [ + { + "key": "ALL", + "value": msg + } + ] + } + self.response_current = response_current + self.response = response_current + self.response_data = response_current.get("DATA", "No Stage DATA") + self.result = {"changed": False, "success": True} + self.result_current = {"changed": False, "success": True} return self.prune_serial_numbers() @@ -227,6 +237,8 @@ def commit(self): self.log.debug(msg) msg = f"result: {json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) + msg = f"self.response_data: {self.response_data}" + self.log.debug(msg) if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 02dd3d8ef..47ac68a8b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -808,6 +808,26 @@ } }, "test_image_mgmt_stage_00009a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "Using only for RETURN_CODE == 200" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success" + } + ], + "message": "" + } + }, + "test_image_mgmt_stage_00020a": { "TEST_NOTES": [ "RETURN_CODE == 200", "DATA.lastOperDataObject.serialNumber is present and is this specific value", @@ -842,7 +862,7 @@ "message": "" } }, - "test_image_mgmt_stage_00010a": { + "test_image_mgmt_stage_00021a": { "TEST_NOTES": [ "RETURN_CODE == 200", "Entries for both serial numbers FDO21120U5D FDO2112189M are present", @@ -877,7 +897,7 @@ "message": "" } }, - "test_image_mgmt_stage_00011a": { + "test_image_mgmt_stage_00022a": { "TEST_NOTES": [ "RETURN_CODE == 200", "Entries for both serial numbers FDO21120U5D FDO2112189M are present", @@ -912,7 +932,7 @@ "message": "" } }, - "test_image_mgmt_stage_00012a": { + "test_image_mgmt_stage_00030a": { "TEST_NOTES": [ "FDO21120U5D upgrade, validated, imageStaged == Success", "FDO2112189M upgrade, validated, imageStaged == Success" @@ -940,7 +960,7 @@ "message": "" } }, - "test_image_mgmt_stage_00013a": { + "test_image_mgmt_stage_00031a": { "TEST_NOTES": [ "FDO21120U5D upgrade, validated, imageStaged == Success", "FDO2112189M upgrade, validated == Success", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 49da259a3..ad354e1a9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -345,6 +345,52 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: def test_image_mgmt_stage_00009( + monkeypatch, image_stage) -> None: + """ + Function + - commit + + Setup + - SwitchIssuDetailsBySerialNumber is mocked to return a successful response + - self.serial_numbers is set to [] (empty list) + + Test + - commit() sets the following to expected values: + - self.result, self.result_current + - self.response, self.response_current + - self.response_data + + Description + When len(serial_numbers) == 0, commit() will set result and + response properties, and return without doing anything else. + """ + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: + key = "test_image_mgmt_stage_00009a" + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) + + response_msg = "No files to stage." + with does_not_raise(): + instance = image_stage + instance.serial_numbers = [] + instance.commit() + assert instance.result == [{"success": True, "changed": False}] + assert instance.result_current == {"success": True, "changed": False} + assert instance.response_current == { + "DATA": [ + { + "key": "ALL", + "value": response_msg + } + ] + } + assert instance.response == [instance.response_current] + assert instance.response_data == [instance.response_current.get("DATA")] + + +def test_image_mgmt_stage_00020( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -367,7 +413,7 @@ def test_image_mgmt_stage_00009( instance = image_stage def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00009a" + key = "test_image_mgmt_stage_00020a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -385,7 +431,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_stage_00010( +def test_image_mgmt_stage_00021( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -410,7 +456,7 @@ def test_image_mgmt_stage_00010( instance = image_stage def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00010a" + key = "test_image_mgmt_stage_00021a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -432,7 +478,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_stage_00011( +def test_image_mgmt_stage_00022( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -455,7 +501,7 @@ def test_image_mgmt_stage_00011( instance = image_stage def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00011a" + key = "test_image_mgmt_stage_00022a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -481,7 +527,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_stage_00012( +def test_image_mgmt_stage_00030( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -508,7 +554,7 @@ def test_image_mgmt_stage_00012( instance = image_stage def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00012a" + key = "test_image_mgmt_stage_00030a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -526,7 +572,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_stage_00013( +def test_image_mgmt_stage_00031( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -543,12 +589,12 @@ def test_image_mgmt_stage_00013( imageStaged == "In-Progress" Description - See test_image_mgmt_stage_00012 + See test_image_mgmt_stage_00030 for functional details. """ instance = image_stage def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00013a" + key = "test_image_mgmt_stage_00031a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) From fe37a6a4b7295c0c704661f371172ba15012827e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 31 Jan 2024 13:13:41 -1000 Subject: [PATCH 240/300] Fix PEP8 errors in sanity tests --- .../module_utils/image_mgmt/image_stage.py | 9 +- .../test_image_upgrade_image_stage.py | 17 ++- .../test_image_upgrade_image_upgrade.py | 112 +++++++++++++----- 3 files changed, 91 insertions(+), 47 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index e23c1061e..e6553d76c 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -187,14 +187,7 @@ def commit(self): if len(self.serial_numbers) == 0: msg = "No files to stage." - response_current = { - "DATA": [ - { - "key": "ALL", - "value": msg - } - ] - } + response_current = {"DATA": [{"key": "ALL", "value": msg}]} self.response_current = response_current self.response = response_current self.response_data = response_current.get("DATA", "No Stage DATA") diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index ad354e1a9..b4fde8088 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -50,7 +50,9 @@ PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." PATCH_COMMON = PATCH_MODULE_UTILS + "common." PATCH_IMAGE_STAGE_REST_SEND_COMMIT = PATCH_IMAGE_MGMT + "image_stage.RestSend.commit" -PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT = PATCH_IMAGE_MGMT + "image_stage.RestSend.result_current" +PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT = ( + PATCH_IMAGE_MGMT + "image_stage.RestSend.result_current" +) DCNM_SEND_CONTROLLER_VERSION = PATCH_COMMON + "controller_version.dcnm_send" DCNM_SEND_IMAGE_STAGE = PATCH_IMAGE_MGMT + "image_stage.dcnm_send" @@ -344,8 +346,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert expected_serial_number_key in instance.payload.keys() -def test_image_mgmt_stage_00009( - monkeypatch, image_stage) -> None: +def test_image_mgmt_stage_00009(monkeypatch, image_stage) -> None: """ Function - commit @@ -364,6 +365,7 @@ def test_image_mgmt_stage_00009( When len(serial_numbers) == 0, commit() will set result and response properties, and return without doing anything else. """ + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_mgmt_stage_00009a" return responses_switch_issu_details(key) @@ -379,13 +381,8 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert instance.result == [{"success": True, "changed": False}] assert instance.result_current == {"success": True, "changed": False} assert instance.response_current == { - "DATA": [ - { - "key": "ALL", - "value": response_msg - } - ] - } + "DATA": [{"key": "ALL", "value": response_msg}] + } assert instance.response == [instance.response_current] assert instance.response_data == [instance.response_current.get("DATA")] diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 75fec9bf9..f2aa0b773 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -37,8 +37,7 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ ImageUpgrade -from .image_upgrade_utils import (does_not_raise, - image_upgrade_fixture, +from .image_upgrade_utils import (does_not_raise, image_upgrade_fixture, issu_details_by_ip_address_fixture, payloads_image_upgrade, responses_image_install_options, @@ -48,9 +47,15 @@ PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT = PATCH_IMAGE_MGMT + "image_upgrade.RestSend.commit" -PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT = PATCH_IMAGE_MGMT + "image_upgrade.RestSend.response_current" -PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT = PATCH_IMAGE_MGMT + "image_upgrade.RestSend.result_current" +PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT = ( + PATCH_IMAGE_MGMT + "image_upgrade.RestSend.commit" +) +PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT = ( + PATCH_IMAGE_MGMT + "image_upgrade.RestSend.response_current" +) +PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT = ( + PATCH_IMAGE_MGMT + "image_upgrade.RestSend.result_current" +) REST_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.RestSend" DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" @@ -300,7 +305,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True}) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -389,7 +396,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True}) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -469,7 +478,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -554,7 +565,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True}) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -638,7 +651,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True}) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -720,7 +735,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -801,7 +818,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -883,7 +902,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -968,7 +989,9 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -1048,7 +1071,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -1129,7 +1154,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -1210,7 +1237,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -1296,7 +1325,9 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -1378,9 +1409,16 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key)) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {'success': False, 'changed': False}) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key) + ) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, + {"success": False, "changed": False}, + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -1558,9 +1596,15 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key)) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {'success': True, 'changed': True}) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key) + ) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True, "changed": True} + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -1635,8 +1679,12 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {'success': True, 'changed': True}) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True, "changed": True} + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -1712,9 +1760,15 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key)) - monkeypatch.setattr(PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {'success': True, 'changed': True}) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade + ) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT, responses_image_upgrade(key) + ) + monkeypatch.setattr( + PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT, {"success": True, "changed": True} + ) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) From 8e6ebfb0e09a90f88dcdc7c3e7677c290959ae40 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 31 Jan 2024 14:00:57 -1000 Subject: [PATCH 241/300] ImageStage: Add unit test for 500 response --- .../module_utils/image_mgmt/image_stage.py | 2 +- .../image_upgrade_responses_ImageStage.json | 16 +++++++ ...e_upgrade_responses_SwitchIssuDetails.json | 20 +++++++++ .../test_image_upgrade_image_stage.py | 45 +++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index e6553d76c..879534dd7 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -235,7 +235,7 @@ def commit(self): if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " - msg = f"failed: {self.result_current}. " + msg += f"failed: {self.result_current}. " msg += f"Controller response: {self.response_current}" self.module.fail_json(msg, **self.failed_result) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json index 140c3bac4..9e37988f0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json @@ -45,5 +45,21 @@ ], "message": "" } + }, + "test_image_mgmt_stage_00010a": { + "TEST_NOTES": [ + "RETURN_CODE == 500", + "MESSAGE == NOK" + ], + "RETURN_CODE": 500, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-info", + "MESSAGE": "NOK", + "DATA": { + "status": "FAILED", + "lastOperDataObject": [ + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 47ac68a8b..2df3c4354 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -827,6 +827,26 @@ "message": "" } }, + "test_image_mgmt_stage_00010a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "Using only for RETURN_CODE == 200" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success" + } + ], + "message": "" + } + }, "test_image_mgmt_stage_00020a": { "TEST_NOTES": [ "RETURN_CODE == 200", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index b4fde8088..8c229f002 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -53,6 +53,10 @@ PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT = ( PATCH_IMAGE_MGMT + "image_stage.RestSend.result_current" ) +PATCH_IMAGE_STAGE_POPULATE_CONTROLLER_VERSION = ( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade." + "ImageStage._populate_controller_version" +) DCNM_SEND_CONTROLLER_VERSION = PATCH_COMMON + "controller_version.dcnm_send" DCNM_SEND_IMAGE_STAGE = PATCH_IMAGE_MGMT + "image_stage.dcnm_send" @@ -387,6 +391,47 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert instance.response_data == [instance.response_current.get("DATA")] +def test_image_mgmt_stage_00010(monkeypatch, image_stage) -> None: + """ + Function + - commit + + Setup + - IssuDetailsBySerialNumber is mocked to return a successful response + - ImageStage is mocked to return a non-successful response + + Test + - commit() will call fail_json() + + Description + commit() will call fail_json() on non-success response from the controller. + """ + + def mock_controller_version(*args) -> None: + instance.controller_version = "12.1.3b" + + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: + key = "test_image_mgmt_stage_00010a" + return responses_switch_issu_details(key) + + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_mgmt_stage_00010a" + return responses_image_stage(key) + + monkeypatch.setattr( + PATCH_IMAGE_STAGE_POPULATE_CONTROLLER_VERSION, mock_controller_version + ) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": False}) + + instance = image_stage + instance.serial_numbers = ["FDO21120U5D"] + MATCH = "ImageStage.commit: failed" + with pytest.raises(AnsibleFailJson, match=MATCH): + instance.commit() + + def test_image_mgmt_stage_00020( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: From f8e7b06deb8aa7258b31520fd5b32e624eeec2d1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 31 Jan 2024 15:00:42 -1000 Subject: [PATCH 242/300] ImageStage: add unit tests, more... Add unit tests for the following properties: - check_interval - check_timeout - serial_numbers --- .../module_utils/image_mgmt/image_stage.py | 15 +-- .../test_image_upgrade_image_stage.py | 93 +++++++++++++++++++ 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 879534dd7..15166936c 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -352,8 +352,7 @@ def serial_numbers(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, list): msg = f"{self.class_name}.{method_name}: " - msg += "instance.serial_numbers must be a " - msg += "python list of switch serial numbers." + msg += "must be a python list of switch serial numbers." self.module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value @@ -367,9 +366,11 @@ def check_interval(self): @check_interval.setter def check_interval(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += "must be a positive integer or zero." if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: instance.check_interval must " - msg += "be an integer." + self.module.fail_json(msg, **self.failed_result) + if value < 0: self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @@ -383,8 +384,10 @@ def check_timeout(self): @check_timeout.setter def check_timeout(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += "must be a positive integer or zero." if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: instance.check_timeout must " - msg += "be an integer." + self.module.fail_json(msg, **self.failed_result) + if value < 0: self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 8c229f002..08864cd0c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -660,3 +660,96 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert len(instance.serial_numbers_done) == 1 assert "FDO21120U5D" in instance.serial_numbers_done assert "FDO2112189M" not in instance.serial_numbers_done + + +MATCH_00040 = "ImageStage.check_interval: must be a positive integer or zero." + + +@pytest.mark.parametrize( + "input, output, context", + [ + (-1, None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + (10, 10, does_not_raise()), + ("a", None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), + ], +) +def test_image_mgmt_stage_00040(image_stage, input, output, context) -> None: + """ + Function + - check_interval + + Test + - Verify inputs to check_interval property + + Description + check_interval expects a positive integer value, or zero. + """ + with does_not_raise(): + instance = image_stage + with context: + instance.check_interval = input + if output is not None: + assert instance.check_interval == output + + +MATCH_00050 = "ImageStage.check_timeout: must be a positive integer or zero." + + +@pytest.mark.parametrize( + "input, output, context", + [ + (-1, None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), + (10, 10, does_not_raise()), + ("a", None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), + ], +) +def test_image_mgmt_stage_00050(image_stage, input, output, context) -> None: + """ + Function + - check_interval + + Test + - Verify inputs to check_timeout property + + Description + check_timeout expects a positive integer value, or zero. + """ + with does_not_raise(): + instance = image_stage + with context: + instance.check_timeout = input + if output is not None: + assert instance.check_timeout == output + + +MATCH_00060 = ( + "ImageStage.serial_numbers: must be a python list of switch serial numbers." +) + + +@pytest.mark.parametrize( + "input, output, context", + [ + ("foo", None, pytest.raises(AnsibleFailJson, match=MATCH_00060)), + (10, None, pytest.raises(AnsibleFailJson, match=MATCH_00060)), + (["DD001115F"], ["DD001115F"], does_not_raise()), + ], +) +def test_image_mgmt_stage_00060(image_stage, input, output, context) -> None: + """ + Function + - serial_numbers + + Test + - Verify inputs to serial_numbers property + - Verify that fail_json is called if the input is not a list + + Description + serial_numbers expects a list of serial numbers. + """ + with does_not_raise(): + instance = image_stage + with context: + instance.serial_numbers = input + if output is not None: + assert instance.serial_numbers == output From 3bc52c8d6487476cc14c22494d0e31a29d2a5378 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 31 Jan 2024 20:39:17 -1000 Subject: [PATCH 243/300] ImageValidate: leverage RestSend, more... Also: ImageValidate: hardening for check_interval and check_timeout setters. ImageValidate: Remove response_data, result, and response propoerties (these are now inherited from ImageUpgradeCommon) test_image_upgrade_image_validate.py: modifications for changes made above. --- .../module_utils/image_mgmt/image_validate.py | 104 ++++++++---------- plugins/modules/dcnm_image_upgrade.py | 6 +- .../test_image_upgrade_image_validate.py | 44 +++++--- 3 files changed, 79 insertions(+), 75 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 244fb662b..298747fb2 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -29,10 +29,10 @@ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsBySerialNumber -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ - dcnm_send class ImageValidate(ImageUpgradeCommon): @@ -76,6 +76,7 @@ def __init__(self, module): self.log.debug("ENTERED ImageValidate()") self.endpoints = ApiEndpoints() + self.rest_send = RestSend(self.module) self.path = self.endpoints.image_validate.get("path") self.verb = self.endpoints.image_validate.get("verb") @@ -93,9 +94,6 @@ def _init_properties(self) -> None: # self.properties is already initialized in the parent class self.properties["check_interval"] = 10 # seconds self.properties["check_timeout"] = 1800 # seconds - self.properties["response_data"] = {} - self.properties["result"] = {} - self.properties["response"] = {} self.properties["non_disruptive"] = False self.properties["serial_numbers"] = [] @@ -177,22 +175,41 @@ def commit(self) -> None: self._wait_for_current_actions_to_complete() self.build_payload() - self.properties["response"] = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(self.payload) - ) - self.properties["result"] = self._handle_response(self.response, self.verb) + self.rest_send.verb = self.verb + self.rest_send.path = self.path + self.rest_send.payload = self.payload - msg = f"payload: {self.payload}" + self.rest_send.commit() + + msg = f"self.rest_send.response_current: {self.rest_send.response_current}" + self.log.debug(msg) + + self.response = self.rest_send.response_current + self.response_current = self.rest_send.response_current + self.response_data = self.response_current.get("DATA", "No Stage DATA") + + self.result = self.rest_send.result_current + self.result_current = self.rest_send.result_current + + msg = f"self.rest_send.result_current: {self.rest_send.result_current}" self.log.debug(msg) - msg = f"response: {self.response}" + msg = f"self.payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"result: {self.result}" + msg = f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"self.result: {json.dumps(self.result, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"self.result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"self.response_data: {self.response_data}" self.log.debug(msg) - if not self.result["success"]: + if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " - msg = f"failed: {self.result}. " - msg += f"Controller response: {self.response}" + msg += f"failed: {self.result_current}. " + msg += f"Controller response: {self.response_current}" self.module.fail_json(msg, **self.failed_result) self.properties["response_data"] = self.response @@ -343,30 +360,6 @@ def non_disruptive(self, value): self.properties["non_disruptive"] = value - @property - def response_data(self): - """ - Return the result of the image staging request - for serial_numbers. - - instance.serial_numbers must be set first. - """ - return self.properties.get("response_data") - - @property - def result(self): - """ - Return the POST result - """ - return self.properties.get("result") - - @property - def response(self): - """ - Return the POST response from the controller - """ - return self.properties.get("response") - @property def check_interval(self): """ @@ -376,20 +369,17 @@ def check_interval(self): @check_interval.setter def check_interval(self, value): - self.method_name = inspect.stack()[0][3] - - result = True + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += "must be a positive integer or zero. " + msg += f"Got value {value} of type {type(value)}." # isinstance(True, int) is True so we need to check for bool first if isinstance(value, bool): - result = False + self.module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - result = False - if result is False: - msg = f"{self.class_name}.{self.method_name}: " - msg += "instance.check_interval must be an integer. " - msg += f"Got {value}." self.module.fail_json(msg, **self.failed_result) - + if value < 0: + self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @property @@ -401,17 +391,15 @@ def check_timeout(self): @check_timeout.setter def check_timeout(self, value): - self.method_name = inspect.stack()[0][3] - - result = True + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += "must be a positive integer or zero. " + msg += f"Got value {value} of type {type(value)}." # isinstance(True, int) is True so we need to check for bool first if isinstance(value, bool): - result = False + self.module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - result = False - if result is False: - msg = f"{self.class_name}.{self.method_name}: " - msg += "instance.check_timeout must be an integer. " - msg += f"Got {value}." + self.module.fail_json(msg, **self.failed_result) + if value < 0: self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 76dc696fe..ff3f9f038 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1069,8 +1069,10 @@ def _validate_images(self, serial_numbers) -> None: instance.commit() for diff in instance.diff: self.task_result.diff_validate = copy.deepcopy(diff) - instance.response.pop("DATA", None) - self.task_result.response_validate = instance.response + for response in instance.response: + if "DATA" in response: + response.pop("DATA") + self.task_result.response_validate = copy.deepcopy(response) def _verify_install_options(self, devices) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 72be7dc72..347c0e825 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -47,6 +47,12 @@ PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." DCNM_SEND_IMAGE_VALIDATE = PATCH_IMAGE_MGMT + "image_validate.dcnm_send" DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +PATCH_IMAGE_VALIDATE_REST_SEND_COMMIT = ( + PATCH_IMAGE_MGMT + "image_validate.RestSend.commit" +) +PATCH_IMAGE_VALIDATE_REST_SEND_RESULT_CURRENT = ( + PATCH_IMAGE_MGMT + "image_validate.RestSend.result_current" +) def test_image_mgmt_validate_00001(image_validate) -> None: @@ -81,9 +87,9 @@ def test_image_mgmt_validate_00002(image_validate) -> None: assert isinstance(instance.properties, dict) assert instance.properties.get("check_interval") == 10 assert instance.properties.get("check_timeout") == 1800 - assert instance.properties.get("response_data") == {} - assert instance.properties.get("response") == {} - assert instance.properties.get("result") == {} + assert instance.properties.get("response_data") == [] + assert instance.properties.get("response") == [] + assert instance.properties.get("result") == [] assert instance.properties.get("non_disruptive") is False assert instance.properties.get("serial_numbers") == [] @@ -413,7 +419,7 @@ def test_image_mgmt_validate_00021(monkeypatch, image_validate) -> None: """ # Needed only for the 200 return code - def mock_dcnm_send_image_validate(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_image_validate(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_validate_00021a" return responses_image_validate(key) @@ -421,7 +427,12 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_validate_00021a" return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_IMAGE_VALIDATE, mock_dcnm_send_image_validate) + monkeypatch.setattr( + PATCH_IMAGE_VALIDATE_REST_SEND_COMMIT, mock_rest_send_image_validate + ) + monkeypatch.setattr( + PATCH_IMAGE_VALIDATE_REST_SEND_RESULT_CURRENT, {"success": True} + ) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" @@ -461,12 +472,11 @@ def test_image_mgmt_validate_00023(monkeypatch, image_validate) -> None: - commit Test - - 501 response from controller endpoint: - POST /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image + - fail_json is called on 501 response from controller """ # Needed only for the 501 return code - def mock_dcnm_send_image_validate(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_image_validate(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_validate_00023a" return responses_image_validate(key) @@ -474,17 +484,21 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_validate_00023a" return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_IMAGE_VALIDATE, mock_dcnm_send_image_validate) + monkeypatch.setattr( + PATCH_IMAGE_VALIDATE_REST_SEND_COMMIT, mock_rest_send_image_validate + ) + monkeypatch.setattr( + PATCH_IMAGE_VALIDATE_REST_SEND_RESULT_CURRENT, + {"success": False, "changed": False}, + ) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) with does_not_raise(): instance = image_validate instance.serial_numbers = ["FDO21120U5D"] - with pytest.raises(AnsibleFailJson, match="failed:"): + MATCH = "ImageValidate.commit: failed: " + with pytest.raises(AnsibleFailJson, match=MATCH): instance.commit() - assert instance.result["success"] is False - assert instance.result["changed"] is False - assert instance.response["RETURN_CODE"] == 501 MATCH_00030 = "ImageValidate.serial_numbers: " @@ -555,7 +569,7 @@ def test_image_mgmt_validate_00040(image_validate, value, expected) -> None: MATCH_00050 = "ImageValidate.check_interval: " -MATCH_00050 += "instance.check_interval must be an integer." +MATCH_00050 += "must be a positive integer or zero." @pytest.mark.parametrize( @@ -587,7 +601,7 @@ def test_image_mgmt_validate_00050(image_validate, value, expected) -> None: MATCH_00060 = "ImageValidate.check_timeout: " -MATCH_00060 += "instance.check_timeout must be an integer." +MATCH_00060 += "must be a positive integer or zero." @pytest.mark.parametrize( From 3a480b0e728019a8906a2ac253ddfa17d65d3e41 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 1 Feb 2024 10:50:25 -1000 Subject: [PATCH 244/300] SwitchDetails: Leverage RestSend Also: RestSend: Add getter/setter property for payload RestSend: Remove a couple debug statements ImageStage: improve fail_json messages for check_interval and check_timeout properties Update the following unit test files to reflect the above changes: - test_image_upgrade_switch_details.py - test_image_upgrade_image_policy_action.py Minor update to docstring for test_image_mgmt_stasge_00010 in: - test_image_upgrade_image_stage.py --- .../module_utils/image_mgmt/image_stage.py | 12 ++- plugins/module_utils/image_mgmt/rest_send.py | 18 ++-- .../module_utils/image_mgmt/switch_details.py | 67 ++++++------- .../test_image_upgrade_image_policy_action.py | 6 +- .../test_image_upgrade_image_stage.py | 2 +- .../test_image_upgrade_switch_details.py | 94 +++++++++++++------ 6 files changed, 126 insertions(+), 73 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 15166936c..36ef89b5d 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -367,7 +367,11 @@ def check_interval(self): def check_interval(self, value): method_name = inspect.stack()[0][3] msg = f"{self.class_name}.{method_name}: " - msg += "must be a positive integer or zero." + msg += "must be a positive integer or zero. " + msg += f"Got value {value} of type {type(value)}." + # isinstance(True, int) is True so we need to check for bool first + if isinstance(value, bool): + self.module.fail_json(msg, **self.failed_result) if not isinstance(value, int): self.module.fail_json(msg, **self.failed_result) if value < 0: @@ -385,7 +389,11 @@ def check_timeout(self): def check_timeout(self, value): method_name = inspect.stack()[0][3] msg = f"{self.class_name}.{method_name}: " - msg += "must be a positive integer or zero." + msg += "must be a positive integer or zero. " + msg += f"Got value {value} of type {type(value)}." + # isinstance(True, int) is True so we need to check for bool first + if isinstance(value, bool): + self.module.fail_json(msg, **self.failed_result) if not isinstance(value, int): self.module.fail_json(msg, **self.failed_result) if value < 0: diff --git a/plugins/module_utils/image_mgmt/rest_send.py b/plugins/module_utils/image_mgmt/rest_send.py index 0ddc464c0..88f37df21 100644 --- a/plugins/module_utils/image_mgmt/rest_send.py +++ b/plugins/module_utils/image_mgmt/rest_send.py @@ -50,7 +50,7 @@ def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED SendRest()" + msg = "ENTERED RestSend()" self.log.debug(msg) self.ansible_module = ansible_module @@ -103,11 +103,6 @@ def commit(self): success = False msg = f"{caller}: Entering commit loop. " - msg += f"timeout {timeout}, send_interval {self.send_interval}" - self.log.debug(msg) - msg = f"verb {self.verb}, path {self.path}," - self.log.debug(msg) - msg = f"payload {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) while timeout > 0 and success is False: @@ -240,6 +235,17 @@ def failed_result(self): """ return ImageUpgradeTaskResult(None).failed_result + @property + def payload(self): + """ + Return the payload to send to the controller + """ + return self.properties["payload"] + + @payload.setter + def payload(self, value): + self.properties["payload"] = value + @property def response_current(self): """ diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_mgmt/switch_details.py index ae2566989..5f904b509 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_mgmt/switch_details.py @@ -19,14 +19,15 @@ __author__ = "Allen Robel" import inspect +import json import logging from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ - dcnm_send +from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.rest_send import \ + RestSend class SwitchDetails(ImageUpgradeCommon): @@ -57,14 +58,13 @@ def __init__(self, module): self.log.debug("ENTERED SwitchDetails()") self.endpoints = ApiEndpoints() + self.rest_send = RestSend(self.module) self._init_properties() def _init_properties(self): # self.properties is already initialized in the parent class self.properties["ip_address"] = None - self.properties["response_data"] = None - self.properties["response"] = None - self.properties["result"] = None + self.properties["info"] = {} def refresh(self): """ @@ -78,19 +78,36 @@ def refresh(self): path = self.endpoints.switches_info.get("path") verb = self.endpoints.switches_info.get("verb") - self.properties["response"] = dcnm_send(self.module, verb, path) - self.properties["result"] = self._handle_response(self.response, verb) + self.rest_send.verb = verb + self.rest_send.path = path + self.rest_send.commit() - if self.response["RETURN_CODE"] != 200: + msg = f"self.rest_send.response_current: {self.rest_send.response_current}" + self.log.debug(msg) + + msg = f"self.rest_send.result_current: {self.rest_send.result_current}" + self.log.debug(msg) + + self.response = self.rest_send.response_current + self.response_current = self.rest_send.response_current + self.response_data = self.response_current.get("DATA", "No_DATA_SwitchDetails") + + self.result = self.rest_send.result_current + self.result_current = self.rest_send.result_current + + if self.response_current["RETURN_CODE"] != 200: msg = f"{self.class_name}.{method_name}: " msg += "Unable to retrieve switch information from the controller. " - msg += f"Got response {self.response}" + msg += f"Got response {self.response_current}" self.module.fail_json(msg, **self.failed_result) - data = self.response.get("DATA") - self.properties["response_data"] = {} + data = self.response_current.get("DATA") + self.properties["info"] = {} for switch in data: - self.properties["response_data"][switch["ipAddress"]] = switch + self.properties["info"][switch["ipAddress"]] = switch + + msg = f"self.properties[info]: {json.dumps(self.properties['info'], indent=4, sort_keys=True)}" + self.log.debug(msg) def _get(self, item): method_name = inspect.stack()[0][3] @@ -101,18 +118,18 @@ def _get(self, item): msg += f"property {item}." self.module.fail_json(msg, **self.failed_result) - if self.ip_address not in self.properties["response_data"]: + if self.ip_address not in self.properties["info"]: msg = f"{self.class_name}.{method_name}: " msg += f"{self.ip_address} does not exist on the controller." self.module.fail_json(msg, **self.failed_result) - if item not in self.properties["response_data"][self.ip_address]: + if item not in self.properties["info"][self.ip_address]: msg = f"{self.class_name}.{method_name}: " msg += f"{self.ip_address} does not have a key named {item}." self.module.fail_json(msg, **self.failed_result) return self.make_boolean( - self.make_none(self.properties["response_data"][self.ip_address].get(item)) + self.make_none(self.properties["info"][self.ip_address].get(item)) ) @property @@ -165,30 +182,14 @@ def model(self): return self._get("model") @property - def response_data(self): + def info(self): """ Return parsed data from the GET request. Return None otherwise NOTE: Keyed on ip_address """ - return self.properties["response_data"] - - @property - def response(self): - """ - Return the raw response from the GET request. - Return None otherwise - """ - return self.properties["response"] - - @property - def result(self): - """ - Return the raw result of the GET request. - Return None otherwise - """ - return self.properties["result"] + return self.propertiies["info"] @property def platform(self): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 48dfa4ad7..75f7c00fe 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -52,7 +52,7 @@ DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" -DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.dcnm_send" +REST_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.RestSend.commit" DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" @@ -411,7 +411,7 @@ def test_image_mgmt_image_policy_action_00021(monkeypatch, image_policy_action) def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) - def mock_dcnm_send_switch_details(*args) -> Dict[str, Any]: + def mock_rest_send_switch_details(*args) -> Dict[str, Any]: return responses_switch_details(key) def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: @@ -424,7 +424,7 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: monkeypatch.setattr( DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_common ) - monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) + monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) monkeypatch.setattr( DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 08864cd0c..33a52bc4d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -398,7 +398,7 @@ def test_image_mgmt_stage_00010(monkeypatch, image_stage) -> None: Setup - IssuDetailsBySerialNumber is mocked to return a successful response - - ImageStage is mocked to return a non-successful response + - ImageStage is mocked to return a non-successful (500) response Test - commit() will call fail_json() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 974d9fafb..8c9477172 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -41,7 +41,14 @@ PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.dcnm_send" +PATCH_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details." +PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT = ( + PATCH_SWITCH_DETAILS + "RestSend.response_current" +) +PATCH_SWITCH_DETAILS_REST_SEND_RESULT_CURRENT = ( + PATCH_SWITCH_DETAILS + "RestSend.result_current" +) +REST_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.RestSend.commit" def test_image_mgmt_switch_details_00001(switch_details) -> None: @@ -69,9 +76,11 @@ def test_image_mgmt_switch_details_00002(switch_details) -> None: assert isinstance(instance.properties, dict) assert instance.properties.get("ip_address") is None - assert instance.properties.get("response_data") is None - assert instance.properties.get("response") is None - assert instance.properties.get("result") is None + assert instance.properties.get("response_data") == [] + assert instance.properties.get("response") == [] + assert instance.properties.get("response_current") == {} + assert instance.properties.get("result") == [] + assert instance.properties.get("result_current") == {} def test_image_mgmt_switch_details_00020(monkeypatch, switch_details) -> None: @@ -79,21 +88,33 @@ def test_image_mgmt_switch_details_00020(monkeypatch, switch_details) -> None: Function - refresh - Test - - response_data, response, result are dictionaries + Test (X == SwitchDetails) + - X.response_data, X.response, X.result are lists + - X.response_current, X.result_current are dictionaries + - X.response_current, X.result_current are set to the mocked RestSend values """ - instance = switch_details + key = "test_image_mgmt_switch_details_00020a" - def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_details_00020a" + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_details(key) - monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) - - instance.refresh() - assert isinstance(instance.response_data, dict) - assert isinstance(instance.result, dict) - assert isinstance(instance.response, dict) + monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT, mock_rest_send_switch_details() + ) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESULT_CURRENT, {"success": True, "found": True} + ) + with does_not_raise(): + instance = switch_details + instance.refresh() + assert isinstance(instance.response_data, list) + assert isinstance(instance.result, list) + assert isinstance(instance.response, list) + assert isinstance(instance.response_current, dict) + assert isinstance(instance.result_current, dict) + assert instance.result_current == {"success": True, "found": True} + assert instance.response_current == responses_switch_details(key) def test_image_mgmt_switch_details_00021(monkeypatch, switch_details) -> None: @@ -108,14 +129,20 @@ def test_image_mgmt_switch_details_00021(monkeypatch, switch_details) -> None: """ instance = switch_details - def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00021a" return responses_switch_details(key) - monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) + monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT, mock_rest_send_switch_details() + ) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESULT_CURRENT, {"success": True, "found": True} + ) instance.refresh() - assert isinstance(instance.response_data, dict) + assert isinstance(instance.response_data, list) instance.ip_address = "172.22.150.110" assert instance.hostname == "cvd-1111-bgw" instance.ip_address = "172.22.150.111" @@ -153,8 +180,7 @@ def test_image_mgmt_switch_details_00022( """ Function - switch_details.refresh - - switch_details.result - - ImageUpgradeCommon._handle_response + - RestSend._handle_response Test - test_image_mgmt_switch_details_00022a @@ -169,10 +195,13 @@ def test_image_mgmt_switch_details_00022( """ instance = switch_details - def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_details(key) - monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) + monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT, mock_rest_send_switch_details() + ) with expected: instance.refresh() @@ -222,11 +251,14 @@ def test_image_mgmt_switch_details_00023( """ instance = switch_details - def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00023a" return responses_switch_details(key) - monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) + monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT, mock_rest_send_switch_details() + ) instance.refresh() instance.ip_address = "172.22.150.110" @@ -252,11 +284,14 @@ def test_image_mgmt_switch_details_00024(monkeypatch, switch_details) -> None: """ instance = switch_details - def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00024a" return responses_switch_details(key) - monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) + monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT, mock_rest_send_switch_details() + ) match = "SwitchDetails._get: 1.1.1.1 does not exist " match += "on the controller." @@ -284,11 +319,14 @@ def test_image_mgmt_switch_details_00025(monkeypatch, switch_details) -> None: """ instance = switch_details - def mock_dcnm_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_mgmt_switch_details_00025a" return responses_switch_details(key) - monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) + monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT, mock_rest_send_switch_details() + ) match = "SwitchDetails._get: 172.22.150.110 does not have a key named FOO." From 57c855460b3e5abed4a6459d9d32af58aea6b777 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 4 Feb 2024 10:51:14 -1000 Subject: [PATCH 245/300] response and result properties should be inherited - SwitchIssuDetails: inherit response* and result* properties from ImageUpgradeCommon - ImageUpgrade: inherit response_data from ImageUpgradeCommon - ImageUpgrade: reorder some items in __init__() - ImageUpgrade: add some debug statements for result, response, response_data, etc - ImageUpgrade: Modify fail_json conditional to read self.result_current rather than rest_send.result_current - ImageUpgradeTask: pop "DATA" only if present in response_current - ImageStage: move debug message - ImagePolicies: fix typo in fail_json message - Update unit tests to reflect above changes - Run everthing through black/isort --- .../module_utils/image_mgmt/image_policies.py | 2 +- .../module_utils/image_mgmt/image_stage.py | 5 +- .../module_utils/image_mgmt/image_upgrade.py | 46 +++++----- .../image_mgmt/switch_issu_details.py | 26 ------ plugins/modules/dcnm_image_upgrade.py | 16 ++-- .../test_image_upgrade_image_policy_action.py | 6 +- .../test_image_upgrade_image_stage.py | 91 +++++++++---------- ...rade_switch_issu_details_by_device_name.py | 13 +-- ...grade_switch_issu_details_by_ip_address.py | 8 +- ...de_switch_issu_details_by_serial_number.py | 9 +- 10 files changed, 98 insertions(+), 124 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index 97966ec99..abea2e4aa 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -88,7 +88,7 @@ def refresh(self): if not self.result["success"]: msg = f"{self.class_name}.{self.method_name}: " - msg += "Bad result when retriving image policy " + msg += "Bad result when retrieving image policy " msg += "information from the controller." self.module.fail_json(msg, **self.failed_result) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 36ef89b5d..154bac3cf 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -176,14 +176,15 @@ def commit(self): msg = "ENTERED commit()" self.log.debug(msg) + msg = f"self.serial_numbers: {self.serial_numbers}" + self.log.debug(msg) + if self.serial_numbers is None: msg = f"{self.class_name}.{method_name}: " msg += "call instance.serial_numbers " msg += "before calling commit." self.module.fail_json(msg, **self.failed_result) - msg = f"self.serial_numbers: {self.serial_numbers}" - self.log.debug(msg) if len(self.serial_numbers) == 0: msg = "No files to stage." diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index a6709a497..42fb83689 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -36,7 +36,6 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsByIpAddress - class ImageUpgrade(ImageUpgradeCommon): """ Endpoint: @@ -139,17 +138,16 @@ def __init__(self, module): self.log.debug("ENTERED ImageUpgrade()") self.endpoints = ApiEndpoints() + self.install_options = ImageInstallOptions(self.module) + self.rest_send = RestSend(self.module) + self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) self.ipv4_done = set() self.ipv4_todo = set() self.payload: Dict[str, Any] = {} self.path = self.endpoints.image_upgrade.get("path") self.verb = self.endpoints.image_upgrade.get("verb") - self.rest_send = RestSend(self.module) - self._init_properties() - self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) - self.install_options = ImageInstallOptions(self.module) def _init_properties(self) -> None: """ @@ -488,13 +486,28 @@ def commit(self) -> None: self.response_current = self.rest_send.response_current self.response_data = self.rest_send.response_current.get("DATA") - self.result_current = self.rest_send.result_current self.result = self.rest_send.result_current + self.result_current = self.rest_send.result_current + + msg = f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"self.response_data: {self.response_data}" + self.log.debug(msg) + + msg = f"self.result: {json.dumps(self.result, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"self.result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) - if not self.rest_send.result_current["success"]: + if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " - msg += f"failed: {self.rest_send.result_current}. " - msg += f"Controller response: {self.rest_send.response_current}" + msg += f"failed: {self.result_current}. " + msg += f"Controller response: {self.response_current}" self.module.fail_json(msg, **self.failed_result) # See image_upgrade_common.py for the definition of self.diff @@ -895,18 +908,3 @@ def check_timeout(self, value): msg = "instance.check_timeout must be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value - - @property - def response_data(self): - """ - Return the data retrieved from the controller for the - image upgrade request. - - instance.devices must be set first. - instance.commit() must be called first. - """ - return self.properties.get("response_data") - - @response_data.setter - def response_data(self, value): - self.properties["response_data"].append(value) diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 7f80b91a3..95631ec27 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -99,9 +99,6 @@ def __init__(self, module): def _init_properties(self): # self.properties is already initialized in the parent class - self.properties["response"] = None - self.properties["result"] = None - self.properties["response_data"] = None # action_keys is used in subclasses to determine if any actions # are in progress. # Property actions_in_progress returns True if so, False otherwise @@ -160,29 +157,6 @@ def _get(self, item): overridden in subclasses """ - @property - def response_data(self): - """ - Return the raw data retrieved from the controller - """ - return self.properties["response_data"] - - @property - def response(self): - """ - Return the raw response from the GET request. - Return None otherwise - """ - return self.properties["response"] - - @property - def result(self): - """ - Return the raw result of the GET request. - Return None otherwise - """ - return self.properties["result"] - @property def device_name(self): """ diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index ff3f9f038..449f07c0c 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1278,11 +1278,15 @@ def handle_query_state(self) -> None: """ instance = SwitchIssuDetailsByIpAddress(self.module) instance.refresh() - response = copy.deepcopy(instance.response) - response.pop("DATA") - self.task_result.response_issu_status = copy.deepcopy(response) + response_current = copy.deepcopy(instance.response_current) + if "DATA" in response_current: + response_current.pop("DATA") + self.task_result.response_issu_status = copy.deepcopy(response_current) for switch in self.need: instance.filter = switch.get("ip_address") + msg = f"SwitchIssuDetailsByIpAddress.filter: {instance.filter}, " + msg += f"SwitchIssuDetailsByIpAddress.filtered_data: {json.dumps(instance.filtered_data, indent=4, sort_keys=True)}" + self.log.debug(msg) if instance.filtered_data is None: continue self.task_result.diff_issu_status = instance.filtered_data @@ -1322,9 +1326,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - # collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - # config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - # log.config = config_file + collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 75f7c00fe..7300c05d5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -52,7 +52,7 @@ DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" -REST_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.RestSend.commit" +DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.RestSend.commit" DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" @@ -411,7 +411,7 @@ def test_image_mgmt_image_policy_action_00021(monkeypatch, image_policy_action) def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) - def mock_rest_send_switch_details(*args) -> Dict[str, Any]: + def mock_dcnm_send_switch_details(*args) -> Dict[str, Any]: return responses_switch_details(key) def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: @@ -424,7 +424,7 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: monkeypatch.setattr( DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_common ) - monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) + monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) monkeypatch.setattr( DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 33a52bc4d..ab24a803a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -59,7 +59,6 @@ ) DCNM_SEND_CONTROLLER_VERSION = PATCH_COMMON + "controller_version.dcnm_send" -DCNM_SEND_IMAGE_STAGE = PATCH_IMAGE_MGMT + "image_stage.dcnm_send" DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" @@ -152,12 +151,12 @@ def test_image_mgmt_stage_00004( prune_serial_numbers removes serial numbers from the list for which imageStaged == "Success" (TODO: AND policy == ) """ + key = "test_image_mgmt_stage_00004a" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00004a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) instance = image_stage instance.issu_detail = issu_details_by_serial_number @@ -194,12 +193,12 @@ def test_image_mgmt_stage_00005( number and raises fail_json if imageStaged == "Failed" for any serial number. """ + key = "test_image_mgmt_stage_00005a" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00005a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) match = "Image staging is failing for the following switch: " match += "cvd-2313-leaf, 172.22.150.108, FDO2112189M. " @@ -235,21 +234,19 @@ def test_image_mgmt_stage_00006( - fail_json is called when serial_numbers is None - fail_json is not called when serial_numbers is set """ + key = "test_image_mgmt_stage_00006a" def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00006a" return responses_controller_version(key) def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00006a" return responses_image_stage(key) - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00006a" + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) @@ -270,22 +267,20 @@ def test_image_mgmt_stage_00007(monkeypatch, image_stage) -> None: - ImageStage.path is set to: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image """ + key = "test_image_mgmt_stage_00007a" def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00007a" return responses_controller_version(key) # Needed only for the 200 return code def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00007a" return responses_image_stage(key) - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00007a" + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) @@ -322,7 +317,7 @@ def test_image_mgmt_stage_00008( commit() will set the payload key name for the serial number based on the controller version, per Expected Results below """ - instance = image_stage + key = "test_image_mgmt_stage_00008a" def mock_controller_version(*args) -> None: instance.controller_version = controller_version @@ -333,17 +328,16 @@ def mock_controller_version(*args) -> None: monkeypatch.setattr(controller_version_patch, mock_controller_version) def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00008a" return responses_image_stage(key) - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00008a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) + instance = image_stage instance.serial_numbers = ["FDO21120U5D"] instance.commit() print(f"instance.payload: {instance.payload.keys()}") @@ -369,12 +363,12 @@ def test_image_mgmt_stage_00009(monkeypatch, image_stage) -> None: When len(serial_numbers) == 0, commit() will set result and response properties, and return without doing anything else. """ + key = "test_image_mgmt_stage_00009a" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00009a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) response_msg = "No files to stage." @@ -406,22 +400,21 @@ def test_image_mgmt_stage_00010(monkeypatch, image_stage) -> None: Description commit() will call fail_json() on non-success response from the controller. """ + key = "test_image_mgmt_stage_00010a" def mock_controller_version(*args) -> None: instance.controller_version = "12.1.3b" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00010a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00010a" return responses_image_stage(key) monkeypatch.setattr( PATCH_IMAGE_STAGE_POPULATE_CONTROLLER_VERSION, mock_controller_version ) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": False}) @@ -452,14 +445,14 @@ def test_image_mgmt_stage_00020( In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ - instance = image_stage + key = "test_image_mgmt_stage_00020a" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00020a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + instance = image_stage instance.issu_detail = issu_details_by_serial_number instance.serial_numbers = [ "FDO21120U5D", @@ -495,14 +488,14 @@ def test_image_mgmt_stage_00021( In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ - instance = image_stage + key = "test_image_mgmt_stage_00021a" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00021a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + instance = image_stage instance.issu_detail = issu_details_by_serial_number instance.serial_numbers = [ "FDO21120U5D", @@ -540,14 +533,14 @@ def test_image_mgmt_stage_00022( Description See test_wait_for_image_stage_to_complete for functional details. """ - instance = image_stage + key = "test_image_mgmt_stage_00022a" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00022a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + instance = image_stage instance.issu_detail = issu_details_by_serial_number instance.serial_numbers = [ "FDO21120U5D", @@ -593,14 +586,14 @@ def test_image_mgmt_stage_00030( - upgrade - validated """ - instance = image_stage + key = "test_image_mgmt_stage_00030a" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00030a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + instance = image_stage instance.issu_detail = issu_details_by_serial_number instance.serial_numbers = [ "FDO21120U5D", @@ -633,19 +626,19 @@ def test_image_mgmt_stage_00031( Description See test_image_mgmt_stage_00030 for functional details. """ - instance = image_stage + key = "test_image_mgmt_stage_00031a" - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_stage_00031a" + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) match = "ImageStage._wait_for_current_actions_to_complete: " match += "Timed out waiting for actions to complete. " match += "serial_numbers_done: FDO21120U5D, " match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" + instance = image_stage instance.issu_detail = issu_details_by_serial_number instance.serial_numbers = [ "FDO21120U5D", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index 82d6f515f..4f5d84212 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -76,9 +76,11 @@ def test_image_mgmt_switch_issu_details_by_device_name_00002( assert isinstance(instance.properties, dict) assert isinstance(instance.properties.get("action_keys"), set) assert instance.properties.get("action_keys") == action_keys - assert instance.properties.get("response_data") is None - assert instance.properties.get("response") is None - assert instance.properties.get("result") is None + assert instance.properties.get("response_data") == [] + assert instance.properties.get("response") == [] + assert instance.properties.get("response_current") == {} + assert instance.properties.get("result") == [] + assert instance.properties.get("result_current") == {} assert instance.properties.get("device_name") is None @@ -93,7 +95,6 @@ def test_image_mgmt_switch_issu_details_by_device_name_00020( - instance.response is a dict - instance.response_data is a list """ - instance = issu_details_by_device_name key = "test_image_mgmt_switch_issu_details_by_device_name_00020a" @@ -102,9 +103,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - + instance = issu_details_by_device_name instance.refresh() - assert isinstance(instance.response, dict) + assert isinstance(instance.response_current, dict) assert isinstance(instance.response_data, list) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 31189dfaa..b73dafbc7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -78,9 +78,11 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00002( assert isinstance(instance.properties, dict) assert isinstance(instance.properties.get("action_keys"), set) assert instance.properties.get("action_keys") == action_keys - assert instance.properties.get("response_data") is None - assert instance.properties.get("response") is None - assert instance.properties.get("result") is None + assert instance.properties.get("response") == [] + assert instance.properties.get("response_current") == {} + assert instance.properties.get("response_data") == [] + assert instance.properties.get("result") == [] + assert instance.properties.get("result_current") == {} assert instance.properties.get("ip_address") is None diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 0400d851e..a4b4ce145 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -72,16 +72,17 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00002( - action_keys contains expected values """ instance = issu_details_by_serial_number - action_keys = {"imageStaged", "upgrade", "validated"} instance._init_properties() # pylint: disable=protected-access assert isinstance(instance.properties, dict) assert isinstance(instance.properties.get("action_keys"), set) assert instance.properties.get("action_keys") == action_keys - assert instance.properties.get("response_data") is None - assert instance.properties.get("response") is None - assert instance.properties.get("result") is None + assert instance.properties.get("response_data") == [] + assert instance.properties.get("response") == [] + assert instance.properties.get("response_current") == {} + assert instance.properties.get("result") == [] + assert instance.properties.get("result_current") == {} assert instance.properties.get("serial_number") is None From dc9d8459f1dbdbb7bbc49df69fdf804c3338d2de Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 4 Feb 2024 11:31:05 -1000 Subject: [PATCH 246/300] Fix ansible-sanity PEP8 issues --- plugins/module_utils/image_mgmt/image_stage.py | 1 - plugins/module_utils/image_mgmt/image_upgrade.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 154bac3cf..5371ebd86 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -185,7 +185,6 @@ def commit(self): msg += "before calling commit." self.module.fail_json(msg, **self.failed_result) - if len(self.serial_numbers) == 0: msg = "No files to stage." response_current = {"DATA": [{"key": "ALL", "value": msg}]} diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_mgmt/image_upgrade.py index 42fb83689..903d1962b 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_mgmt/image_upgrade.py @@ -36,6 +36,7 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ SwitchIssuDetailsByIpAddress + class ImageUpgrade(ImageUpgradeCommon): """ Endpoint: From 2e36073e9fd9ad8aedf57fe415cdab1491331c12 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 5 Feb 2024 12:54:30 -1000 Subject: [PATCH 247/300] Increase example logging_config.json maxBytes from 500K to 50M --- plugins/module_utils/common/logging_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/common/logging_config.json b/plugins/module_utils/common/logging_config.json index 64a660bf2..5bb98868b 100644 --- a/plugins/module_utils/common/logging_config.json +++ b/plugins/module_utils/common/logging_config.json @@ -14,7 +14,7 @@ "filename": "/tmp/dcnm.log", "mode": "a", "encoding": "utf-8", - "maxBytes": 500000, + "maxBytes": 50000000, "backupCount": 4 } }, From 319cf88fbf84f7ea1547e12b3da5f8efcbfd3f3f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 5 Feb 2024 13:00:51 -1000 Subject: [PATCH 248/300] Update comments to include correct testcase name --- .../integration/targets/dcnm_image_upgrade/tests/deleted.yaml | 3 ++- .../targets/dcnm_image_upgrade/tests/merged_global_config.yaml | 3 ++- .../tests/merged_override_global_config.yaml | 2 +- tests/integration/targets/dcnm_image_upgrade/tests/query.yaml | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml index fa4a2de1a..b785691a1 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml @@ -7,6 +7,7 @@ # 29:10.63 # 30:39.32 # 32:36.36 +# 28:58.81 ################################################################################ # STEPS @@ -40,7 +41,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: merged +# testcase: deleted # fabric_name: f1 # username: admin # password: "foobar" diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml index f913b9d6c..e42cce4f4 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml @@ -23,6 +23,7 @@ # 31:41.42 # 31:28.12 # 29:18.47 +# 28:57.34 ################################################################################ # STEPS @@ -55,7 +56,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: merged +# testcase: merged_global_config # fabric_name: f1 # username: admin # password: "foobar" diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml index c7e5c58f3..0312fa071 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml @@ -57,7 +57,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: merged +# testcase: merged_override_global_config # fabric_name: f1 # username: admin # password: "foobar" diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml index fdf42977e..1c047f311 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml @@ -42,7 +42,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: merged +# testcase: query # fabric_name: f1 # username: admin # password: "foobar" From da496e81c8b6e0d53601535361e6e6d0b35c9cca Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 6 Feb 2024 07:27:40 -1000 Subject: [PATCH 249/300] Fix reference to non-existing property --- plugins/modules/dcnm_image_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 449f07c0c..796c06266 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -525,7 +525,7 @@ def get_want(self) -> None: if len(self.want) == 0: self.task_result.result["changed"] = False - self.module.exit_json(**self.task_result.result) + self.module.exit_json(**self.task_result.module_result) def _build_idempotent_want(self, want) -> None: """ From d11dc36b9bb7b0840a31ead1da0158d9c21dde5b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 6 Feb 2024 07:48:35 -1000 Subject: [PATCH 250/300] Disable logging --- plugins/modules/dcnm_image_upgrade.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 796c06266..03793f82b 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1326,9 +1326,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - log.config = config_file + # collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + # config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + # log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) From 48520d5165bd41b783b984a1e1fadbf9641cbcf3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 6 Feb 2024 16:53:13 -1000 Subject: [PATCH 251/300] SwitchIssuDetails: rename refresh() to refresh_super(), more... SwitchIssuDetails: rename refresh() to refresh_supper() so that subclasses can call it from within their local refresh() methods (previously, they called super().refresh()). ImageValidate.commit(): don't upgrade property values directly, use inherited properties instead. Update unit tests to reflect the above changes. --- .../module_utils/image_mgmt/image_validate.py | 8 ++- .../image_mgmt/switch_issu_details.py | 55 +++++++------------ .../test_image_upgrade_image_validate.py | 4 +- ...rade_switch_issu_details_by_device_name.py | 11 ++-- ...grade_switch_issu_details_by_ip_address.py | 21 ++++--- ...de_switch_issu_details_by_serial_number.py | 24 +++++--- 6 files changed, 63 insertions(+), 60 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_mgmt/image_validate.py index 298747fb2..f5de6936b 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_mgmt/image_validate.py @@ -165,9 +165,11 @@ def commit(self) -> None: if len(self.serial_numbers) == 0: msg = "No serial numbers to validate." - self.properties["response"] = {"response": msg} - self.properties["response_data"] = {"response": msg} - self.properties["result"] = {"success": True} + self.response_current = {"response": msg} + self.result_current = {"success": True} + self.response_data = {"response": msg} + self.response = self.response_current + self.result = self.result_current return self.prune_serial_numbers() diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_mgmt/switch_issu_details.py index 95631ec27..49b3f83a5 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_mgmt/switch_issu_details.py @@ -19,6 +19,7 @@ __author__ = "Allen Robel" import inspect +import json import logging from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ @@ -107,7 +108,7 @@ def _init_properties(self): self.properties["action_keys"].add("upgrade") self.properties["action_keys"].add("validated") - def refresh(self) -> None: + def refresh_super(self) -> None: """ Refresh current issu details from the controller. """ @@ -116,16 +117,25 @@ def refresh(self) -> None: path = self.endpoints.issu_info.get("path") verb = self.endpoints.issu_info.get("verb") - self.properties["response"] = dcnm_send(self.module, verb, path) - self.properties["result"] = self._handle_response(self.response, verb) + msg = f"verb: {verb}, path {path}" + self.log.debug(msg) - if self.result["success"] is False or self.result["found"] is False: + self.response_current = dcnm_send(self.module, verb, path) + self.result_current = self._handle_response(self.response_current, verb) + + msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"self.result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if self.result_current["success"] is False or self.result_current["found"] is False: msg = f"{self.class_name}.{method_name}: " msg += "Bad result when retriving switch " msg += "information from the controller" self.module.fail_json(msg, **self.failed_result) - data = self.response.get("DATA").get("lastOperDataObject") + data = self.response_current.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.{method_name}: " @@ -137,10 +147,6 @@ def refresh(self) -> None: msg += "The controller has no switch ISSU information." self.module.fail_json(msg, **self.failed_result) - self.properties["response_data"] = self.response.get("DATA", {}).get( - "lastOperDataObject", [] - ) - @property def actions_in_progress(self): """ @@ -658,21 +664,15 @@ def __init__(self, module): self.log.debug("ENTERED SwitchIssuDetailsByIpAddress()") self.data_subclass = {} - self._init_properties() - - def _init_properties(self): - super()._init_properties() self.properties["filter"] = None def refresh(self): """ - Caller: __init__() - Refresh ip_address current issu details from the controller """ - super().refresh() + self.refresh_super() self.data_subclass = {} - for switch in self.response_data: + for switch in self.response_current["DATA"]["lastOperDataObject"]: self.data_subclass[switch["ipAddress"]] = switch def _get(self, item): @@ -748,23 +748,16 @@ def __init__(self, module): self.log.debug("ENTERED SwitchIssuDetailsBySerialNumber()") self.data_subclass = {} - self._init_properties() - - def _init_properties(self): - super()._init_properties() self.properties["filter"] = None def refresh(self): """ - Caller: __init__() - Refresh serial_number current issu details from NDFC """ - super().refresh() - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.refresh_super() self.data_subclass = {} - for switch in self.response_data: + for switch in self.response_current["DATA"]["lastOperDataObject"]: self.data_subclass[switch["serialNumber"]] = switch def _get(self, item): @@ -840,21 +833,15 @@ def __init__(self, module): self.log.debug("ENTERED SwitchIssuDetailsByDeviceName()") self.data_subclass = {} - self._init_properties() - - def _init_properties(self): - super()._init_properties() self.properties["filter"] = None def refresh(self): """ - Caller: __init__() - Refresh device_name current issu details from NDFC """ - super().refresh() + self.refresh_super() self.data_subclass = {} - for switch in self.response_data: + for switch in self.response_current["DATA"]["lastOperDataObject"]: self.data_subclass[switch["deviceName"]] = switch def _get(self, item): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 347c0e825..eeab421c6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -462,8 +462,8 @@ def test_image_mgmt_validate_00022(image_validate) -> None: instance = image_validate instance.serial_numbers = [] instance.commit() - assert instance.response == {"response": "No serial numbers to validate."} - assert instance.result == {"success": True} + assert instance.response == [{"response": "No serial numbers to validate."}] + assert instance.result == [{"success": True}] def test_image_mgmt_validate_00023(monkeypatch, image_validate) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index 4f5d84212..723dd2709 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -211,9 +211,10 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - assert isinstance(instance.result, dict) - assert instance.result.get("found") is True - assert instance.result.get("success") is True + assert isinstance(instance.result, list) + assert isinstance(instance.result_current, dict) + assert instance.result_current.get("found") is True + assert instance.result_current.get("success") is True def test_image_mgmt_switch_issu_details_by_device_name_00023( @@ -261,7 +262,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - match = "SwitchIssuDetailsByDeviceName.refresh: " + match = "SwitchIssuDetailsByDeviceName.refresh_super: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): instance.refresh() @@ -288,7 +289,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - match = "SwitchIssuDetailsByDeviceName.refresh: " + match = "SwitchIssuDetailsByDeviceName.refresh_super: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): instance.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index b73dafbc7..d51969f25 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -94,7 +94,10 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00020( - refresh Test - - instance.response is a dict + - instance.response is a list + - instance.response_current is a dict + - instance.result is a list + - instance.result_current is a dict - instance.response_data is a list """ instance = issu_details_by_ip_address @@ -108,7 +111,10 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - assert isinstance(instance.response, dict) + assert isinstance(instance.response, list) + assert isinstance(instance.response_current, dict) + assert isinstance(instance.result, list) + assert isinstance(instance.result_current, dict) assert isinstance(instance.response_data, list) @@ -214,9 +220,10 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - assert isinstance(instance.result, dict) - assert instance.result.get("found") is True - assert instance.result.get("success") is True + assert isinstance(instance.result, list) + assert isinstance(instance.result_current, dict) + assert instance.result_current.get("found") is True + assert instance.result_current.get("success") is True def test_image_mgmt_switch_issu_details_by_ip_address_00023( @@ -265,7 +272,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - match = "SwitchIssuDetailsByIpAddress.refresh: " + match = "SwitchIssuDetailsByIpAddress.refresh_super: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): instance.refresh() @@ -292,7 +299,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - match = "SwitchIssuDetailsByIpAddress.refresh: " + match = "SwitchIssuDetailsByIpAddress.refresh_super: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): instance.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index a4b4ce145..3df71f658 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -94,7 +94,10 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00020( - refresh Test - - instance.response is a dict + - instance.response is a list + - instance.response_current is a dict + - instance.result is a list + - instance.result_current is a dict - instance.response_data is a list """ instance = issu_details_by_serial_number @@ -108,7 +111,10 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - assert isinstance(instance.response, dict) + assert isinstance(instance.response, list) + assert isinstance(instance.response_current, dict) + assert isinstance(instance.result, list) + assert isinstance(instance.result_current, dict) assert isinstance(instance.response_data, list) @@ -199,8 +205,8 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00022( - refresh Test - - instance.result is a dict - - instance.result contains expected key/values for 200 RESULT_CODE + - instance.result_current is a dict + - instance.result_current contains expected key/values for 200 RESULT_CODE """ instance = issu_details_by_serial_number @@ -212,9 +218,9 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) instance.refresh() - assert isinstance(instance.result, dict) - assert instance.result.get("found") is True - assert instance.result.get("success") is True + assert isinstance(instance.result_current, dict) + assert instance.result_current.get("found") is True + assert instance.result_current.get("success") is True def test_image_mgmt_switch_issu_details_by_serial_number_00023( @@ -262,7 +268,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - match = "SwitchIssuDetailsBySerialNumber.refresh: " + match = "SwitchIssuDetailsBySerialNumber.refresh_super: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): instance.refresh() @@ -288,7 +294,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - match = "SwitchIssuDetailsBySerialNumber.refresh: " + match = "SwitchIssuDetailsBySerialNumber.refresh_super: " match += "The controller has no switch ISSU information." with pytest.raises(AnsibleFailJson, match=match): instance.refresh() From 7126064cd26ffea0ea984f936008e43b1ef42d22 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 8 Feb 2024 12:47:41 -1000 Subject: [PATCH 252/300] ImagePolicies: do not fail_json if length of DATA.lastOperDataObject == 0 While writing unit tests for dcnm_image_policy module, found that we cannot fail_json if no policies are present on the controller. Modified ImagePolicies() class accordingly, as well as unit test for this case. --- .../module_utils/image_mgmt/image_policies.py | 12 ++++++++--- .../test_image_upgrade_image_policies.py | 20 ++++++++----------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_mgmt/image_policies.py index abea2e4aa..d61b0b6d2 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_mgmt/image_policies.py @@ -100,10 +100,16 @@ def refresh(self): msg += "information from the controller." self.module.fail_json(msg, **self.failed_result) + # We cannot fail_json here since dcnm_image_policy merged + # state will fail if there are no policies defined. + # if len(data) == 0: + # msg = f"{self.class_name}.{self.method_name}: " + # msg += "the controller has no defined image policies." + # self.module.fail_json(msg, **self.failed_result) + if len(data) == 0: - msg = f"{self.class_name}.{self.method_name}: " - msg += "the controller has no defined image policies." - self.module.fail_json(msg, **self.failed_result) + msg = "the controller has no defined image policies." + self.log.debug(msg) self.properties["response_data"] = {} self.properties["all_policies"] = {} diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index 8e0e5874c..dc9a02976 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -201,11 +201,17 @@ def test_image_mgmt_image_policies_00023(monkeypatch, image_policies) -> None: - refresh Test - - fail_json is called when DATA.lastOperDataObject length == 0 + - do not fail_json is called when DATA.lastOperDataObject length == 0 - 200 response Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies + + Discussion + dcnm_image_policy classes ImagePolicyCreate and ImagePolicyCreateBulk + both call ImagePolicies.refresh() when checking if the image policies + they are creating already exist on the controller. Hence, we cannot + fail_json when the length of DATA.lastOperDataObject is zero. """ key = "test_image_mgmt_image_policies_00023a" @@ -215,11 +221,8 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) - match = "ImagePolicies.refresh: " - match += "the controller has no defined image policies." - instance = image_policies - with pytest.raises(AnsibleFailJson, match=match): + with does_not_raise(): instance.refresh() @@ -250,13 +253,6 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: image_policies.policy_name = "FOO" assert image_policies.policy is None - # match = "ImagePolicies._get: " - # match += "policy_name FOO is not defined on the controller." - - # instance = image_policies - # with pytest.raises(AnsibleFailJson, match=match): - # if instance.policy_type == "PLATFORM": - # pass def test_image_mgmt_image_policies_00025(monkeypatch, image_policies) -> None: From e5b95bdbfb841fe5d0f83789cbd59fa198331831 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 18 Feb 2024 12:42:59 -1000 Subject: [PATCH 253/300] Don't call refresh() more than needed. - ImageStage.prune_serial_numbers: Call refresh() outside of for loop. - ImageStage.validate_serial_numbers: Call refresh() outside of for loop. The above could be made even more efficient by calling refresh() in commit(). This would require rethinking a couple unit tests, so will leave this for a future commit. Also: - test_image_upgrade_image_stage: Add a Summary section which provides the "gist" of a test case. --- .../module_utils/image_mgmt/image_stage.py | 6 +- .../test_image_upgrade_image_stage.py | 59 ++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_mgmt/image_stage.py index 5371ebd86..bd3d1eb6e 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_mgmt/image_stage.py @@ -48,7 +48,6 @@ class ImageStage(ImageUpgradeCommon): stage = ImageStage(module) stage.serial_numbers = ["FDO211218HH", "FDO211218GC"] stage.commit() - data = stage.data Request body (12.1.2e) (yes, serialNum is misspelled): { @@ -140,9 +139,9 @@ def prune_serial_numbers(self): serial number from the list of serial numbers to stage. """ serial_numbers = copy.copy(self.serial_numbers) + self.issu_detail.refresh() for serial_number in serial_numbers: self.issu_detail.filter = serial_number - self.issu_detail.refresh() if self.issu_detail.image_staged == "Success": self.serial_numbers.remove(serial_number) @@ -152,9 +151,9 @@ def validate_serial_numbers(self): is Failed. """ method_name = inspect.stack()[0][3] + self.issu_detail.refresh() for serial_number in self.serial_numbers: self.issu_detail.filter = serial_number - self.issu_detail.refresh() if self.issu_detail.image_staged == "Failed": msg = f"{self.class_name}.{method_name}: " @@ -195,6 +194,7 @@ def commit(self): self.result_current = {"changed": False, "success": True} return + # self.issu_detail.refresh() self.prune_serial_numbers() self.validate_serial_numbers() self._wait_for_current_actions_to_complete() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index ab24a803a..311af17bc 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -141,6 +141,10 @@ def test_image_mgmt_stage_00004( Function - prune_serial_numbers + Summary + Verify that prune_serial_numbers prunes serial numbers that have already + been staged. + Test - module.serial_numbers contains only serial numbers for which imageStaged == "none" (FDO2112189M, FDO211218AX, FDO211218B5) @@ -149,13 +153,14 @@ def test_image_mgmt_stage_00004( Description prune_serial_numbers removes serial numbers from the list for which - imageStaged == "Success" (TODO: AND policy == ) + imageStaged == "Success" """ key = "test_image_mgmt_stage_00004a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) instance = image_stage @@ -184,6 +189,9 @@ def test_image_mgmt_stage_00005( Function - validate_serial_numbers + Summary + Verify that validate_serial_numbers raises fail_json appropriately. + Test - fail_json is not called when imageStaged == "Success" - fail_json is called when imageStaged == "Failed" @@ -230,6 +238,10 @@ def test_image_mgmt_stage_00006( Function commit + Summary + Verify that commit raises fail_json appropriately based on value of + instance.serial_numbers. + Test - fail_json is called when serial_numbers is None - fail_json is not called when serial_numbers is set @@ -262,6 +274,9 @@ def test_image_mgmt_stage_00007(monkeypatch, image_stage) -> None: Function - commit + Summary + Verify that verb is set to POST and path is set to the expected value. + Test - ImageStage.verb is set to POST - ImageStage.path is set to: @@ -309,6 +324,10 @@ def test_image_mgmt_stage_00008( Function - commit + Summary + Verify that the serial number key name in the payload is set correctly + based on the controller version. + Test - controller_version 12.1.2e -> key name "sereialNum" (yes, misspelled) - controller_version 12.1.3b -> key name "serialNumbers @@ -349,6 +368,10 @@ def test_image_mgmt_stage_00009(monkeypatch, image_stage) -> None: Function - commit + Summary + Verify that commit() sets result, response, and response_data + appropriately when serial_numbers is empty. + Setup - SwitchIssuDetailsBySerialNumber is mocked to return a successful response - self.serial_numbers is set to [] (empty list) @@ -390,6 +413,9 @@ def test_image_mgmt_stage_00010(monkeypatch, image_stage) -> None: Function - commit + Summary + Verify that commit() calls fail_json() on 500 response from the controller. + Setup - IssuDetailsBySerialNumber is mocked to return a successful response - ImageStage is mocked to return a non-successful (500) response @@ -432,6 +458,10 @@ def test_image_mgmt_stage_00020( Function - _wait_for_image_stage_to_complete + Summary + Verify proper behavior of _wait_for_image_stage_to_complete when + imageStaged is "Success" for all serial numbers. + Test - imageStaged == "Success" for all serial numbers so fail_json is not called @@ -473,6 +503,11 @@ def test_image_mgmt_stage_00021( Function - _wait_for_image_stage_to_complete + Summary + Verify proper behavior of _wait_for_image_stage_to_complete when + imageStaged is "Failed" for one serial number and imageStaged + is "Success" for one serial number. + Test - module.serial_numbers_done is a set() - module.serial_numbers_done has length 1 @@ -520,6 +555,11 @@ def test_image_mgmt_stage_00022( Function - _wait_for_image_stage_to_complete + Summary + Verify proper behavior of _wait_for_image_stage_to_complete when + timeout is reached for one serial number (i.e. imageStaged is + "In-Progress") and imageStaged is "Success" for one serial number. + Test - module.serial_numbers_done is a set() - module.serial_numbers_done has length 1 @@ -569,6 +609,10 @@ def test_image_mgmt_stage_00030( Function - _wait_for_current_actions_to_complete + Summary + Verify proper behavior of _wait_for_current_actions_to_complete when + there are no actions pending. + Test - instance.serial_numbers_done is a set() - instance.serial_numbers_done has length 2 @@ -614,6 +658,10 @@ def test_image_mgmt_stage_00031( Function - _wait_for_current_actions_to_complete + Summary + Verify proper behavior of _wait_for_current_actions_to_complete when + there is a timeout waiting for one serial number to complete staging. + Test - module.serial_numbers_done is a set() - module.serial_numbers_done has length 1 @@ -671,6 +719,9 @@ def test_image_mgmt_stage_00040(image_stage, input, output, context) -> None: Function - check_interval + Summary + Verify that check_interval input validation works as expected. + Test - Verify inputs to check_interval property @@ -701,6 +752,9 @@ def test_image_mgmt_stage_00050(image_stage, input, output, context) -> None: Function - check_interval + Summary + Verify that check_timeout input validation works as expected. + Test - Verify inputs to check_timeout property @@ -733,6 +787,9 @@ def test_image_mgmt_stage_00060(image_stage, input, output, context) -> None: Function - serial_numbers + Summary + Verify that serial_numbers input validation works as expected. + Test - Verify inputs to serial_numbers property - Verify that fail_json is called if the input is not a list From 53a5fe0128ff5b49edd4c841a2603e84290ddf9a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 18 Feb 2024 12:47:16 -1000 Subject: [PATCH 254/300] Fix pep8 too many blank lines --- .../dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 311af17bc..a0193d238 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -160,7 +160,6 @@ def test_image_mgmt_stage_00004( def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) instance = image_stage From 3bd708a1645c3c2c977f56d51c32427375771f08 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 18 Feb 2024 13:48:32 -1000 Subject: [PATCH 255/300] rename image_mgmt directory to image_upgrade Background: Initially, image_mgmt was going to hold the module_utils for image_policy, image_upgrade, and image_upload. I never coordinated with Mallik on this, and later decided it was a bit of organizational overkill. This commit comprises the following: 1. Renaming directory module_utils/image_mgmt to module_utils/image_upgrade. 2. Modifying all python imports that point to module_utils/image_mgmt, to point to module_utils/image_upgrade. 3. Modifying all unit test names to replace image_mgmt with image_upgrade 4. Changing key names for all the unit tests to replace image_mgmt with image_upgrade. --- .../module_utils/common/controller_version.py | 4 +- .../{image_mgmt => image_upgrade}/__init__.py | 0 .../api_endpoints.py | 0 .../image_policies.py | 4 +- .../image_policy_action.py | 8 +- .../image_stage.py | 8 +- .../image_upgrade.py | 10 +- .../image_upgrade_common.py | 2 +- .../image_upgrade_task_result.py | 0 .../image_validate.py | 8 +- .../install_options.py | 4 +- .../rest_send.py | 2 +- .../switch_details.py | 6 +- .../switch_issu_details.py | 4 +- plugins/modules/dcnm_image_upgrade.py | 36 ++--- .../image_upgrade_payloads_ImageUpgrade.json | 6 +- .../image_upgrade_playbook_configs.json | 34 ++-- ...e_upgrade_responses_ControllerVersion.json | 8 +- ...upgrade_responses_ImageInstallOptions.json | 42 ++--- ...image_upgrade_responses_ImagePolicies.json | 20 +-- ...e_upgrade_responses_ImagePolicyAction.json | 2 +- .../image_upgrade_responses_ImageStage.json | 8 +- .../image_upgrade_responses_ImageUpgrade.json | 36 ++--- ..._upgrade_responses_ImageUpgradeCommon.json | 42 ++--- ...image_upgrade_responses_ImageValidate.json | 6 +- ...image_upgrade_responses_SwitchDetails.json | 18 +-- ...e_upgrade_responses_SwitchIssuDetails.json | 152 +++++++++--------- .../dcnm_image_upgrade/image_upgrade_utils.py | 18 +-- .../test_image_upgrade_api_endpoints.py | 34 ++-- ...est_image_upgrade_image_install_options.py | 50 +++--- .../test_image_upgrade_image_policies.py | 40 ++--- .../test_image_upgrade_image_policy_action.py | 50 +++--- .../test_image_upgrade_image_stage.py | 82 +++++----- .../test_image_upgrade_image_upgrade.py | 152 +++++++++--------- ...test_image_upgrade_image_upgrade_common.py | 68 ++++---- .../test_image_upgrade_image_upgrade_task.py | 76 ++++----- .../test_image_upgrade_image_validate.py | 68 ++++---- .../test_image_upgrade_switch_details.py | 48 +++--- ...rade_switch_issu_details_by_device_name.py | 40 ++--- ...grade_switch_issu_details_by_ip_address.py | 40 ++--- ...de_switch_issu_details_by_serial_number.py | 40 ++--- 41 files changed, 638 insertions(+), 638 deletions(-) rename plugins/module_utils/{image_mgmt => image_upgrade}/__init__.py (100%) rename plugins/module_utils/{image_mgmt => image_upgrade}/api_endpoints.py (100%) rename plugins/module_utils/{image_mgmt => image_upgrade}/image_policies.py (98%) rename plugins/module_utils/{image_mgmt => image_upgrade}/image_policy_action.py (97%) rename plugins/module_utils/{image_mgmt => image_upgrade}/image_stage.py (97%) rename plugins/module_utils/{image_mgmt => image_upgrade}/image_upgrade.py (98%) rename plugins/module_utils/{image_mgmt => image_upgrade}/image_upgrade_common.py (99%) rename plugins/module_utils/{image_mgmt => image_upgrade}/image_upgrade_task_result.py (100%) rename plugins/module_utils/{image_mgmt => image_upgrade}/image_validate.py (97%) rename plugins/module_utils/{image_mgmt => image_upgrade}/install_options.py (98%) rename plugins/module_utils/{image_mgmt => image_upgrade}/rest_send.py (99%) rename plugins/module_utils/{image_mgmt => image_upgrade}/switch_details.py (95%) rename plugins/module_utils/{image_mgmt => image_upgrade}/switch_issu_details.py (99%) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 55201e80a..91f6d8165 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -24,9 +24,9 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ dcnm_send diff --git a/plugins/module_utils/image_mgmt/__init__.py b/plugins/module_utils/image_upgrade/__init__.py similarity index 100% rename from plugins/module_utils/image_mgmt/__init__.py rename to plugins/module_utils/image_upgrade/__init__.py diff --git a/plugins/module_utils/image_mgmt/api_endpoints.py b/plugins/module_utils/image_upgrade/api_endpoints.py similarity index 100% rename from plugins/module_utils/image_mgmt/api_endpoints.py rename to plugins/module_utils/image_upgrade/api_endpoints.py diff --git a/plugins/module_utils/image_mgmt/image_policies.py b/plugins/module_utils/image_upgrade/image_policies.py similarity index 98% rename from plugins/module_utils/image_mgmt/image_policies.py rename to plugins/module_utils/image_upgrade/image_policies.py index d61b0b6d2..6256e4dd8 100644 --- a/plugins/module_utils/image_mgmt/image_policies.py +++ b/plugins/module_utils/image_upgrade/image_policies.py @@ -23,9 +23,9 @@ import logging from typing import Any, AnyStr, Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ dcnm_send diff --git a/plugins/module_utils/image_mgmt/image_policy_action.py b/plugins/module_utils/image_upgrade/image_policy_action.py similarity index 97% rename from plugins/module_utils/image_mgmt/image_policy_action.py rename to plugins/module_utils/image_upgrade/image_policy_action.py index 3702f649e..7fdd92526 100644 --- a/plugins/module_utils/image_mgmt/image_policy_action.py +++ b/plugins/module_utils/image_upgrade/image_policy_action.py @@ -24,13 +24,13 @@ import logging from typing import Any, Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_policies import \ ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber diff --git a/plugins/module_utils/image_mgmt/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py similarity index 97% rename from plugins/module_utils/image_mgmt/image_stage.py rename to plugins/module_utils/image_upgrade/image_stage.py index bd3d1eb6e..488881563 100644 --- a/plugins/module_utils/image_mgmt/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -26,13 +26,13 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ ControllerVersion -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.rest_send import \ RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber diff --git a/plugins/module_utils/image_mgmt/image_upgrade.py b/plugins/module_utils/image_upgrade/image_upgrade.py similarity index 98% rename from plugins/module_utils/image_mgmt/image_upgrade.py rename to plugins/module_utils/image_upgrade/image_upgrade.py index 903d1962b..3c6bfac90 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade.py +++ b/plugins/module_utils/image_upgrade/image_upgrade.py @@ -25,15 +25,15 @@ from time import sleep from typing import Any, Dict, List, Set -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.install_options import \ ImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.rest_send import \ RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsByIpAddress diff --git a/plugins/module_utils/image_mgmt/image_upgrade_common.py b/plugins/module_utils/image_upgrade/image_upgrade_common.py similarity index 99% rename from plugins/module_utils/image_mgmt/image_upgrade_common.py rename to plugins/module_utils/image_upgrade/image_upgrade_common.py index c90b15a13..ea10907f8 100644 --- a/plugins/module_utils/image_mgmt/image_upgrade_common.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_common.py @@ -25,7 +25,7 @@ from time import sleep # Using only for its failed_result property -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_task_result import \ ImageUpgradeTaskResult from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ dcnm_send diff --git a/plugins/module_utils/image_mgmt/image_upgrade_task_result.py b/plugins/module_utils/image_upgrade/image_upgrade_task_result.py similarity index 100% rename from plugins/module_utils/image_mgmt/image_upgrade_task_result.py rename to plugins/module_utils/image_upgrade/image_upgrade_task_result.py diff --git a/plugins/module_utils/image_mgmt/image_validate.py b/plugins/module_utils/image_upgrade/image_validate.py similarity index 97% rename from plugins/module_utils/image_mgmt/image_validate.py rename to plugins/module_utils/image_upgrade/image_validate.py index f5de6936b..8a065dc03 100644 --- a/plugins/module_utils/image_mgmt/image_validate.py +++ b/plugins/module_utils/image_upgrade/image_validate.py @@ -25,13 +25,13 @@ from time import sleep from typing import List, Set -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.rest_send import \ RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber diff --git a/plugins/module_utils/image_mgmt/install_options.py b/plugins/module_utils/image_upgrade/install_options.py similarity index 98% rename from plugins/module_utils/image_mgmt/install_options.py rename to plugins/module_utils/image_upgrade/install_options.py index cd874caeb..505832c1e 100644 --- a/plugins/module_utils/image_mgmt/install_options.py +++ b/plugins/module_utils/image_upgrade/install_options.py @@ -25,9 +25,9 @@ import time from typing import Any, Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ dcnm_send diff --git a/plugins/module_utils/image_mgmt/rest_send.py b/plugins/module_utils/image_upgrade/rest_send.py similarity index 99% rename from plugins/module_utils/image_mgmt/rest_send.py rename to plugins/module_utils/image_upgrade/rest_send.py index 88f37df21..f01c6d424 100644 --- a/plugins/module_utils/image_mgmt/rest_send.py +++ b/plugins/module_utils/image_upgrade/rest_send.py @@ -25,7 +25,7 @@ from time import sleep # Using only for its failed_result property -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_task_result import \ ImageUpgradeTaskResult from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ dcnm_send diff --git a/plugins/module_utils/image_mgmt/switch_details.py b/plugins/module_utils/image_upgrade/switch_details.py similarity index 95% rename from plugins/module_utils/image_mgmt/switch_details.py rename to plugins/module_utils/image_upgrade/switch_details.py index 5f904b509..b03bd13f3 100644 --- a/plugins/module_utils/image_mgmt/switch_details.py +++ b/plugins/module_utils/image_upgrade/switch_details.py @@ -22,11 +22,11 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.rest_send import \ RestSend diff --git a/plugins/module_utils/image_mgmt/switch_issu_details.py b/plugins/module_utils/image_upgrade/switch_issu_details.py similarity index 99% rename from plugins/module_utils/image_mgmt/switch_issu_details.py rename to plugins/module_utils/image_upgrade/switch_issu_details.py index 49b3f83a5..9e21f92ed 100644 --- a/plugins/module_utils/image_mgmt/switch_issu_details.py +++ b/plugins/module_utils/image_upgrade/switch_issu_details.py @@ -22,9 +22,9 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ dcnm_send diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 03793f82b..b5732b8c8 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -299,19 +299,19 @@ EXAMPLES = """ # This module supports the following states: -# + # merged: # Attach image policy to one or more devices. # Stage image on one or more devices. # Validate image on one or more devices. # Upgrade image on one or more devices. -# + # query: # Return ISSU details for one or more devices. -# + # deleted: # Delete image policy from one or more devices -# + # Attach image policy NR3F to two devices # Stage and validate the image on two devices but do not upgrade @@ -414,27 +414,27 @@ ParamsMergeDefaults from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_policies import \ ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_policy_action import \ ImagePolicyAction -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_stage import \ ImageStage -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade import \ ImageUpgrade -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_task_result import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_task_result import \ ImageUpgradeTaskResult -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_validate import \ ImageValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.install_options import \ ImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_details import \ SwitchDetails -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsByIpAddress @@ -1326,9 +1326,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - # collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - # config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - # log.config = config_file + collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json index 3bbd1d77b..ffbc00588 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_payloads_ImageUpgrade.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_upgrade_00019a": { + "test_image_upgrade_upgrade_00019a": { "devices": [ { "policyName": "KR5M", @@ -28,7 +28,7 @@ "writeErase": false } }, - "test_image_mgmt_upgrade_00020a": { + "test_image_upgrade_upgrade_00020a": { "devices": [ { "policyName": "NR3F", @@ -57,7 +57,7 @@ "writeErase": false } }, - "test_image_mgmt_upgrade_00021a": { + "test_image_upgrade_upgrade_00021a": { "devices": [ { "policyName": "NR3F", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json index 854b6b58f..4e45c4534 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_playbook_configs.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_upgrade_task_00003a": { + "test_image_upgrade_upgrade_task_00003a": { "TEST_NOTES": [ "fail_json is called because config.switches is not a list" ], @@ -7,7 +7,7 @@ "switches": "FOO" } }, - "test_image_mgmt_upgrade_task_00004a": { + "test_image_upgrade_upgrade_task_00004a": { "TEST_NOTES": [ "fail_json is called because config.switches is empty" ], @@ -15,7 +15,7 @@ "switches": [] } }, - "test_image_mgmt_upgrade_task_00005a": { + "test_image_upgrade_upgrade_task_00005a": { "TEST_NOTES": [ "fail_json is called because mandatory keys are missing" ], @@ -27,7 +27,7 @@ ] } }, - "test_image_mgmt_upgrade_task_00030a": { + "test_image_upgrade_upgrade_task_00030a": { "TEST_NOTES": [ "switch 1.1.1.1 uses default options", "switch 2.2.2.2 overrides default options" @@ -95,7 +95,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00031a": { + "test_image_upgrade_upgrade_task_00031a": { "TEST_NOTES": [ "switch 1.1.1.1 overrides global_config.options.nxos.bios_force with default value (False)", "switch 1.1.1.1 overrides global_config.options.nxos.mode with a non-default value (non_disruptive)", @@ -158,7 +158,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00040a": { + "test_image_upgrade_upgrade_task_00040a": { "config": { "switches": [ { @@ -169,7 +169,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00041a": { + "test_image_upgrade_upgrade_task_00041a": { "config": { "switches": [ { @@ -182,7 +182,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00042a": { + "test_image_upgrade_upgrade_task_00042a": { "config": { "switches": [ { @@ -195,7 +195,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00043a": { + "test_image_upgrade_upgrade_task_00043a": { "config": { "switches": [ { @@ -208,7 +208,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00044a": { + "test_image_upgrade_upgrade_task_00044a": { "config": { "switches": [ { @@ -221,7 +221,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00045a": { + "test_image_upgrade_upgrade_task_00045a": { "config": { "switches": [ { @@ -234,7 +234,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00046a": { + "test_image_upgrade_upgrade_task_00046a": { "config": { "switches": [ { @@ -247,7 +247,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00047a": { + "test_image_upgrade_upgrade_task_00047a": { "config": { "switches": [ { @@ -260,7 +260,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00048a": { + "test_image_upgrade_upgrade_task_00048a": { "config": { "switches": [ { @@ -273,7 +273,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00049a": { + "test_image_upgrade_upgrade_task_00049a": { "config": { "switches": [ { @@ -286,7 +286,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00050a": { + "test_image_upgrade_upgrade_task_00050a": { "config": { "switches": [ { @@ -299,7 +299,7 @@ }, "state": "merged" }, - "test_image_mgmt_upgrade_task_00051a": { + "test_image_upgrade_upgrade_task_00051a": { "config": { "switches": [ { diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json index a4905185c..2ef2018a2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_stage_00003a": { + "test_image_upgrade_stage_00003a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -15,7 +15,7 @@ "is_upgrade_inprogress": "false" } }, - "test_image_mgmt_stage_00003b": { + "test_image_upgrade_stage_00003b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", @@ -31,7 +31,7 @@ "is_upgrade_inprogress": "false" } }, - "test_image_mgmt_stage_00006a": { + "test_image_upgrade_stage_00006a": { "TEST_NOTES": [ "Needed only for the 200 return code" ], @@ -50,7 +50,7 @@ "is_upgrade_inprogress": "false" } }, - "test_image_mgmt_stage_00007a": { + "test_image_upgrade_stage_00007a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json index 2b520541d..f915b8a99 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageInstallOptions.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_install_options_00005a": { + "test_image_upgrade_install_options_00005a": { "TEST_NOTES": [ "All attributes in compatibilityStatusList are tested", "epldModules is tested", @@ -32,7 +32,7 @@ "errMessage": "" } }, - "test_image_mgmt_install_options_00006a": { + "test_image_upgrade_install_options_00006a": { "TEST_NOTES": [ "RETURN_CODE 500 is tested" ], @@ -44,7 +44,7 @@ "error": "null" } }, - "test_image_mgmt_install_options_00007a": { + "test_image_upgrade_install_options_00007a": { "TEST_NOTES": [ "POST REQUEST contents: issu == true, epld == false, packageInstall false", "Device has no policy attached", @@ -81,7 +81,7 @@ "errMessage": "" } }, - "test_image_mgmt_install_options_00008a": { + "test_image_upgrade_install_options_00008a": { "TEST_NOTES": [ "POST REQUEST contents: issu == true, epld == true, packageInstall false", "Device has no policy attached", @@ -146,7 +146,7 @@ "errMessage": "" } }, - "test_image_mgmt_install_options_00009a": { + "test_image_upgrade_install_options_00009a": { "TEST_NOTES": [ "POST REQUEST contents: issu == false, epld == true, packageInstall false", "Device has no policy attached", @@ -195,7 +195,7 @@ "errMessage": "" } }, - "test_image_mgmt_install_options_00010a": { + "test_image_upgrade_install_options_00010a": { "TEST_NOTES": [ "POST REQUEST contents: issu == true, epld == true, packageInstall true", "RETURN_CODE is 500 due to packageInstall is true, but policy contains no packages", @@ -210,7 +210,7 @@ "error": "Selected policy KR5M does not have package to continue." } }, - "test_image_mgmt_upgrade_00019a": { + "test_image_upgrade_upgrade_00019a": { "DATA": { "compatibilityStatusList": [ { @@ -265,7 +265,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00020a": { + "test_image_upgrade_upgrade_00020a": { "DATA": { "compatibilityStatusList": [ { @@ -320,7 +320,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00021a": { + "test_image_upgrade_upgrade_00021a": { "DATA": { "compatibilityStatusList": [ { @@ -375,7 +375,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00022a": { + "test_image_upgrade_upgrade_00022a": { "DATA": { "compatibilityStatusList": [ { @@ -430,7 +430,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00023a": { + "test_image_upgrade_upgrade_00023a": { "DATA": { "compatibilityStatusList": [ { @@ -485,7 +485,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00024a": { + "test_image_upgrade_upgrade_00024a": { "DATA": { "compatibilityStatusList": [ { @@ -540,7 +540,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00025a": { + "test_image_upgrade_upgrade_00025a": { "DATA": { "compatibilityStatusList": [ { @@ -595,7 +595,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00026a": { + "test_image_upgrade_upgrade_00026a": { "DATA": { "compatibilityStatusList": [ { @@ -650,7 +650,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00027a": { + "test_image_upgrade_upgrade_00027a": { "DATA": { "compatibilityStatusList": [ { @@ -705,7 +705,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00028a": { + "test_image_upgrade_upgrade_00028a": { "DATA": { "compatibilityStatusList": [ { @@ -760,7 +760,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00029a": { + "test_image_upgrade_upgrade_00029a": { "DATA": { "compatibilityStatusList": [ { @@ -815,7 +815,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00030a": { + "test_image_upgrade_upgrade_00030a": { "DATA": { "compatibilityStatusList": [ { @@ -870,7 +870,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00031a": { + "test_image_upgrade_upgrade_00031a": { "DATA": { "compatibilityStatusList": [ { @@ -925,7 +925,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00032a": { + "test_image_upgrade_upgrade_00032a": { "DATA": { "compatibilityStatusList": [ { @@ -980,7 +980,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/install-options", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00033a": { + "test_image_upgrade_upgrade_00033a": { "DATA": { "compatibilityStatusList": [ { diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json index 16624a302..ab7391117 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_image_policies_00010a": { + "test_image_upgrade_image_policies_00010a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", @@ -39,7 +39,7 @@ "message": "" } }, - "test_image_mgmt_image_policies_00020a": { + "test_image_upgrade_image_policies_00020a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", @@ -79,7 +79,7 @@ "message": "" } }, - "test_image_mgmt_image_policies_00021a": { + "test_image_upgrade_image_policies_00021a": { "TEST_NOTES": [ "404 RETURN_CODE" ], @@ -94,7 +94,7 @@ "path": "/rest/policymgnt/policiess" } }, - "test_image_mgmt_image_policies_00022a": { + "test_image_upgrade_image_policies_00022a": { "TEST_NOTES": [ "DATA field is empty" ], @@ -104,7 +104,7 @@ "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_policies_00023a": { + "test_image_upgrade_image_policies_00023a": { "TEST_NOTES": [ "DATA has no defined image policies" ], @@ -118,7 +118,7 @@ "message": "" } }, - "test_image_mgmt_image_policies_00024a": { + "test_image_upgrade_image_policies_00024a": { "TEST_NOTES": [ "RETURN_CODE 200", "policyName FOO is missing in response" @@ -148,7 +148,7 @@ "message": "" } }, - "test_image_mgmt_image_policies_00025a": { + "test_image_upgrade_image_policies_00025a": { "TEST_NOTES": [ "RETURN_CODE 200", "policyName key is missing" @@ -177,7 +177,7 @@ "message": "" } }, - "test_image_mgmt_image_policy_action_00013a": { + "test_image_upgrade_image_policy_action_00013a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", @@ -203,7 +203,7 @@ "message": "" } }, - "test_image_mgmt_image_policy_action_00020a": { + "test_image_upgrade_image_policy_action_00020a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", @@ -229,7 +229,7 @@ "message": "" } }, - "test_image_mgmt_image_policy_action_00021a": { + "test_image_upgrade_image_policy_action_00021a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json index bf47cc21a..59c2203a3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_image_policy_action_00021a": { + "test_image_upgrade_image_policy_action_00021a": { "RETURN_CODE": 200, "METHOD": "DELETE", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy?serialNumber=FDO21120U5D,FDO211218GC,FOX2109PGD0", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json index 9e37988f0..0ce8b8bed 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_stage_00006a": { + "test_image_upgrade_stage_00006a": { "TEST_NOTES": [ "Needed only for the 200 return code" ], @@ -14,7 +14,7 @@ "message": "" } }, - "test_image_mgmt_stage_00007a": { + "test_image_upgrade_stage_00007a": { "TEST_NOTES": [ "RETURN_CODE == 200", "MESSAGE == OK" @@ -30,7 +30,7 @@ "message": "" } }, - "test_image_mgmt_stage_00008a": { + "test_image_upgrade_stage_00008a": { "TEST_NOTES": [ "RETURN_CODE == 200", "MESSAGE == OK" @@ -46,7 +46,7 @@ "message": "" } }, - "test_image_mgmt_stage_00010a": { + "test_image_upgrade_stage_00010a": { "TEST_NOTES": [ "RETURN_CODE == 500", "MESSAGE == NOK" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json index 711894769..021f7959e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgrade.json @@ -1,124 +1,124 @@ { - "test_image_mgmt_upgrade_00019a": { + "test_image_upgrade_upgrade_00019a": { "DATA": 121, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00020a": { + "test_image_upgrade_upgrade_00020a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00022a": { + "test_image_upgrade_upgrade_00022a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00023a": { + "test_image_upgrade_upgrade_00023a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00024a": { + "test_image_upgrade_upgrade_00024a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00025a": { + "test_image_upgrade_upgrade_00025a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00026a": { + "test_image_upgrade_upgrade_00026a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00027a": { + "test_image_upgrade_upgrade_00027a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00028a": { + "test_image_upgrade_upgrade_00028a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00029a": { + "test_image_upgrade_upgrade_00029a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00030a": { + "test_image_upgrade_upgrade_00030a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00031a": { + "test_image_upgrade_upgrade_00031a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00032a": { + "test_image_upgrade_upgrade_00032a": { "DATA": 123, "MESSAGE": "Internal Server Error", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 500 }, - "test_image_mgmt_upgrade_00033a": { + "test_image_upgrade_upgrade_00033a": { "DATA": 123, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00045a": { + "test_image_upgrade_upgrade_00045a": { "DATA": 121, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00046a": { + "test_image_upgrade_upgrade_00046a": { "DATA": 121, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00047a": { + "test_image_upgrade_upgrade_00047a": { "DATA": 121, "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_0000XX": { + "test_image_upgrade_upgrade_0000XX": { "DATA": { "error": "Selected upgrade option is 'isGolden'. It does not allow when upgrade options selected more than one option. " }, diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json index 55570b080..210d8286d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageUpgradeCommon.json @@ -1,131 +1,131 @@ { - "test_image_mgmt_image_upgrade_common_00020a": { + "test_image_upgrade_image_upgrade_common_00020a": { "RETURN_CODE": 200, "METHOD": "DELETE", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00020b": { + "test_image_upgrade_image_upgrade_common_00020b": { "RETURN_CODE": 400, "METHOD": "DELETE", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "error: image not found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00020c": { + "test_image_upgrade_image_upgrade_common_00020c": { "RETURN_CODE": 200, "METHOD": "DELETE", "REQUEST_PATH": "https://blah_noop", "ERROR": "image not found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00030a": { + "test_image_upgrade_image_upgrade_common_00030a": { "RETURN_CODE": 200, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00030b": { + "test_image_upgrade_image_upgrade_common_00030b": { "RETURN_CODE": 400, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "error: image not found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00030c": { + "test_image_upgrade_image_upgrade_common_00030c": { "RETURN_CODE": 200, "METHOD": "POST", "REQUEST_PATH": "https://blah_noop", "ERROR": "image not found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00040a": { + "test_image_upgrade_image_upgrade_common_00040a": { "RETURN_CODE": 200, "METHOD": "PUT", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00040b": { + "test_image_upgrade_image_upgrade_common_00040b": { "RETURN_CODE": 400, "METHOD": "PUT", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "error: image not found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00040c": { + "test_image_upgrade_image_upgrade_common_00040c": { "RETURN_CODE": 200, "METHOD": "PUT", "REQUEST_PATH": "https://blah_noop", "ERROR": "image not found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00050a": { + "test_image_upgrade_image_upgrade_common_00050a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00050b": { + "test_image_upgrade_image_upgrade_common_00050b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "not OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00050c": { + "test_image_upgrade_image_upgrade_common_00050c": { "RETURN_CODE": 404, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "Not Found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00050d": { + "test_image_upgrade_image_upgrade_common_00050d": { "RETURN_CODE": 500, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00060a": { + "test_image_upgrade_image_upgrade_common_00060a": { "RETURN_CODE": 200, "METHOD": "FOO", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "not OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00070a": { + "test_image_upgrade_image_upgrade_common_00070a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00070b": { + "test_image_upgrade_image_upgrade_common_00070b": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "not OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00070c": { + "test_image_upgrade_image_upgrade_common_00070c": { "RETURN_CODE": 404, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "Not Found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00070d": { + "test_image_upgrade_image_upgrade_common_00070d": { "RETURN_CODE": 500, "METHOD": "GET", "REQUEST_PATH": "https://blah_noop", "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00080a": { + "test_image_upgrade_image_upgrade_common_00080a": { "TEST_NOTES": [ "RETURN_CODE does not matter", "_handle_delete_post_put_response() considers only the MESSAGE field" @@ -136,7 +136,7 @@ "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00080b": { + "test_image_upgrade_image_upgrade_common_00080b": { "TEST_NOTES": [ "RETURN_CODE does not matter", "_handle_delete_post_put_response() considers only the MESSAGE field" @@ -147,7 +147,7 @@ "MESSAGE": "error: image not found", "DATA": {} }, - "test_image_mgmt_image_upgrade_common_00080c": { + "test_image_upgrade_image_upgrade_common_00080c": { "TEST_NOTES": [ "RETURN_CODE does not matter", "_handle_delete_post_put_response() considers only the MESSAGE field" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json index c65864e91..ee9557d91 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_validate_00020a": { + "test_image_upgrade_validate_00020a": { "TEST_NOTES": [ "Needed only for the 200 return code" ], @@ -14,7 +14,7 @@ "message": "" } }, - "test_image_mgmt_validate_00021a": { + "test_image_upgrade_validate_00021a": { "TEST_NOTES": [ "Needed only for the 200 return code", "RETURN_CODE == 200", @@ -31,7 +31,7 @@ "message": "" } }, - "test_image_mgmt_validate_00023a": { + "test_image_upgrade_validate_00023a": { "TEST_NOTES": [ "RETURN_CODE == 501", "MESSAGE == INTERNAL SERVER ERROR" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json index 212dc7fb5..780d62bf2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_switch_details_00020a": { + "test_image_upgrade_switch_details_00020a": { "TEST_NOTES": [ "RETURN_CODE: 200", "DATA.ipAddress" @@ -14,7 +14,7 @@ } ] }, - "test_image_mgmt_switch_details_00021a": { + "test_image_upgrade_switch_details_00021a": { "TEST_NOTES": [ "RETURN_CODE: 200", "DATA: key/values removed which are not needed by the test" @@ -39,7 +39,7 @@ } ] }, - "test_image_mgmt_switch_details_00022a": { + "test_image_upgrade_switch_details_00022a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", @@ -50,21 +50,21 @@ } ] }, - "test_image_mgmt_switch_details_00022b": { + "test_image_upgrade_switch_details_00022b": { "RETURN_CODE": 404, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/bad/path", "MESSAGE": "Not Found", "DATA": {} }, - "test_image_mgmt_switch_details_00022c": { + "test_image_upgrade_switch_details_00022c": { "RETURN_CODE": 500, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "MESSAGE": "Internal Server Error", "DATA": {} }, - "test_image_mgmt_switch_details_00023a": { + "test_image_upgrade_switch_details_00023a": { "TEST_NOTES": [ "RETURN_CODE: 200", "DATA: key/values removed which are not needed by the test" @@ -89,7 +89,7 @@ } ] }, - "test_image_mgmt_switch_details_00024a": { + "test_image_upgrade_switch_details_00024a": { "TEST_NOTES": [ "RETURN_CODE: 200", "DATA: Contains ipAddress which is not known" @@ -104,7 +104,7 @@ } ] }, - "test_image_mgmt_switch_details_00025a": { + "test_image_upgrade_switch_details_00025a": { "TEST_NOTES": [ "RETURN_CODE: 200", "DATA: Does not contain a key named FOO", @@ -120,7 +120,7 @@ } ] }, - "test_image_mgmt_image_policy_action_00021a": { + "test_image_upgrade_image_policy_action_00021a": { "TEST_NOTES": [ "RETURN_CODE: 200", "MESSAGE: OK" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 2df3c4354..7d9d611dd 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -1,5 +1,5 @@ { - "test_image_mgmt_switch_issu_details_by_device_name_00020a": { + "test_image_upgrade_switch_issu_details_by_device_name_00020a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.deviceName is required by the test" @@ -18,7 +18,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_device_name_00021a": { + "test_image_upgrade_switch_issu_details_by_device_name_00021a": { "TEST_NOTES": [ "RETURN_CODE 200", "Two switches present", @@ -79,7 +79,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_device_name_00022a": { + "test_image_upgrade_switch_issu_details_by_device_name_00022a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.deviceName is required by the test" @@ -98,7 +98,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_device_name_00023a": { + "test_image_upgrade_switch_issu_details_by_device_name_00023a": { "TEST_NOTES": [ "RETURN_CODE 404", "MESSAGE != OK", @@ -115,7 +115,7 @@ "path": "/bad/path" } }, - "test_image_mgmt_switch_issu_details_by_device_name_00024a": { + "test_image_upgrade_switch_issu_details_by_device_name_00024a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA is empty" @@ -126,7 +126,7 @@ "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_switch_issu_details_by_device_name_00025a": { + "test_image_upgrade_switch_issu_details_by_device_name_00025a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.lastOperDataObject is empty" @@ -141,7 +141,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_device_name_00040a": { + "test_image_upgrade_switch_issu_details_by_device_name_00040a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.deviceName != FOO is required by the test" @@ -160,7 +160,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_device_name_00041a": { + "test_image_upgrade_switch_issu_details_by_device_name_00041a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.deviceName == leaf1 is required by the test" @@ -179,7 +179,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_ip_address_00020a": { + "test_image_upgrade_switch_issu_details_by_ip_address_00020a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.lastOperDataObject.ipAddress is required by the test" @@ -198,7 +198,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_ip_address_00021a": { + "test_image_upgrade_switch_issu_details_by_ip_address_00021a": { "TEST_NOTES": [ "RETURN_CODE 200", "Two switches present", @@ -260,7 +260,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_ip_address_00022a": { + "test_image_upgrade_switch_issu_details_by_ip_address_00022a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.ipAddress is required by the test" @@ -279,7 +279,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_ip_address_00023a": { + "test_image_upgrade_switch_issu_details_by_ip_address_00023a": { "TEST_NOTES": [ "RETURN_CODE 404", "MESSAGE != OK", @@ -296,7 +296,7 @@ "path": "/bad/path" } }, - "test_image_mgmt_switch_issu_details_by_ip_address_00024a": { + "test_image_upgrade_switch_issu_details_by_ip_address_00024a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA is empty" @@ -307,7 +307,7 @@ "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_switch_issu_details_by_ip_address_00025a": { + "test_image_upgrade_switch_issu_details_by_ip_address_00025a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.lastOperDataObject is empty" @@ -322,7 +322,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_ip_address_00040a": { + "test_image_upgrade_switch_issu_details_by_ip_address_00040a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.ipAddress != 1.1.1.1 is required by the test" @@ -341,7 +341,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_ip_address_00041a": { + "test_image_upgrade_switch_issu_details_by_ip_address_00041a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.ipAddress == 172.22.150.102 is required by the test" @@ -360,7 +360,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_serial_number_00020a": { + "test_image_upgrade_switch_issu_details_by_serial_number_00020a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.lastOperDataObject.serialNumber is required by the test" @@ -379,7 +379,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_serial_number_00021a": { + "test_image_upgrade_switch_issu_details_by_serial_number_00021a": { "TEST_NOTES": [ "RETURN_CODE 200", "Two switches present", @@ -440,7 +440,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_serial_number_00022a": { + "test_image_upgrade_switch_issu_details_by_serial_number_00022a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.serialNumber is required by the test" @@ -459,7 +459,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_serial_number_00023a": { + "test_image_upgrade_switch_issu_details_by_serial_number_00023a": { "TEST_NOTES": [ "RETURN_CODE 404", "MESSAGE != OK", @@ -476,7 +476,7 @@ "path": "/bad/path" } }, - "test_image_mgmt_switch_issu_details_by_serial_number_00024a": { + "test_image_upgrade_switch_issu_details_by_serial_number_00024a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA is empty" @@ -487,7 +487,7 @@ "MESSAGE": "OK", "DATA": {} }, - "test_image_mgmt_switch_issu_details_by_serial_number_00025a": { + "test_image_upgrade_switch_issu_details_by_serial_number_00025a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.lastOperDataObject is empty" @@ -502,7 +502,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_serial_number_00040a": { + "test_image_upgrade_switch_issu_details_by_serial_number_00040a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.serialNumber != FOO00000BAR is required by the test" @@ -521,7 +521,7 @@ "message": "" } }, - "test_image_mgmt_switch_issu_details_by_serial_number_00041a": { + "test_image_upgrade_switch_issu_details_by_serial_number_00041a": { "TEST_NOTES": [ "RETURN_CODE 200", "DATA.serialNumber == FDO21120U5D is required by the test" @@ -540,7 +540,7 @@ "message": "" } }, - "test_image_mgmt_stage_00004a": { + "test_image_upgrade_stage_00004a": { "TEST_NOTES": [ "FDO2112189M: imageStaged == none", "FDO211218AX: imageStaged == none", @@ -579,7 +579,7 @@ "message": "" } }, - "test_image_mgmt_stage_00005a": { + "test_image_upgrade_stage_00005a": { "TEST_NOTES": [ "FDO21120U5D: imageStaged == Success", "FDO2112189M: imageStaged == Failed", @@ -606,7 +606,7 @@ "message": "" } }, - "test_image_mgmt_stage_00006a": { + "test_image_upgrade_stage_00006a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -696,7 +696,7 @@ "message": "" } }, - "test_image_mgmt_stage_00007a": { + "test_image_upgrade_stage_00007a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -786,7 +786,7 @@ "message": "" } }, - "test_image_mgmt_stage_00008a": { + "test_image_upgrade_stage_00008a": { "TEST_NOTES": [ "RETURN_CODE == 200", "DATA.lastOperDataObject.imageStaged == Success", @@ -807,7 +807,7 @@ "message": "" } }, - "test_image_mgmt_stage_00009a": { + "test_image_upgrade_stage_00009a": { "TEST_NOTES": [ "RETURN_CODE == 200", "Using only for RETURN_CODE == 200" @@ -827,7 +827,7 @@ "message": "" } }, - "test_image_mgmt_stage_00010a": { + "test_image_upgrade_stage_00010a": { "TEST_NOTES": [ "RETURN_CODE == 200", "Using only for RETURN_CODE == 200" @@ -847,7 +847,7 @@ "message": "" } }, - "test_image_mgmt_stage_00020a": { + "test_image_upgrade_stage_00020a": { "TEST_NOTES": [ "RETURN_CODE == 200", "DATA.lastOperDataObject.serialNumber is present and is this specific value", @@ -882,7 +882,7 @@ "message": "" } }, - "test_image_mgmt_stage_00021a": { + "test_image_upgrade_stage_00021a": { "TEST_NOTES": [ "RETURN_CODE == 200", "Entries for both serial numbers FDO21120U5D FDO2112189M are present", @@ -917,7 +917,7 @@ "message": "" } }, - "test_image_mgmt_stage_00022a": { + "test_image_upgrade_stage_00022a": { "TEST_NOTES": [ "RETURN_CODE == 200", "Entries for both serial numbers FDO21120U5D FDO2112189M are present", @@ -952,7 +952,7 @@ "message": "" } }, - "test_image_mgmt_stage_00030a": { + "test_image_upgrade_stage_00030a": { "TEST_NOTES": [ "FDO21120U5D upgrade, validated, imageStaged == Success", "FDO2112189M upgrade, validated, imageStaged == Success" @@ -980,7 +980,7 @@ "message": "" } }, - "test_image_mgmt_stage_00031a": { + "test_image_upgrade_stage_00031a": { "TEST_NOTES": [ "FDO21120U5D upgrade, validated, imageStaged == Success", "FDO2112189M upgrade, validated == Success", @@ -1009,7 +1009,7 @@ "message": "" } }, - "test_image_mgmt_validate_00003a": { + "test_image_upgrade_validate_00003a": { "TEST_NOTES": [ "FDO2112189M validated: none", "FDO211218AX validated: none", @@ -1223,7 +1223,7 @@ "message": "" } }, - "test_image_mgmt_validate_00004a": { + "test_image_upgrade_validate_00004a": { "TEST_NOTES": [ "FDO21120U5D validated: Success", "FDO2112189M validated: Failed" @@ -1317,7 +1317,7 @@ "message": "" } }, - "test_image_mgmt_validate_00005a": { + "test_image_upgrade_validate_00005a": { "TEST_NOTES": [ "FDO21120U5D validated: Success", "FDO2112189M validated: Success" @@ -1411,7 +1411,7 @@ "message": "" } }, - "test_image_mgmt_validate_00006a": { + "test_image_upgrade_validate_00006a": { "TEST_NOTES": [ "FDO21120U5D validated: Success", "FDO2112189M validated: Failed" @@ -1505,7 +1505,7 @@ "message": "" } }, - "test_image_mgmt_validate_00007a": { + "test_image_upgrade_validate_00007a": { "TEST_NOTES": [ "FDO21120U5D validated: Success", "FDO21120U5D imageStagedPercent: 100", @@ -1601,7 +1601,7 @@ "message": "" } }, - "test_image_mgmt_validate_00008a": { + "test_image_upgrade_validate_00008a": { "TEST_NOTES": [ "FDO21120U5D imageStaged, upgrade, validated: Success", "FDO2112189M imageStaged, upgrade, validated: Success" @@ -1695,7 +1695,7 @@ "message": "" } }, - "test_image_mgmt_validate_00009a": { + "test_image_upgrade_validate_00009a": { "TEST_NOTES": [ "FDO21120U5D imageStaged, upgrade, validated: Success", "FDO2112189M imageStaged, upgrade: Success", @@ -1790,7 +1790,7 @@ "message": "" } }, - "test_image_mgmt_validate_00020a": { + "test_image_upgrade_validate_00020a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1880,7 +1880,7 @@ "message": "" } }, - "test_image_mgmt_validate_00021a": { + "test_image_upgrade_validate_00021a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -1970,7 +1970,7 @@ "message": "" } }, - "test_image_mgmt_validate_00023a": { + "test_image_upgrade_validate_00023a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2060,7 +2060,7 @@ "message": "" } }, - "test_image_mgmt_image_policy_action_00003a": { + "test_image_upgrade_image_policy_action_00003a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2267,7 +2267,7 @@ "message": "" } }, - "test_image_mgmt_image_policy_action_00004a": { + "test_image_upgrade_image_policy_action_00004a": { "TEST_NOTES": [ "deviceName: null" ], @@ -2321,7 +2321,7 @@ "message": "" } }, - "test_image_mgmt_image_policy_action_00013a": { + "test_image_upgrade_image_policy_action_00013a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2372,7 +2372,7 @@ "message": "" } }, - "test_image_mgmt_image_policy_action_00020a": { + "test_image_upgrade_image_policy_action_00020a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2423,7 +2423,7 @@ "message": "" } }, - "test_image_mgmt_image_policy_action_00021a": { + "test_image_upgrade_image_policy_action_00021a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -2474,7 +2474,7 @@ "message": "" } }, - "test_image_mgmt_upgrade_00004a": { + "test_image_upgrade_upgrade_00004a": { "TEST_NOTES": [ "FDO21120U5D imageStaged == Success", "FDO2112189M imageStage == Success" @@ -2568,7 +2568,7 @@ "message": "" } }, - "test_image_mgmt_upgrade_00005a": { + "test_image_upgrade_upgrade_00005a": { "TEST_NOTES": [ "FDO21120U5D imageStaged == Success", "FDO2112189M imageStage == Failed" @@ -2662,7 +2662,7 @@ "message": "" } }, - "test_image_mgmt_upgrade_00019a": { + "test_image_upgrade_upgrade_00019a": { "DATA": { "lastOperDataObject": [ { @@ -2713,7 +2713,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00020a": { + "test_image_upgrade_upgrade_00020a": { "DATA": { "lastOperDataObject": [ { @@ -2764,7 +2764,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00021a": { + "test_image_upgrade_upgrade_00021a": { "DATA": { "lastOperDataObject": [ { @@ -2815,7 +2815,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00022a": { + "test_image_upgrade_upgrade_00022a": { "DATA": { "lastOperDataObject": [ { @@ -2866,7 +2866,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00023a": { + "test_image_upgrade_upgrade_00023a": { "DATA": { "lastOperDataObject": [ { @@ -2917,7 +2917,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00024a": { + "test_image_upgrade_upgrade_00024a": { "DATA": { "lastOperDataObject": [ { @@ -2968,7 +2968,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00025a": { + "test_image_upgrade_upgrade_00025a": { "DATA": { "lastOperDataObject": [ { @@ -3019,7 +3019,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00026a": { + "test_image_upgrade_upgrade_00026a": { "DATA": { "lastOperDataObject": [ { @@ -3070,7 +3070,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00027a": { + "test_image_upgrade_upgrade_00027a": { "DATA": { "lastOperDataObject": [ { @@ -3121,7 +3121,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00028a": { + "test_image_upgrade_upgrade_00028a": { "DATA": { "lastOperDataObject": [ { @@ -3172,7 +3172,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00029a": { + "test_image_upgrade_upgrade_00029a": { "DATA": { "lastOperDataObject": [ { @@ -3223,7 +3223,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00030a": { + "test_image_upgrade_upgrade_00030a": { "DATA": { "lastOperDataObject": [ { @@ -3274,7 +3274,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00031a": { + "test_image_upgrade_upgrade_00031a": { "DATA": { "lastOperDataObject": [ { @@ -3325,7 +3325,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00032a": { + "test_image_upgrade_upgrade_00032a": { "DATA": { "lastOperDataObject": [ { @@ -3376,7 +3376,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00033a": { + "test_image_upgrade_upgrade_00033a": { "DATA": { "lastOperDataObject": [ { @@ -3427,7 +3427,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00045a": { + "test_image_upgrade_upgrade_00045a": { "DATA": { "lastOperDataObject": [ { @@ -3478,7 +3478,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00046a": { + "test_image_upgrade_upgrade_00046a": { "DATA": { "lastOperDataObject": [ { @@ -3529,7 +3529,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00047a": { + "test_image_upgrade_upgrade_00047a": { "DATA": { "lastOperDataObject": [ { @@ -3580,7 +3580,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_mgmt_upgrade_00080a": { + "test_image_upgrade_upgrade_00080a": { "TEST_NOTES": [ "172.22.150.102 validated, upgrade, imageStaged: Success", "172.22.150.108 validated, upgrade, imageStaged: Success" @@ -3674,7 +3674,7 @@ "message": "" } }, - "test_image_mgmt_upgrade_00081a": { + "test_image_upgrade_upgrade_00081a": { "TEST_NOTES": [ "172.22.150.102 upgrade: Success", "172.22.150.108 upgrade: In-Progress", @@ -3769,7 +3769,7 @@ "message": "" } }, - "test_image_mgmt_upgrade_00090a": { + "test_image_upgrade_upgrade_00090a": { "TEST_NOTES": [ "172.22.150.102 upgrade: Success", "172.22.150.108 upgrade: Failed", @@ -3864,7 +3864,7 @@ "message": "" } }, - "test_image_mgmt_upgrade_00091a": { + "test_image_upgrade_upgrade_00091a": { "TEST_NOTES": [ "172.22.150.102 upgrade: Success", "172.22.150.108 upgrade: In-Progress", @@ -3959,7 +3959,7 @@ "message": "" } }, - "test_image_mgmt_upgrade_task_00020a": { + "test_image_upgrade_upgrade_task_00020a": { "TEST_NOTES": [ "RETURN_CODE 200", "Two switches present", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index 291eb2c5e..c0efcb3b0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -27,23 +27,23 @@ ControllerVersion from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_policies import \ ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_policy_action import \ ImagePolicyAction -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_stage import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_stage import \ ImageStage -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade import \ ImageUpgrade -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade_common import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_validate import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_validate import \ ImageValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.install_options import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.install_options import \ ImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_details import \ SwitchDetails -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import ( SwitchIssuDetailsByDeviceName, SwitchIssuDetailsByIpAddress, SwitchIssuDetailsBySerialNumber) from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py index 4ba374315..099ba8e53 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_api_endpoints.py @@ -19,11 +19,11 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -def test_image_mgmt_api_00001() -> None: +def test_image_upgrade_api_00001() -> None: """ Endpoints.__init__ """ @@ -53,7 +53,7 @@ def test_image_mgmt_api_00001() -> None: ) -def test_image_mgmt_api_00002() -> None: +def test_image_upgrade_api_00002() -> None: """ Endpoints.bootflash_info """ @@ -65,7 +65,7 @@ def test_image_mgmt_api_00002() -> None: ) -def test_image_mgmt_api_00003() -> None: +def test_image_upgrade_api_00003() -> None: """ Endpoints.install_options """ @@ -77,7 +77,7 @@ def test_image_mgmt_api_00003() -> None: ) -def test_image_mgmt_api_00004() -> None: +def test_image_upgrade_api_00004() -> None: """ Endpoints.image_stage """ @@ -89,7 +89,7 @@ def test_image_mgmt_api_00004() -> None: ) -def test_image_mgmt_api_00005() -> None: +def test_image_upgrade_api_00005() -> None: """ Endpoints.image_upgrade """ @@ -101,7 +101,7 @@ def test_image_mgmt_api_00005() -> None: ) -def test_image_mgmt_api_00006() -> None: +def test_image_upgrade_api_00006() -> None: """ Endpoints.image_validate """ @@ -113,7 +113,7 @@ def test_image_mgmt_api_00006() -> None: ) -def test_image_mgmt_api_00007() -> None: +def test_image_upgrade_api_00007() -> None: """ Endpoints.issu_info """ @@ -125,7 +125,7 @@ def test_image_mgmt_api_00007() -> None: ) -def test_image_mgmt_api_00008() -> None: +def test_image_upgrade_api_00008() -> None: """ Endpoints.controller_version """ @@ -137,7 +137,7 @@ def test_image_mgmt_api_00008() -> None: ) -def test_image_mgmt_api_00009() -> None: +def test_image_upgrade_api_00009() -> None: """ Endpoints.policies_attached_info """ @@ -149,7 +149,7 @@ def test_image_mgmt_api_00009() -> None: ) -def test_image_mgmt_api_00010() -> None: +def test_image_upgrade_api_00010() -> None: """ Endpoints.policies_info """ @@ -161,7 +161,7 @@ def test_image_mgmt_api_00010() -> None: ) -def test_image_mgmt_api_00011() -> None: +def test_image_upgrade_api_00011() -> None: """ Endpoints.policy_attach """ @@ -173,7 +173,7 @@ def test_image_mgmt_api_00011() -> None: ) -def test_image_mgmt_api_00012() -> None: +def test_image_upgrade_api_00012() -> None: """ Endpoints.policy_create """ @@ -185,7 +185,7 @@ def test_image_mgmt_api_00012() -> None: ) -def test_image_mgmt_api_00013() -> None: +def test_image_upgrade_api_00013() -> None: """ Endpoints.policy_detach """ @@ -197,7 +197,7 @@ def test_image_mgmt_api_00013() -> None: ) -def test_image_mgmt_api_00014() -> None: +def test_image_upgrade_api_00014() -> None: """ Endpoints.policy_info """ @@ -208,7 +208,7 @@ def test_image_mgmt_api_00014() -> None: assert endpoints.policy_info.get("path") == path -def test_image_mgmt_api_00015() -> None: +def test_image_upgrade_api_00015() -> None: """ Endpoints.stage_info """ @@ -220,7 +220,7 @@ def test_image_mgmt_api_00015() -> None: ) -def test_image_mgmt_api_00016() -> None: +def test_image_upgrade_api_00016() -> None: """ Endpoints.switches_info """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index d134d9d42..255f57eee 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -31,7 +31,7 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, @@ -39,11 +39,11 @@ responses_image_install_options) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_INSTALL_OPTIONS = PATCH_image_upgrade + "install_options.dcnm_send" -def test_image_mgmt_install_options_00001(image_install_options) -> None: +def test_image_upgrade_install_options_00001(image_install_options) -> None: """ Function - __init__ @@ -64,7 +64,7 @@ def test_image_mgmt_install_options_00001(image_install_options) -> None: assert instance.compatibility_status == {} -def test_image_mgmt_install_options_00002(image_install_options) -> None: +def test_image_upgrade_install_options_00002(image_install_options) -> None: """ Function - _init_properties @@ -90,7 +90,7 @@ def test_image_mgmt_install_options_00002(image_install_options) -> None: assert instance.properties.get("unit_test") is False -def test_image_mgmt_install_options_00003(image_install_options) -> None: +def test_image_upgrade_install_options_00003(image_install_options) -> None: """ Function - refresh @@ -109,7 +109,7 @@ def test_image_mgmt_install_options_00003(image_install_options) -> None: instance.refresh() -def test_image_mgmt_install_options_00004(image_install_options) -> None: +def test_image_upgrade_install_options_00004(image_install_options) -> None: """ Function - refresh @@ -128,7 +128,7 @@ def test_image_mgmt_install_options_00004(image_install_options) -> None: image_install_options.refresh() -def test_image_mgmt_install_options_00005(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00005(monkeypatch, image_install_options) -> None: """ Function - refresh @@ -140,7 +140,7 @@ def test_image_mgmt_install_options_00005(monkeypatch, image_install_options) -> """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00005a" + key = "test_image_upgrade_install_options_00005a" return responses_image_install_options(key) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -174,7 +174,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_mgmt_install_options_00006(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00006(monkeypatch, image_install_options) -> None: """ Function - refresh @@ -184,7 +184,7 @@ def test_image_mgmt_install_options_00006(monkeypatch, image_install_options) -> """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00006a" + key = "test_image_upgrade_install_options_00006a" return responses_image_install_options(key) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -201,7 +201,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_install_options_00007(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00007(monkeypatch, image_install_options) -> None: """ Function - refresh @@ -222,7 +222,7 @@ def test_image_mgmt_install_options_00007(monkeypatch, image_install_options) -> """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00007a" + key = "test_image_upgrade_install_options_00007a" return responses_image_install_options(key) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -258,7 +258,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_mgmt_install_options_00008(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00008(monkeypatch, image_install_options) -> None: """ Function - refresh @@ -279,7 +279,7 @@ def test_image_mgmt_install_options_00008(monkeypatch, image_install_options) -> """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00008a" + key = "test_image_upgrade_install_options_00008a" return responses_image_install_options(key) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -319,7 +319,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_mgmt_install_options_00009(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00009(monkeypatch, image_install_options) -> None: """ Function - refresh @@ -340,7 +340,7 @@ def test_image_mgmt_install_options_00009(monkeypatch, image_install_options) -> """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00009a" + key = "test_image_upgrade_install_options_00009a" return responses_image_install_options(key) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -380,7 +380,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_mgmt_install_options_00010(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00010(monkeypatch, image_install_options) -> None: """ Function - refresh @@ -403,7 +403,7 @@ def test_image_mgmt_install_options_00010(monkeypatch, image_install_options) -> """ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_install_options_00010a" + key = "test_image_upgrade_install_options_00010a" return responses_image_install_options(key) monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) @@ -421,7 +421,7 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_install_options_00011(image_install_options) -> None: +def test_image_upgrade_install_options_00011(image_install_options) -> None: """ Function - refresh @@ -463,7 +463,7 @@ def test_image_mgmt_install_options_00011(image_install_options) -> None: assert instance.response_data.get("errMessage") == "" -def test_image_mgmt_install_options_00020(image_install_options) -> None: +def test_image_upgrade_install_options_00020(image_install_options) -> None: """ Function - build_payload @@ -486,7 +486,7 @@ def test_image_mgmt_install_options_00020(image_install_options) -> None: assert instance.payload.get("packageInstall") is False -def test_image_mgmt_install_options_00021(image_install_options) -> None: +def test_image_upgrade_install_options_00021(image_install_options) -> None: """ Function - build_payload @@ -513,7 +513,7 @@ def test_image_mgmt_install_options_00021(image_install_options) -> None: assert instance.payload.get("packageInstall") is True -def test_image_mgmt_install_options_00022(image_install_options) -> None: +def test_image_upgrade_install_options_00022(image_install_options) -> None: """ Function - issu setter @@ -530,7 +530,7 @@ def test_image_mgmt_install_options_00022(image_install_options) -> None: instance.issu = "FOO" -def test_image_mgmt_install_options_00023(image_install_options) -> None: +def test_image_upgrade_install_options_00023(image_install_options) -> None: """ Function - epld setter @@ -547,7 +547,7 @@ def test_image_mgmt_install_options_00023(image_install_options) -> None: instance.epld = "FOO" -def test_image_mgmt_install_options_00024(image_install_options) -> None: +def test_image_upgrade_install_options_00024(image_install_options) -> None: """ Function - package_install setter diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index dc9a02976..5609a054f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -31,7 +31,7 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, @@ -39,11 +39,11 @@ responses_image_policies) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_IMAGE_POLICIES = PATCH_image_upgrade + "image_policies.dcnm_send" -def test_image_mgmt_image_policies_00001(image_policies) -> None: +def test_image_upgrade_image_policies_00001(image_policies) -> None: """ Function - __init__ @@ -58,7 +58,7 @@ def test_image_mgmt_image_policies_00001(image_policies) -> None: assert isinstance(instance.endpoints, ApiEndpoints) -def test_image_mgmt_image_policies_00002(image_policies) -> None: +def test_image_upgrade_image_policies_00002(image_policies) -> None: """ Function - _init_properties @@ -75,7 +75,7 @@ def test_image_mgmt_image_policies_00002(image_policies) -> None: assert instance.properties.get("result") is None -def test_image_mgmt_image_policies_00010(monkeypatch, image_policies) -> None: +def test_image_upgrade_image_policies_00010(monkeypatch, image_policies) -> None: """ Function - refresh @@ -88,7 +88,7 @@ def test_image_mgmt_image_policies_00010(monkeypatch, image_policies) -> None: Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - key = "test_image_mgmt_image_policies_00010a" + key = "test_image_upgrade_image_policies_00010a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) @@ -114,7 +114,7 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: assert instance.rpm_images is None -def test_image_mgmt_image_policies_00020(monkeypatch, image_policies) -> None: +def test_image_upgrade_image_policies_00020(monkeypatch, image_policies) -> None: """ Function - refresh @@ -125,7 +125,7 @@ def test_image_mgmt_image_policies_00020(monkeypatch, image_policies) -> None: Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - key = "test_image_mgmt_image_policies_00020a" + key = "test_image_upgrade_image_policies_00020a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -141,7 +141,7 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: assert instance.result.get("success") is True -def test_image_mgmt_image_policies_00021(monkeypatch, image_policies) -> None: +def test_image_upgrade_image_policies_00021(monkeypatch, image_policies) -> None: """ Function - refresh @@ -152,7 +152,7 @@ def test_image_mgmt_image_policies_00021(monkeypatch, image_policies) -> None: Endpoint - /bad/path """ - key = "test_image_mgmt_image_policies_00021a" + key = "test_image_upgrade_image_policies_00021a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -168,7 +168,7 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_image_policies_00022(monkeypatch, image_policies) -> None: +def test_image_upgrade_image_policies_00022(monkeypatch, image_policies) -> None: """ Function - refresh @@ -179,7 +179,7 @@ def test_image_mgmt_image_policies_00022(monkeypatch, image_policies) -> None: Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - key = "test_image_mgmt_image_policies_00022a" + key = "test_image_upgrade_image_policies_00022a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -195,7 +195,7 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_image_policies_00023(monkeypatch, image_policies) -> None: +def test_image_upgrade_image_policies_00023(monkeypatch, image_policies) -> None: """ Function - refresh @@ -213,7 +213,7 @@ def test_image_mgmt_image_policies_00023(monkeypatch, image_policies) -> None: they are creating already exist on the controller. Hence, we cannot fail_json when the length of DATA.lastOperDataObject is zero. """ - key = "test_image_mgmt_image_policies_00023a" + key = "test_image_upgrade_image_policies_00023a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -226,7 +226,7 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_image_policies_00024(monkeypatch, image_policies) -> None: +def test_image_upgrade_image_policies_00024(monkeypatch, image_policies) -> None: """ Function - refresh @@ -239,7 +239,7 @@ def test_image_mgmt_image_policies_00024(monkeypatch, image_policies) -> None: Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - key = "test_image_mgmt_image_policies_00024a" + key = "test_image_upgrade_image_policies_00024a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -255,7 +255,7 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: assert image_policies.policy is None -def test_image_mgmt_image_policies_00025(monkeypatch, image_policies) -> None: +def test_image_upgrade_image_policies_00025(monkeypatch, image_policies) -> None: """ Function - refresh @@ -271,7 +271,7 @@ def test_image_mgmt_image_policies_00025(monkeypatch, image_policies) -> None: - This scenario should never happen. - Consider removing this check, and this testcase. """ - key = "test_image_mgmt_image_policies_00025a" + key = "test_image_upgrade_image_policies_00025a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") @@ -287,7 +287,7 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_image_policies_00040(image_policies) -> None: +def test_image_upgrade_image_policies_00040(image_policies) -> None: """ Function - _get diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 7300c05d5..3b5de5866 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -31,11 +31,11 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policy_action import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_policy_action import \ ImagePolicyAction -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture @@ -48,15 +48,15 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_MGMT + "image_policies.dcnm_send" -DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" -DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.RestSend.commit" -DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +DCNM_SEND_IMAGE_POLICIES = PATCH_image_upgrade + "image_policies.dcnm_send" +DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_image_upgrade + "image_upgrade_common.dcnm_send" +DCNM_SEND_SWITCH_DETAILS = PATCH_image_upgrade + "switch_details.RestSend.commit" +DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" -def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: +def test_image_upgrade_image_policy_action_00001(image_policy_action) -> None: """ Function - __init__ @@ -77,7 +77,7 @@ def test_image_mgmt_image_policy_action_00001(image_policy_action) -> None: assert instance.verb is None -def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: +def test_image_upgrade_image_policy_action_00002(image_policy_action) -> None: """ Function - _init_properties @@ -97,7 +97,7 @@ def test_image_mgmt_image_policy_action_00002(image_policy_action) -> None: assert instance.properties.get("serial_numbers") is None -def test_image_mgmt_image_policy_action_00003( +def test_image_upgrade_image_policy_action_00003( monkeypatch, image_policy_action, issu_details_by_serial_number ) -> None: """ @@ -115,7 +115,7 @@ def test_image_mgmt_image_policy_action_00003( """ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_image_policy_action_00003a" + key = "test_image_upgrade_image_policy_action_00003a" return responses_switch_issu_details(key) monkeypatch.setattr( @@ -138,7 +138,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: assert len(instance.payloads) == 5 -def test_image_mgmt_image_policy_action_00004( +def test_image_upgrade_image_policy_action_00004( monkeypatch, image_policy_action, issu_details_by_serial_number ) -> None: """ @@ -156,7 +156,7 @@ def test_image_mgmt_image_policy_action_00004( """ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_image_policy_action_00004a" + key = "test_image_upgrade_image_policy_action_00004a" return responses_switch_issu_details(key) monkeypatch.setattr( @@ -177,7 +177,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: instance.build_payload() -def test_image_mgmt_image_policy_action_00010( +def test_image_upgrade_image_policy_action_00010( image_policy_action, issu_details_by_serial_number ) -> None: """ @@ -217,7 +217,7 @@ def test_image_mgmt_image_policy_action_00010( ("query", pytest.raises(AnsibleFailJson, match=MATCH_00011)), ], ) -def test_image_mgmt_image_policy_action_00011( +def test_image_upgrade_image_policy_action_00011( action, expected, image_policy_action, issu_details_by_serial_number ) -> None: """ @@ -256,7 +256,7 @@ def test_image_mgmt_image_policy_action_00011( ("query", does_not_raise()), ], ) -def test_image_mgmt_image_policy_action_00012( +def test_image_upgrade_image_policy_action_00012( action, expected, image_policy_action, issu_details_by_serial_number ) -> None: """ @@ -286,7 +286,7 @@ def test_image_mgmt_image_policy_action_00012( instance.validate_request() -def test_image_mgmt_image_policy_action_00013( +def test_image_upgrade_image_policy_action_00013( monkeypatch, image_policy_action, issu_details_by_serial_number, image_policies ) -> None: """ @@ -305,7 +305,7 @@ def test_image_mgmt_image_policy_action_00013( If any of these validations fail, the function calls fail_json with a validation-specific error message. """ - key = "test_image_mgmt_image_policy_action_00013a" + key = "test_image_upgrade_image_policy_action_00013a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -333,7 +333,7 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance.validate_request() -def test_image_mgmt_image_policy_action_00020(monkeypatch, image_policy_action) -> None: +def test_image_upgrade_image_policy_action_00020(monkeypatch, image_policy_action) -> None: """ Function - commit @@ -358,7 +358,7 @@ def test_image_mgmt_image_policy_action_00020(monkeypatch, image_policy_action) Since action == "FOO" is not covered in commit()'s if clauses, the else clause is taken and fail_json is called. """ - key = "test_image_mgmt_image_policy_action_00020a" + key = "test_image_upgrade_image_policy_action_00020a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -388,7 +388,7 @@ def mock_validate_request(*args) -> None: instance.commit() -def test_image_mgmt_image_policy_action_00021(monkeypatch, image_policy_action) -> None: +def test_image_upgrade_image_policy_action_00021(monkeypatch, image_policy_action) -> None: """ Function - commit @@ -406,7 +406,7 @@ def test_image_mgmt_image_policy_action_00021(monkeypatch, image_policy_action) action == "detach" : _detach_policy action == "query" : _query_policy """ - key = "test_image_mgmt_image_policy_action_00021a" + key = "test_image_upgrade_image_policy_action_00021a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) @@ -460,7 +460,7 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060)), ], ) -def test_image_mgmt_image_policy_action_00060( +def test_image_upgrade_image_policy_action_00060( image_policy_action, value, expected ) -> None: """ @@ -492,7 +492,7 @@ def test_image_mgmt_image_policy_action_00060( ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00061)), ], ) -def test_image_mgmt_image_policy_action_00061( +def test_image_upgrade_image_policy_action_00061( image_policy_action, value, expected ) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index a0193d238..ff0aa70f7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -34,9 +34,9 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, @@ -47,11 +47,11 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." PATCH_COMMON = PATCH_MODULE_UTILS + "common." -PATCH_IMAGE_STAGE_REST_SEND_COMMIT = PATCH_IMAGE_MGMT + "image_stage.RestSend.commit" +PATCH_IMAGE_STAGE_REST_SEND_COMMIT = PATCH_image_upgrade + "image_stage.RestSend.commit" PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT = ( - PATCH_IMAGE_MGMT + "image_stage.RestSend.result_current" + PATCH_image_upgrade + "image_stage.RestSend.result_current" ) PATCH_IMAGE_STAGE_POPULATE_CONTROLLER_VERSION = ( "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade." @@ -59,10 +59,10 @@ ) DCNM_SEND_CONTROLLER_VERSION = PATCH_COMMON + "controller_version.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" -def test_image_mgmt_stage_00001(image_stage) -> None: +def test_image_upgrade_stage_00001(image_stage) -> None: """ Function - __init__ @@ -83,7 +83,7 @@ def test_image_mgmt_stage_00001(image_stage) -> None: assert isinstance(instance.endpoints, ApiEndpoints) -def test_image_mgmt_stage_00002(image_stage) -> None: +def test_image_upgrade_stage_00002(image_stage) -> None: """ Function - _init_properties @@ -104,18 +104,18 @@ def test_image_mgmt_stage_00002(image_stage) -> None: @pytest.mark.parametrize( "key, expected", [ - ("test_image_mgmt_stage_00003a", "12.1.2e"), - ("test_image_mgmt_stage_00003b", "12.1.3b"), + ("test_image_upgrade_stage_00003a", "12.1.2e"), + ("test_image_upgrade_stage_00003b", "12.1.3b"), ], ) -def test_image_mgmt_stage_00003(monkeypatch, image_stage, key, expected) -> None: +def test_image_upgrade_stage_00003(monkeypatch, image_stage, key, expected) -> None: """ Function - _populate_controller_version Test - - test_image_mgmt_stage_00003a -> instance.controller_version == "12.1.2e" - - test_image_mgmt_stage_00003b -> instance.controller_version == "12.1.3b" + - test_image_upgrade_stage_00003a -> instance.controller_version == "12.1.2e" + - test_image_upgrade_stage_00003b -> instance.controller_version == "12.1.3b" Description _populate_controller_version retrieves the controller version from @@ -134,7 +134,7 @@ def mock_dcnm_send_controller_version(*args) -> Dict[str, Any]: assert instance.controller_version == expected -def test_image_mgmt_stage_00004( +def test_image_upgrade_stage_00004( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -155,7 +155,7 @@ def test_image_mgmt_stage_00004( prune_serial_numbers removes serial numbers from the list for which imageStaged == "Success" """ - key = "test_image_mgmt_stage_00004a" + key = "test_image_upgrade_stage_00004a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -181,7 +181,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: assert "FDO211218GC" not in instance.serial_numbers -def test_image_mgmt_stage_00005( +def test_image_upgrade_stage_00005( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -200,7 +200,7 @@ def test_image_mgmt_stage_00005( number and raises fail_json if imageStaged == "Failed" for any serial number. """ - key = "test_image_mgmt_stage_00005a" + key = "test_image_upgrade_stage_00005a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -230,7 +230,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: (False, pytest.raises(AnsibleFailJson, match=MATCH_00006)), ], ) -def test_image_mgmt_stage_00006( +def test_image_upgrade_stage_00006( monkeypatch, image_stage, serial_numbers_is_set, expected ) -> None: """ @@ -245,7 +245,7 @@ def test_image_mgmt_stage_00006( - fail_json is called when serial_numbers is None - fail_json is not called when serial_numbers is set """ - key = "test_image_mgmt_stage_00006a" + key = "test_image_upgrade_stage_00006a" def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: return responses_controller_version(key) @@ -268,7 +268,7 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_stage_00007(monkeypatch, image_stage) -> None: +def test_image_upgrade_stage_00007(monkeypatch, image_stage) -> None: """ Function - commit @@ -281,7 +281,7 @@ def test_image_mgmt_stage_00007(monkeypatch, image_stage) -> None: - ImageStage.path is set to: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image """ - key = "test_image_mgmt_stage_00007a" + key = "test_image_upgrade_stage_00007a" def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: return responses_controller_version(key) @@ -316,7 +316,7 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: ("12.1.3b", "serialNumbers"), ], ) -def test_image_mgmt_stage_00008( +def test_image_upgrade_stage_00008( monkeypatch, image_stage, controller_version, expected_serial_number_key ) -> None: """ @@ -335,7 +335,7 @@ def test_image_mgmt_stage_00008( commit() will set the payload key name for the serial number based on the controller version, per Expected Results below """ - key = "test_image_mgmt_stage_00008a" + key = "test_image_upgrade_stage_00008a" def mock_controller_version(*args) -> None: instance.controller_version = controller_version @@ -362,7 +362,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: assert expected_serial_number_key in instance.payload.keys() -def test_image_mgmt_stage_00009(monkeypatch, image_stage) -> None: +def test_image_upgrade_stage_00009(monkeypatch, image_stage) -> None: """ Function - commit @@ -385,7 +385,7 @@ def test_image_mgmt_stage_00009(monkeypatch, image_stage) -> None: When len(serial_numbers) == 0, commit() will set result and response properties, and return without doing anything else. """ - key = "test_image_mgmt_stage_00009a" + key = "test_image_upgrade_stage_00009a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -407,7 +407,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: assert instance.response_data == [instance.response_current.get("DATA")] -def test_image_mgmt_stage_00010(monkeypatch, image_stage) -> None: +def test_image_upgrade_stage_00010(monkeypatch, image_stage) -> None: """ Function - commit @@ -425,7 +425,7 @@ def test_image_mgmt_stage_00010(monkeypatch, image_stage) -> None: Description commit() will call fail_json() on non-success response from the controller. """ - key = "test_image_mgmt_stage_00010a" + key = "test_image_upgrade_stage_00010a" def mock_controller_version(*args) -> None: instance.controller_version = "12.1.3b" @@ -450,7 +450,7 @@ def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_stage_00020( +def test_image_upgrade_stage_00020( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -474,7 +474,7 @@ def test_image_mgmt_stage_00020( In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ - key = "test_image_mgmt_stage_00020a" + key = "test_image_upgrade_stage_00020a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -495,7 +495,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_stage_00021( +def test_image_upgrade_stage_00021( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -522,7 +522,7 @@ def test_image_mgmt_stage_00021( In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ - key = "test_image_mgmt_stage_00021a" + key = "test_image_upgrade_stage_00021a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -547,7 +547,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_stage_00022( +def test_image_upgrade_stage_00022( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -572,7 +572,7 @@ def test_image_mgmt_stage_00022( Description See test_wait_for_image_stage_to_complete for functional details. """ - key = "test_image_mgmt_stage_00022a" + key = "test_image_upgrade_stage_00022a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -601,7 +601,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_stage_00030( +def test_image_upgrade_stage_00030( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -629,7 +629,7 @@ def test_image_mgmt_stage_00030( - upgrade - validated """ - key = "test_image_mgmt_stage_00030a" + key = "test_image_upgrade_stage_00030a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -650,7 +650,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_stage_00031( +def test_image_upgrade_stage_00031( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: """ @@ -671,9 +671,9 @@ def test_image_mgmt_stage_00031( imageStaged == "In-Progress" Description - See test_image_mgmt_stage_00030 for functional details. + See test_image_upgrade_stage_00030 for functional details. """ - key = "test_image_mgmt_stage_00031a" + key = "test_image_upgrade_stage_00031a" def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -713,7 +713,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: ("a", None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), ], ) -def test_image_mgmt_stage_00040(image_stage, input, output, context) -> None: +def test_image_upgrade_stage_00040(image_stage, input, output, context) -> None: """ Function - check_interval @@ -746,7 +746,7 @@ def test_image_mgmt_stage_00040(image_stage, input, output, context) -> None: ("a", None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), ], ) -def test_image_mgmt_stage_00050(image_stage, input, output, context) -> None: +def test_image_upgrade_stage_00050(image_stage, input, output, context) -> None: """ Function - check_interval @@ -781,7 +781,7 @@ def test_image_mgmt_stage_00050(image_stage, input, output, context) -> None: (["DD001115F"], ["DD001115F"], does_not_raise()), ], ) -def test_image_mgmt_stage_00060(image_stage, input, output, context) -> None: +def test_image_upgrade_stage_00060(image_stage, input, output, context) -> None: """ Function - serial_numbers diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index f2aa0b773..213605cc7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -34,7 +34,7 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_upgrade import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade import \ ImageUpgrade from .image_upgrade_utils import (does_not_raise, image_upgrade_fixture, @@ -45,25 +45,25 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT = ( - PATCH_IMAGE_MGMT + "image_upgrade.RestSend.commit" + PATCH_image_upgrade + "image_upgrade.RestSend.commit" ) PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT = ( - PATCH_IMAGE_MGMT + "image_upgrade.RestSend.response_current" + PATCH_image_upgrade + "image_upgrade.RestSend.response_current" ) PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT = ( - PATCH_IMAGE_MGMT + "image_upgrade.RestSend.result_current" + PATCH_image_upgrade + "image_upgrade.RestSend.result_current" ) -REST_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.RestSend" -DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_MGMT + "image_upgrade_common.dcnm_send" -DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +REST_SEND_IMAGE_UPGRADE = PATCH_image_upgrade + "image_upgrade.RestSend" +DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_image_upgrade + "image_upgrade_common.dcnm_send" +DCNM_SEND_INSTALL_OPTIONS = PATCH_image_upgrade + "install_options.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" -def test_image_mgmt_upgrade_00001(image_upgrade) -> None: +def test_image_upgrade_upgrade_00001(image_upgrade) -> None: """ Function - __init__ @@ -84,7 +84,7 @@ def test_image_mgmt_upgrade_00001(image_upgrade) -> None: assert instance.verb == "POST" -def test_image_mgmt_upgrade_00003(image_upgrade) -> None: +def test_image_upgrade_upgrade_00003(image_upgrade) -> None: """ Function - _init_properties @@ -121,7 +121,7 @@ def test_image_mgmt_upgrade_00003(image_upgrade) -> None: } -def test_image_mgmt_upgrade_00004(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00004(monkeypatch, image_upgrade) -> None: """ Function - validate_devices @@ -146,7 +146,7 @@ def test_image_mgmt_upgrade_00004(monkeypatch, image_upgrade) -> None: instance.devices = devices def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_upgrade_00004a" + key = "test_image_upgrade_upgrade_00004a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -158,7 +158,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" in instance.ip_addresses -def test_image_mgmt_upgrade_00005(image_upgrade) -> None: +def test_image_upgrade_upgrade_00005(image_upgrade) -> None: """ Function - commit @@ -176,7 +176,7 @@ def test_image_mgmt_upgrade_00005(image_upgrade) -> None: instance.commit() -def test_image_mgmt_upgrade_00018(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00018(monkeypatch, image_upgrade) -> None: """ Function - ImageUpgrade.commit @@ -199,7 +199,7 @@ def test_image_mgmt_upgrade_00018(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00019a" + key = "test_image_upgrade_upgrade_00019a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -253,7 +253,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): instance.commit() -def test_image_mgmt_upgrade_00019(monkeypatch, image_upgrade, caplog) -> None: +def test_image_upgrade_upgrade_00019(monkeypatch, image_upgrade, caplog) -> None: """ Function - ImageUpgrade._build_payload @@ -288,7 +288,7 @@ def test_image_mgmt_upgrade_00019(monkeypatch, image_upgrade, caplog) -> None: caplog.set_level(logging.DEBUG) instance = image_upgrade - key = "test_image_mgmt_upgrade_00019a" + key = "test_image_upgrade_upgrade_00019a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -348,7 +348,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: assert instance.payload == payloads_image_upgrade(key) -def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade, caplog) -> None: +def test_image_upgrade_upgrade_00020(monkeypatch, image_upgrade, caplog) -> None: """ Function - ImageUpgrade.commit @@ -379,7 +379,7 @@ def test_image_mgmt_upgrade_00020(monkeypatch, image_upgrade, caplog) -> None: caplog.set_level(logging.DEBUG) instance = image_upgrade - key = "test_image_mgmt_upgrade_00020a" + key = "test_image_upgrade_upgrade_00020a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -438,7 +438,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: assert instance.payload == payloads_image_upgrade(key) -def test_image_mgmt_upgrade_00021(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00021(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -461,7 +461,7 @@ def test_image_mgmt_upgrade_00021(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00021a" + key = "test_image_upgrade_upgrade_00021a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -522,7 +522,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00022(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00022(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -548,7 +548,7 @@ def test_image_mgmt_upgrade_00022(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00022a" + key = "test_image_upgrade_upgrade_00022a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -608,7 +608,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: assert instance.payload["issuUpgradeOptions1"]["nonDisruptive"] is True -def test_image_mgmt_upgrade_00023(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00023(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -634,7 +634,7 @@ def test_image_mgmt_upgrade_00023(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00023a" + key = "test_image_upgrade_upgrade_00023a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -694,7 +694,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: assert instance.payload["issuUpgradeOptions1"]["nonDisruptive"] is False -def test_image_mgmt_upgrade_00024(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00024(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -718,7 +718,7 @@ def test_image_mgmt_upgrade_00024(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00024a" + key = "test_image_upgrade_upgrade_00024a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -777,7 +777,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00025(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00025(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -801,7 +801,7 @@ def test_image_mgmt_upgrade_00025(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00025a" + key = "test_image_upgrade_upgrade_00025a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -862,7 +862,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00026(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00026(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -885,7 +885,7 @@ def test_image_mgmt_upgrade_00026(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00026a" + key = "test_image_upgrade_upgrade_00026a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -945,7 +945,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00027(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00027(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -968,7 +968,7 @@ def test_image_mgmt_upgrade_00027(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00027a" + key = "test_image_upgrade_upgrade_00027a" image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: @@ -1031,7 +1031,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00028(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00028(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1054,7 +1054,7 @@ def test_image_mgmt_upgrade_00028(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00028a" + key = "test_image_upgrade_upgrade_00028a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1113,7 +1113,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00029(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00029(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1137,7 +1137,7 @@ def test_image_mgmt_upgrade_00029(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00029a" + key = "test_image_upgrade_upgrade_00029a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1196,7 +1196,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00030(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00030(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1220,7 +1220,7 @@ def test_image_mgmt_upgrade_00030(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00030a" + key = "test_image_upgrade_upgrade_00030a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1279,7 +1279,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00031(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00031(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1308,7 +1308,7 @@ def test_image_mgmt_upgrade_00031(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00031a" + key = "test_image_upgrade_upgrade_00031a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1367,7 +1367,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00032(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00032(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1392,7 +1392,7 @@ def test_image_mgmt_upgrade_00032(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00032a" + key = "test_image_upgrade_upgrade_00032a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1463,7 +1463,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_mgmt_upgrade_00033(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00033(monkeypatch, image_upgrade) -> None: """ Function - commit @@ -1487,7 +1487,7 @@ def test_image_mgmt_upgrade_00033(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00033a" + key = "test_image_upgrade_upgrade_00033a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -1540,7 +1540,7 @@ def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): # test getter properties -def test_image_mgmt_upgrade_00043(image_upgrade) -> None: +def test_image_upgrade_upgrade_00043(image_upgrade) -> None: """ Function - check_interval @@ -1549,7 +1549,7 @@ def test_image_mgmt_upgrade_00043(image_upgrade) -> None: assert instance.check_interval == 10 -def test_image_mgmt_upgrade_00044(image_upgrade) -> None: +def test_image_upgrade_upgrade_00044(image_upgrade) -> None: """ Function - check_timeout @@ -1558,7 +1558,7 @@ def test_image_mgmt_upgrade_00044(image_upgrade) -> None: assert instance.check_timeout == 1800 -def test_image_mgmt_upgrade_00045(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00045(monkeypatch, image_upgrade) -> None: """ Function - response_data @@ -1579,7 +1579,7 @@ def test_image_mgmt_upgrade_00045(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00045a" + key = "test_image_upgrade_upgrade_00045a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1641,7 +1641,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: assert instance.response_data == [121] -def test_image_mgmt_upgrade_00046(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00046(monkeypatch, image_upgrade) -> None: """ Function - result @@ -1662,7 +1662,7 @@ def test_image_mgmt_upgrade_00046(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00046a" + key = "test_image_upgrade_upgrade_00046a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1722,7 +1722,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: assert instance.result == [{"success": True, "changed": True}] -def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: +def test_image_upgrade_upgrade_00047(monkeypatch, image_upgrade) -> None: """ Function - response @@ -1743,7 +1743,7 @@ def test_image_mgmt_upgrade_00047(monkeypatch, image_upgrade) -> None: """ instance = image_upgrade - key = "test_image_mgmt_upgrade_00047a" + key = "test_image_upgrade_upgrade_00047a" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return {} @@ -1820,7 +1820,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060)), ], ) -def test_image_mgmt_upgrade_00060(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00060(image_upgrade, value, expected) -> None: """ Function - bios_force setter @@ -1847,7 +1847,7 @@ def test_image_mgmt_upgrade_00060(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00061)), ], ) -def test_image_mgmt_upgrade_00061(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00061(image_upgrade, value, expected) -> None: """ Function - config_reload setter @@ -1887,7 +1887,7 @@ def test_image_mgmt_upgrade_00061(image_upgrade, value, expected) -> None: (DATA_00062_FAIL_3, pytest.raises(AnsibleFailJson, match=MATCH_00062_FAIL_3)), ], ) -def test_image_mgmt_upgrade_00062(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00062(image_upgrade, value, expected) -> None: """ Function - devices setter @@ -1910,7 +1910,7 @@ def test_image_mgmt_upgrade_00062(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00063)), ], ) -def test_image_mgmt_upgrade_00063(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00063(image_upgrade, value, expected) -> None: """ Function - disruptive setter @@ -1937,7 +1937,7 @@ def test_image_mgmt_upgrade_00063(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00064)), ], ) -def test_image_mgmt_upgrade_00064(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00064(image_upgrade, value, expected) -> None: """ Function - epld_golden setter @@ -1964,7 +1964,7 @@ def test_image_mgmt_upgrade_00064(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00065)), ], ) -def test_image_mgmt_upgrade_00065(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00065(image_upgrade, value, expected) -> None: """ Function - epld_upgrade setter @@ -1993,7 +1993,7 @@ def test_image_mgmt_upgrade_00065(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00066_FAIL_1)), ], ) -def test_image_mgmt_upgrade_00066(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00066(image_upgrade, value, expected) -> None: """ Function - epld_module setter @@ -2015,7 +2015,7 @@ def test_image_mgmt_upgrade_00066(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00067)), ], ) -def test_image_mgmt_upgrade_00067(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00067(image_upgrade, value, expected) -> None: """ Function - force_non_disruptive setter @@ -2042,7 +2042,7 @@ def test_image_mgmt_upgrade_00067(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00068)), ], ) -def test_image_mgmt_upgrade_00068(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00068(image_upgrade, value, expected) -> None: """ Function - non_disruptive setter @@ -2069,7 +2069,7 @@ def test_image_mgmt_upgrade_00068(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00069)), ], ) -def test_image_mgmt_upgrade_00069(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00069(image_upgrade, value, expected) -> None: """ Function - package_install setter @@ -2096,7 +2096,7 @@ def test_image_mgmt_upgrade_00069(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00070)), ], ) -def test_image_mgmt_upgrade_00070(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00070(image_upgrade, value, expected) -> None: """ Function - package_uninstall setter @@ -2123,7 +2123,7 @@ def test_image_mgmt_upgrade_00070(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00071)), ], ) -def test_image_mgmt_upgrade_00071(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00071(image_upgrade, value, expected) -> None: """ Function - reboot setter @@ -2150,7 +2150,7 @@ def test_image_mgmt_upgrade_00071(image_upgrade, value, expected) -> None: ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00072)), ], ) -def test_image_mgmt_upgrade_00072(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00072(image_upgrade, value, expected) -> None: """ Function - write_erase setter @@ -2165,7 +2165,7 @@ def test_image_mgmt_upgrade_00072(image_upgrade, value, expected) -> None: assert instance.write_erase == expected -def test_image_mgmt_upgrade_00080( +def test_image_upgrade_upgrade_00080( monkeypatch, image_upgrade, issu_details_by_ip_address ) -> None: """ @@ -2194,7 +2194,7 @@ def test_image_mgmt_upgrade_00080( instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_upgrade_00080a" + key = "test_image_upgrade_upgrade_00080a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2213,7 +2213,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" in instance.ipv4_done -def test_image_mgmt_upgrade_00081( +def test_image_upgrade_upgrade_00081( monkeypatch, image_upgrade, issu_details_by_ip_address ) -> None: """ @@ -2224,7 +2224,7 @@ def test_image_mgmt_upgrade_00081( - one switch is added to ipv4_done - fail_json is called due to timeout - See test_image_mgmt_upgrade_00080 for functional details. + See test_image_upgrade_upgrade_00080 for functional details. Expectations: - instance.ipv4_done is a set() @@ -2237,7 +2237,7 @@ def test_image_mgmt_upgrade_00081( instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_upgrade_00081a" + key = "test_image_upgrade_upgrade_00081a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2264,7 +2264,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" not in instance.ipv4_done -def test_image_mgmt_upgrade_00090( +def test_image_upgrade_upgrade_00090( monkeypatch, image_upgrade, issu_details_by_ip_address ) -> None: """ @@ -2290,7 +2290,7 @@ def test_image_mgmt_upgrade_00090( instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_upgrade_00090a" + key = "test_image_upgrade_upgrade_00090a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2315,7 +2315,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" not in instance.ipv4_done -def test_image_mgmt_upgrade_00091( +def test_image_upgrade_upgrade_00091( monkeypatch, image_upgrade, issu_details_by_ip_address ) -> None: """ @@ -2344,7 +2344,7 @@ def test_image_mgmt_upgrade_00091( instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_upgrade_00091a" + key = "test_image_upgrade_upgrade_00091a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 7f16dbc64..509fb9a7d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -41,7 +41,7 @@ responses_image_upgrade_common) -def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: +def test_image_upgrade_image_upgrade_common_00001(image_upgrade_common) -> None: """ Function - __init__ @@ -64,20 +64,20 @@ def test_image_mgmt_image_upgrade_common_00001(image_upgrade_common) -> None: "key, expected", [ ( - "test_image_mgmt_image_upgrade_common_00020a", + "test_image_upgrade_image_upgrade_common_00020a", {"success": True, "changed": True}, ), ( - "test_image_mgmt_image_upgrade_common_00020b", + "test_image_upgrade_image_upgrade_common_00020b", {"success": False, "changed": False}, ), ( - "test_image_mgmt_image_upgrade_common_00020c", + "test_image_upgrade_image_upgrade_common_00020c", {"success": False, "changed": False}, ), ], ) -def test_image_mgmt_image_upgrade_common_00020( +def test_image_upgrade_image_upgrade_common_00020( image_upgrade_common, key, expected ) -> None: """ @@ -107,20 +107,20 @@ def test_image_mgmt_image_upgrade_common_00020( "key, expected", [ ( - "test_image_mgmt_image_upgrade_common_00030a", + "test_image_upgrade_image_upgrade_common_00030a", {"success": True, "changed": True}, ), ( - "test_image_mgmt_image_upgrade_common_00030b", + "test_image_upgrade_image_upgrade_common_00030b", {"success": False, "changed": False}, ), ( - "test_image_mgmt_image_upgrade_common_00030c", + "test_image_upgrade_image_upgrade_common_00030c", {"success": False, "changed": False}, ), ], ) -def test_image_mgmt_image_upgrade_common_00030( +def test_image_upgrade_image_upgrade_common_00030( image_upgrade_common, key, expected ) -> None: """ @@ -150,20 +150,20 @@ def test_image_mgmt_image_upgrade_common_00030( "key, expected", [ ( - "test_image_mgmt_image_upgrade_common_00040a", + "test_image_upgrade_image_upgrade_common_00040a", {"success": True, "changed": True}, ), ( - "test_image_mgmt_image_upgrade_common_00040b", + "test_image_upgrade_image_upgrade_common_00040b", {"success": False, "changed": False}, ), ( - "test_image_mgmt_image_upgrade_common_00040c", + "test_image_upgrade_image_upgrade_common_00040c", {"success": False, "changed": False}, ), ], ) -def test_image_mgmt_image_upgrade_common_00040( +def test_image_upgrade_image_upgrade_common_00040( image_upgrade_common, key, expected ) -> None: """ @@ -193,24 +193,24 @@ def test_image_mgmt_image_upgrade_common_00040( "key, expected", [ ( - "test_image_mgmt_image_upgrade_common_00050a", + "test_image_upgrade_image_upgrade_common_00050a", {"success": True, "found": True}, ), ( - "test_image_mgmt_image_upgrade_common_00050b", + "test_image_upgrade_image_upgrade_common_00050b", {"success": False, "found": False}, ), ( - "test_image_mgmt_image_upgrade_common_00050c", + "test_image_upgrade_image_upgrade_common_00050c", {"success": True, "found": False}, ), ( - "test_image_mgmt_image_upgrade_common_00050d", + "test_image_upgrade_image_upgrade_common_00050d", {"success": False, "found": False}, ), ], ) -def test_image_mgmt_image_upgrade_common_00050( +def test_image_upgrade_image_upgrade_common_00050( image_upgrade_common, key, expected ) -> None: """ @@ -230,7 +230,7 @@ def test_image_mgmt_image_upgrade_common_00050( assert result.get("changed") == expected.get("changed") -def test_image_mgmt_image_upgrade_common_00060(image_upgrade_common) -> None: +def test_image_upgrade_image_upgrade_common_00060(image_upgrade_common) -> None: """ Function - _handle_response @@ -240,7 +240,7 @@ def test_image_mgmt_image_upgrade_common_00060(image_upgrade_common) -> None: """ instance = image_upgrade_common - data = responses_image_upgrade_common("test_image_mgmt_image_upgrade_common_00060a") + data = responses_image_upgrade_common("test_image_upgrade_image_upgrade_common_00060a") with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") @@ -251,24 +251,24 @@ def test_image_mgmt_image_upgrade_common_00060(image_upgrade_common) -> None: "key, expected", [ ( - "test_image_mgmt_image_upgrade_common_00070a", + "test_image_upgrade_image_upgrade_common_00070a", {"success": True, "found": True}, ), ( - "test_image_mgmt_image_upgrade_common_00070b", + "test_image_upgrade_image_upgrade_common_00070b", {"success": False, "found": False}, ), ( - "test_image_mgmt_image_upgrade_common_00070c", + "test_image_upgrade_image_upgrade_common_00070c", {"success": True, "found": False}, ), ( - "test_image_mgmt_image_upgrade_common_00070d", + "test_image_upgrade_image_upgrade_common_00070d", {"success": False, "found": False}, ), ], ) -def test_image_mgmt_image_upgrade_common_00070( +def test_image_upgrade_image_upgrade_common_00070( image_upgrade_common, key, expected ) -> None: """ @@ -295,20 +295,20 @@ def test_image_mgmt_image_upgrade_common_00070( "key, expected", [ ( - "test_image_mgmt_image_upgrade_common_00080a", + "test_image_upgrade_image_upgrade_common_00080a", {"success": True, "changed": True}, ), ( - "test_image_mgmt_image_upgrade_common_00080b", + "test_image_upgrade_image_upgrade_common_00080b", {"success": False, "changed": False}, ), ( - "test_image_mgmt_image_upgrade_common_00080c", + "test_image_upgrade_image_upgrade_common_00080c", {"success": False, "changed": False}, ), ], ) -def test_image_mgmt_image_upgrade_common_00080( +def test_image_upgrade_image_upgrade_common_00080( image_upgrade_common, key, expected ) -> None: """ @@ -350,7 +350,7 @@ def test_image_mgmt_image_upgrade_common_00080( ([1, 2, "3"], [1, 2, "3"]), ], ) -def test_image_mgmt_image_upgrade_common_00090( +def test_image_upgrade_image_upgrade_common_00090( image_upgrade_common, key, expected ) -> None: """ @@ -384,7 +384,7 @@ def test_image_mgmt_image_upgrade_common_00090( ([1, 2, "3"], [1, 2, "3"]), ], ) -def test_image_mgmt_image_upgrade_common_00100( +def test_image_upgrade_image_upgrade_common_00100( image_upgrade_common, key, expected ) -> None: """ @@ -398,7 +398,7 @@ def test_image_mgmt_image_upgrade_common_00100( assert instance.make_none(key) == expected -def test_image_mgmt_image_upgrade_common_00110(image_upgrade_common) -> None: +def test_image_upgrade_image_upgrade_common_00110(image_upgrade_common) -> None: """ Function - log.log_msg @@ -414,7 +414,7 @@ def test_image_mgmt_image_upgrade_common_00110(image_upgrade_common) -> None: assert instance.log.info(message) is None -# def test_image_mgmt_image_upgrade_common_00111(tmp_path, image_upgrade_common) -> None: +# def test_image_upgrade_image_upgrade_common_00111(tmp_path, image_upgrade_common) -> None: # """ # Function # - log.log_msg @@ -478,7 +478,7 @@ def test_image_mgmt_image_upgrade_common_00110(image_upgrade_common) -> None: # assert len(list(tmp_path.iterdir())) == 1 -# def test_image_mgmt_image_upgrade_common_00112(tmp_path, image_upgrade_common) -> None: +# def test_image_upgrade_image_upgrade_common_00112(tmp_path, image_upgrade_common) -> None: # """ # Function # - log.log_msg diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index d607f0083..dfcd8324a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -33,9 +33,9 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.image_policies import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_policies import \ ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_details import \ SwitchDetails from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ ImageUpgradeTask @@ -50,11 +50,11 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_IMAGE_UPGRADE = PATCH_IMAGE_MGMT + "image_upgrade.dcnm_send" -DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_MGMT + "install_options.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +DCNM_SEND_IMAGE_UPGRADE = PATCH_image_upgrade + "image_upgrade.dcnm_send" +DCNM_SEND_INSTALL_OPTIONS = PATCH_image_upgrade + "install_options.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" @pytest.fixture(name="image_upgrade_task_bare") @@ -68,7 +68,7 @@ def image_upgrade_task_bare_fixture(): return ImageUpgradeTask -def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00001(image_upgrade_task_bare) -> None: """ Function - __init__ @@ -112,7 +112,7 @@ def test_image_mgmt_upgrade_task_00001(image_upgrade_task_bare) -> None: assert isinstance(instance.image_policies, ImagePolicies) -def test_image_mgmt_upgrade_task_00002(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00002(image_upgrade_task_bare) -> None: """ Function - __init__ @@ -130,7 +130,7 @@ def test_image_mgmt_upgrade_task_00002(image_upgrade_task_bare) -> None: assert isinstance(instance, ImageUpgradeTask) -def test_image_mgmt_upgrade_task_00020(monkeypatch, image_upgrade_task) -> None: +def test_image_upgrade_upgrade_task_00020(monkeypatch, image_upgrade_task) -> None: """ Function - get_have @@ -138,7 +138,7 @@ def test_image_mgmt_upgrade_task_00020(monkeypatch, image_upgrade_task) -> None: Test - SwitchIssuDetailsByIpAddress attributes are set to expected values """ - key = "test_image_mgmt_upgrade_task_00020a" + key = "test_image_upgrade_upgrade_task_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -155,7 +155,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert instance.have.fabric == "hard" -def test_image_mgmt_upgrade_task_00030(monkeypatch, image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00030(monkeypatch, image_upgrade_task_bare) -> None: """ Function - get_want @@ -170,7 +170,7 @@ def test_image_mgmt_upgrade_task_00030(monkeypatch, image_upgrade_task_bare) -> - switch_2 overrides all global_config options so all values will be non-default """ - key = "test_image_mgmt_upgrade_task_00030a" + key = "test_image_upgrade_upgrade_task_00030a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -216,7 +216,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert switch_2.get("validate") is False -def test_image_mgmt_upgrade_task_00031(monkeypatch, image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00031(monkeypatch, image_upgrade_task_bare) -> None: """ Function - get_want @@ -241,7 +241,7 @@ def test_image_mgmt_upgrade_task_00031(monkeypatch, image_upgrade_task_bare) -> with a non-default value (True) - All other values for switch_1 and switch_2 are default """ - key = "test_image_mgmt_upgrade_task_00031a" + key = "test_image_upgrade_upgrade_task_00031a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -289,7 +289,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert switch_2.get("validate") is True -def test_image_mgmt_upgrade_task_00040(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00040(image_upgrade_task_bare) -> None: """ Function - get_want @@ -304,7 +304,7 @@ def test_image_mgmt_upgrade_task_00040(image_upgrade_task_bare) -> None: Test - instance.switch_configs contains expected default values """ - key = "test_image_mgmt_upgrade_task_00040a" + key = "test_image_upgrade_upgrade_task_00040a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -327,7 +327,7 @@ def test_image_mgmt_upgrade_task_00040(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00041(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00041(image_upgrade_task_bare) -> None: """ Function - get_want @@ -344,7 +344,7 @@ def test_image_mgmt_upgrade_task_00041(image_upgrade_task_bare) -> None: - instance.switch_configs contains expected non-default values """ - key = "test_image_mgmt_upgrade_task_00041a" + key = "test_image_upgrade_upgrade_task_00041a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -367,7 +367,7 @@ def test_image_mgmt_upgrade_task_00041(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00042(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00042(image_upgrade_task_bare) -> None: """ Function - get_want @@ -384,7 +384,7 @@ def test_image_mgmt_upgrade_task_00042(image_upgrade_task_bare) -> None: - instance.switch_configs contains expected non-default values """ - key = "test_image_mgmt_upgrade_task_00042a" + key = "test_image_upgrade_upgrade_task_00042a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -407,7 +407,7 @@ def test_image_mgmt_upgrade_task_00042(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00043(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00043(image_upgrade_task_bare) -> None: """ Function - get_want @@ -427,7 +427,7 @@ def test_image_mgmt_upgrade_task_00043(image_upgrade_task_bare) -> None: Description When options is empty, the default values for all sub-options are added """ - key = "test_image_mgmt_upgrade_task_00043a" + key = "test_image_upgrade_upgrade_task_00043a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -450,7 +450,7 @@ def test_image_mgmt_upgrade_task_00043(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00044(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00044(image_upgrade_task_bare) -> None: """ Function - get_want @@ -471,7 +471,7 @@ def test_image_mgmt_upgrade_task_00044(image_upgrade_task_bare) -> None: When options.nxos.mode is the only key present in options.nxos, options.nxos.bios_force sub-option should be added with default value. """ - key = "test_image_mgmt_upgrade_task_00044a" + key = "test_image_upgrade_upgrade_task_00044a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -494,7 +494,7 @@ def test_image_mgmt_upgrade_task_00044(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00045(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00045(image_upgrade_task_bare) -> None: """ Function - get_want @@ -515,7 +515,7 @@ def test_image_mgmt_upgrade_task_00045(image_upgrade_task_bare) -> None: When options.nxos.bios_force is the only key present in options.nxos, options.nxos.mode sub-option should be added with default value. """ - key = "test_image_mgmt_upgrade_task_00045a" + key = "test_image_upgrade_upgrade_task_00045a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -538,7 +538,7 @@ def test_image_mgmt_upgrade_task_00045(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00046(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00046(image_upgrade_task_bare) -> None: """ Function - get_want @@ -559,7 +559,7 @@ def test_image_mgmt_upgrade_task_00046(image_upgrade_task_bare) -> None: When options.epld.module is the only key present in options.epld, options.epld.golden sub-option should be added with default value. """ - key = "test_image_mgmt_upgrade_task_00046a" + key = "test_image_upgrade_upgrade_task_00046a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -582,7 +582,7 @@ def test_image_mgmt_upgrade_task_00046(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00047(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00047(image_upgrade_task_bare) -> None: """ Function - get_want @@ -603,7 +603,7 @@ def test_image_mgmt_upgrade_task_00047(image_upgrade_task_bare) -> None: When options.epld.golden is the only key present in options.epld, options.epld.module sub-option should be added with default value. """ - key = "test_image_mgmt_upgrade_task_00047a" + key = "test_image_upgrade_upgrade_task_00047a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -626,7 +626,7 @@ def test_image_mgmt_upgrade_task_00047(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00048(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00048(image_upgrade_task_bare) -> None: """ Function - get_want @@ -648,7 +648,7 @@ def test_image_mgmt_upgrade_task_00048(image_upgrade_task_bare) -> None: When options.reboot.config_reload is the only key present in options.reboot, options.reboot.write_erase sub-option should be added with default value. """ - key = "test_image_mgmt_upgrade_task_00048a" + key = "test_image_upgrade_upgrade_task_00048a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -671,7 +671,7 @@ def test_image_mgmt_upgrade_task_00048(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00049(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00049(image_upgrade_task_bare) -> None: """ Function - get_want @@ -693,7 +693,7 @@ def test_image_mgmt_upgrade_task_00049(image_upgrade_task_bare) -> None: When options.reboot.write_erase is the only key present in options.reboot, options.reboot.config_reload sub-option should be added with default value. """ - key = "test_image_mgmt_upgrade_task_00049a" + key = "test_image_upgrade_upgrade_task_00049a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -716,7 +716,7 @@ def test_image_mgmt_upgrade_task_00049(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00050(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00050(image_upgrade_task_bare) -> None: """ Function - get_want @@ -738,7 +738,7 @@ def test_image_mgmt_upgrade_task_00050(image_upgrade_task_bare) -> None: When options.package.install is the only key present in options.package, options.package.uninstall sub-option should be added with default value. """ - key = "test_image_mgmt_upgrade_task_00050a" + key = "test_image_upgrade_upgrade_task_00050a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) @@ -761,7 +761,7 @@ def test_image_mgmt_upgrade_task_00050(image_upgrade_task_bare) -> None: assert instance.switch_configs[0]["options"]["package"]["uninstall"] is False -def test_image_mgmt_upgrade_task_00051(image_upgrade_task_bare) -> None: +def test_image_upgrade_upgrade_task_00051(image_upgrade_task_bare) -> None: """ Function - get_want @@ -783,7 +783,7 @@ def test_image_mgmt_upgrade_task_00051(image_upgrade_task_bare) -> None: When options.package.uninstall is the only key present in options.package, options.package.install sub-option should be added with default value. """ - key = "test_image_mgmt_upgrade_task_00051a" + key = "test_image_upgrade_upgrade_task_00051a" mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index eeab421c6..d5de5b9eb 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -33,9 +33,9 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.api_endpoints import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_issu_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber from .image_upgrade_utils import (does_not_raise, image_validate_fixture, @@ -44,18 +44,18 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -DCNM_SEND_IMAGE_VALIDATE = PATCH_IMAGE_MGMT + "image_validate.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_IMAGE_VALIDATE = PATCH_image_upgrade + "image_validate.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" PATCH_IMAGE_VALIDATE_REST_SEND_COMMIT = ( - PATCH_IMAGE_MGMT + "image_validate.RestSend.commit" + PATCH_image_upgrade + "image_validate.RestSend.commit" ) PATCH_IMAGE_VALIDATE_REST_SEND_RESULT_CURRENT = ( - PATCH_IMAGE_MGMT + "image_validate.RestSend.result_current" + PATCH_image_upgrade + "image_validate.RestSend.result_current" ) -def test_image_mgmt_validate_00001(image_validate) -> None: +def test_image_upgrade_validate_00001(image_validate) -> None: """ Function - __init__ @@ -75,7 +75,7 @@ def test_image_mgmt_validate_00001(image_validate) -> None: assert instance.verb == "POST" -def test_image_mgmt_validate_00002(image_validate) -> None: +def test_image_upgrade_validate_00002(image_validate) -> None: """ Function - _init_properties @@ -94,7 +94,7 @@ def test_image_mgmt_validate_00002(image_validate) -> None: assert instance.properties.get("serial_numbers") == [] -def test_image_mgmt_validate_00003( +def test_image_upgrade_validate_00003( monkeypatch, image_validate, issu_details_by_serial_number ) -> None: """ @@ -118,7 +118,7 @@ def test_image_mgmt_validate_00003( instance = image_validate def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00003a" + key = "test_image_upgrade_validate_00003a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -141,7 +141,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO211218GC" not in instance.serial_numbers -def test_image_mgmt_validate_00004( +def test_image_upgrade_validate_00004( monkeypatch, image_validate, issu_details_by_serial_number ) -> None: """ @@ -160,7 +160,7 @@ def test_image_mgmt_validate_00004( instance = image_validate def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00004a" + key = "test_image_upgrade_validate_00004a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -178,7 +178,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: instance.validate_serial_numbers() -def test_image_mgmt_validate_00005( +def test_image_upgrade_validate_00005( monkeypatch, image_validate, issu_details_by_serial_number ) -> None: """ @@ -200,7 +200,7 @@ def test_image_mgmt_validate_00005( instance = image_validate def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00005a" + key = "test_image_upgrade_validate_00005a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -218,7 +218,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_validate_00006( +def test_image_upgrade_validate_00006( monkeypatch, image_validate, issu_details_by_serial_number ) -> None: """ @@ -242,7 +242,7 @@ def test_image_mgmt_validate_00006( instance = image_validate def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00006a" + key = "test_image_upgrade_validate_00006a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -269,7 +269,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_validate_00007( +def test_image_upgrade_validate_00007( monkeypatch, image_validate, issu_details_by_serial_number ) -> None: """ @@ -290,7 +290,7 @@ def test_image_mgmt_validate_00007( instance = image_validate def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00007a" + key = "test_image_upgrade_validate_00007a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -316,7 +316,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_validate_00008( +def test_image_upgrade_validate_00008( monkeypatch, image_validate, issu_details_by_serial_number ) -> None: """ @@ -342,7 +342,7 @@ def test_image_mgmt_validate_00008( instance = image_validate def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00008a" + key = "test_image_upgrade_validate_00008a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -360,7 +360,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" in instance.serial_numbers_done -def test_image_mgmt_validate_00009( +def test_image_upgrade_validate_00009( monkeypatch, image_validate, issu_details_by_serial_number ) -> None: """ @@ -381,7 +381,7 @@ def test_image_mgmt_validate_00009( instance = image_validate def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00009a" + key = "test_image_upgrade_validate_00009a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -407,7 +407,7 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_mgmt_validate_00021(monkeypatch, image_validate) -> None: +def test_image_upgrade_validate_00021(monkeypatch, image_validate) -> None: """ Function - commit @@ -420,11 +420,11 @@ def test_image_mgmt_validate_00021(monkeypatch, image_validate) -> None: # Needed only for the 200 return code def mock_rest_send_image_validate(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00021a" + key = "test_image_upgrade_validate_00021a" return responses_image_validate(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00021a" + key = "test_image_upgrade_validate_00021a" return responses_switch_issu_details(key) monkeypatch.setattr( @@ -445,7 +445,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert instance.verb == "POST" -def test_image_mgmt_validate_00022(image_validate) -> None: +def test_image_upgrade_validate_00022(image_validate) -> None: """ Function - commit @@ -466,7 +466,7 @@ def test_image_mgmt_validate_00022(image_validate) -> None: assert instance.result == [{"success": True}] -def test_image_mgmt_validate_00023(monkeypatch, image_validate) -> None: +def test_image_upgrade_validate_00023(monkeypatch, image_validate) -> None: """ Function - commit @@ -477,11 +477,11 @@ def test_image_mgmt_validate_00023(monkeypatch, image_validate) -> None: # Needed only for the 501 return code def mock_rest_send_image_validate(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00023a" + key = "test_image_upgrade_validate_00023a" return responses_image_validate(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_validate_00023a" + key = "test_image_upgrade_validate_00023a" return responses_switch_issu_details(key) monkeypatch.setattr( @@ -519,7 +519,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: ({"a": 1, "b": 2}, pytest.raises(AnsibleFailJson, match=MATCH_00030)), ], ) -def test_image_mgmt_validate_00030(image_validate, value, expected) -> None: +def test_image_upgrade_validate_00030(image_validate, value, expected) -> None: """ Function - serial_numbers.setter @@ -552,7 +552,7 @@ def test_image_mgmt_validate_00030(image_validate, value, expected) -> None: ({"a": 1, "b": 2}, pytest.raises(AnsibleFailJson, match=MATCH_00040)), ], ) -def test_image_mgmt_validate_00040(image_validate, value, expected) -> None: +def test_image_upgrade_validate_00040(image_validate, value, expected) -> None: """ Function - non_disruptive.setter @@ -584,7 +584,7 @@ def test_image_mgmt_validate_00040(image_validate, value, expected) -> None: ({"a": 1, "b": 2}, pytest.raises(AnsibleFailJson, match=MATCH_00050)), ], ) -def test_image_mgmt_validate_00050(image_validate, value, expected) -> None: +def test_image_upgrade_validate_00050(image_validate, value, expected) -> None: """ Function - check_interval.setter @@ -616,7 +616,7 @@ def test_image_mgmt_validate_00050(image_validate, value, expected) -> None: ({"a": 1, "b": 2}, pytest.raises(AnsibleFailJson, match=MATCH_00060)), ], ) -def test_image_mgmt_validate_00060(image_validate, value, expected) -> None: +def test_image_upgrade_validate_00060(image_validate, value, expected) -> None: """ Function - check_timeout.setter diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 8c9477172..e8558b417 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -33,25 +33,25 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_mgmt.switch_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_details import \ SwitchDetails from .image_upgrade_utils import (does_not_raise, responses_switch_details, switch_details_fixture) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -PATCH_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details." +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +PATCH_SWITCH_DETAILS = PATCH_image_upgrade + "switch_details." PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT = ( PATCH_SWITCH_DETAILS + "RestSend.response_current" ) PATCH_SWITCH_DETAILS_REST_SEND_RESULT_CURRENT = ( PATCH_SWITCH_DETAILS + "RestSend.result_current" ) -REST_SEND_SWITCH_DETAILS = PATCH_IMAGE_MGMT + "switch_details.RestSend.commit" +REST_SEND_SWITCH_DETAILS = PATCH_image_upgrade + "switch_details.RestSend.commit" -def test_image_mgmt_switch_details_00001(switch_details) -> None: +def test_image_upgrade_switch_details_00001(switch_details) -> None: """ Function - __init__ @@ -64,7 +64,7 @@ def test_image_mgmt_switch_details_00001(switch_details) -> None: assert instance.class_name == "SwitchDetails" -def test_image_mgmt_switch_details_00002(switch_details) -> None: +def test_image_upgrade_switch_details_00002(switch_details) -> None: """ Function - _init_properties @@ -83,7 +83,7 @@ def test_image_mgmt_switch_details_00002(switch_details) -> None: assert instance.properties.get("result_current") == {} -def test_image_mgmt_switch_details_00020(monkeypatch, switch_details) -> None: +def test_image_upgrade_switch_details_00020(monkeypatch, switch_details) -> None: """ Function - refresh @@ -93,7 +93,7 @@ def test_image_mgmt_switch_details_00020(monkeypatch, switch_details) -> None: - X.response_current, X.result_current are dictionaries - X.response_current, X.result_current are set to the mocked RestSend values """ - key = "test_image_mgmt_switch_details_00020a" + key = "test_image_upgrade_switch_details_00020a" def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_details(key) @@ -117,7 +117,7 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: assert instance.response_current == responses_switch_details(key) -def test_image_mgmt_switch_details_00021(monkeypatch, switch_details) -> None: +def test_image_upgrade_switch_details_00021(monkeypatch, switch_details) -> None: """ Function - refresh @@ -130,7 +130,7 @@ def test_image_mgmt_switch_details_00021(monkeypatch, switch_details) -> None: instance = switch_details def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_details_00021a" + key = "test_image_upgrade_switch_details_00021a" return responses_switch_details(key) monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) @@ -163,18 +163,18 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: @pytest.mark.parametrize( "key,expected", [ - ("test_image_mgmt_switch_details_00022a", does_not_raise()), + ("test_image_upgrade_switch_details_00022a", does_not_raise()), ( - "test_image_mgmt_switch_details_00022b", + "test_image_upgrade_switch_details_00022b", pytest.raises(AnsibleFailJson, match=MATCH_00022), ), ( - "test_image_mgmt_switch_details_00022c", + "test_image_upgrade_switch_details_00022c", pytest.raises(AnsibleFailJson, match=MATCH_00022), ), ], ) -def test_image_mgmt_switch_details_00022( +def test_image_upgrade_switch_details_00022( monkeypatch, switch_details, key, expected ) -> None: """ @@ -183,13 +183,13 @@ def test_image_mgmt_switch_details_00022( - RestSend._handle_response Test - - test_image_mgmt_switch_details_00022a + - test_image_upgrade_switch_details_00022a - 200 RETURN_CODE, MESSAGE == "OK" - result == {'found': True, 'success': True} - - test_image_mgmt_switch_details_00022b + - test_image_upgrade_switch_details_00022b - 404 RETURN_CODE, MESSAGE == "Not Found" - result == {'found': False, 'success': True} - - test_image_mgmt_switch_details_00022c + - test_image_upgrade_switch_details_00022c - 500 RETURN_CODE, MESSAGE ~= "Internal Server Error" - result == {'found': False, 'success': False} """ @@ -222,7 +222,7 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: ("switchRole", "border gateway"), ], ) -def test_image_mgmt_switch_details_00023( +def test_image_upgrade_switch_details_00023( monkeypatch, switch_details, item, expected ) -> None: """ @@ -252,7 +252,7 @@ def test_image_mgmt_switch_details_00023( instance = switch_details def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_details_00023a" + key = "test_image_upgrade_switch_details_00023a" return responses_switch_details(key) monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) @@ -265,7 +265,7 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: assert instance._get(item) == expected -def test_image_mgmt_switch_details_00024(monkeypatch, switch_details) -> None: +def test_image_upgrade_switch_details_00024(monkeypatch, switch_details) -> None: """ Function - switch_details.refresh @@ -285,7 +285,7 @@ def test_image_mgmt_switch_details_00024(monkeypatch, switch_details) -> None: instance = switch_details def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_details_00024a" + key = "test_image_upgrade_switch_details_00024a" return responses_switch_details(key) monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) @@ -302,7 +302,7 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: instance._get("hostName") -def test_image_mgmt_switch_details_00025(monkeypatch, switch_details) -> None: +def test_image_upgrade_switch_details_00025(monkeypatch, switch_details) -> None: """ Function - switch_details.refresh @@ -320,7 +320,7 @@ def test_image_mgmt_switch_details_00025(monkeypatch, switch_details) -> None: instance = switch_details def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_details_00025a" + key = "test_image_upgrade_switch_details_00025a" return responses_switch_details(key) monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) @@ -346,7 +346,7 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: (False, None), ], ) -def test_image_mgmt_switch_details_00060( +def test_image_upgrade_switch_details_00060( switch_details, ip_address_is_set, expected ) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index 723dd2709..a042ddff0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -37,11 +37,11 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" -def test_image_mgmt_switch_issu_details_by_device_name_00001( +def test_image_upgrade_switch_issu_details_by_device_name_00001( issu_details_by_device_name, ) -> None: """ @@ -57,7 +57,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00001( assert isinstance(instance.properties, dict) -def test_image_mgmt_switch_issu_details_by_device_name_00002( +def test_image_upgrade_switch_issu_details_by_device_name_00002( issu_details_by_device_name, ) -> None: """ @@ -84,7 +84,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00002( assert instance.properties.get("device_name") is None -def test_image_mgmt_switch_issu_details_by_device_name_00020( +def test_image_upgrade_switch_issu_details_by_device_name_00020( monkeypatch, issu_details_by_device_name ) -> None: """ @@ -96,7 +96,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00020( - instance.response_data is a list """ - key = "test_image_mgmt_switch_issu_details_by_device_name_00020a" + key = "test_image_upgrade_switch_issu_details_by_device_name_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -109,7 +109,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert isinstance(instance.response_data, list) -def test_image_mgmt_switch_issu_details_by_device_name_00021( +def test_image_upgrade_switch_issu_details_by_device_name_00021( monkeypatch, issu_details_by_device_name ) -> None: """ @@ -122,7 +122,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00021( """ instance = issu_details_by_device_name - key = "test_image_mgmt_switch_issu_details_by_device_name_00021a" + key = "test_image_upgrade_switch_issu_details_by_device_name_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -189,7 +189,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert instance.vpc_role2 == "BAR" -def test_image_mgmt_switch_issu_details_by_device_name_00022( +def test_image_upgrade_switch_issu_details_by_device_name_00022( monkeypatch, issu_details_by_device_name ) -> None: """ @@ -202,7 +202,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00022( """ instance = issu_details_by_device_name - key = "test_image_mgmt_switch_issu_details_by_device_name_00022a" + key = "test_image_upgrade_switch_issu_details_by_device_name_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -217,7 +217,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_mgmt_switch_issu_details_by_device_name_00023( +def test_image_upgrade_switch_issu_details_by_device_name_00023( monkeypatch, issu_details_by_device_name ) -> None: """ @@ -230,7 +230,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00023( """ instance = issu_details_by_device_name - key = "test_image_mgmt_switch_issu_details_by_device_name_00023a" + key = "test_image_upgrade_switch_issu_details_by_device_name_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -242,7 +242,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_device_name_00024( +def test_image_upgrade_switch_issu_details_by_device_name_00024( monkeypatch, issu_details_by_device_name ) -> None: """ @@ -255,7 +255,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00024( """ instance = issu_details_by_device_name - key = "test_image_mgmt_switch_issu_details_by_device_name_00024a" + key = "test_image_upgrade_switch_issu_details_by_device_name_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -268,7 +268,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_device_name_00025( +def test_image_upgrade_switch_issu_details_by_device_name_00025( monkeypatch, issu_details_by_device_name ) -> None: """ @@ -281,7 +281,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00025( """ instance = issu_details_by_device_name - key = "test_image_mgmt_switch_issu_details_by_device_name_00025a" + key = "test_image_upgrade_switch_issu_details_by_device_name_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -295,7 +295,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_device_name_00040( +def test_image_upgrade_switch_issu_details_by_device_name_00040( monkeypatch, issu_details_by_device_name ) -> None: """ @@ -315,7 +315,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00040( instance = issu_details_by_device_name def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_issu_details_by_device_name_00040a" + key = "test_image_upgrade_switch_issu_details_by_device_name_00040a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -328,7 +328,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance._get("serialNumber") # pylint: disable=protected-access -def test_image_mgmt_switch_issu_details_by_device_name_00041( +def test_image_upgrade_switch_issu_details_by_device_name_00041( monkeypatch, issu_details_by_device_name ) -> None: """ @@ -349,7 +349,7 @@ def test_image_mgmt_switch_issu_details_by_device_name_00041( instance = issu_details_by_device_name def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_issu_details_by_device_name_00041a" + key = "test_image_upgrade_switch_issu_details_by_device_name_00041a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index d51969f25..9fdf4d5c0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -37,11 +37,11 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" -def test_image_mgmt_switch_issu_details_by_ip_address_00001( +def test_image_upgrade_switch_issu_details_by_ip_address_00001( issu_details_by_ip_address, ) -> None: """ @@ -57,7 +57,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00001( assert isinstance(instance.properties, dict) -def test_image_mgmt_switch_issu_details_by_ip_address_00002( +def test_image_upgrade_switch_issu_details_by_ip_address_00002( issu_details_by_ip_address, ) -> None: """ @@ -86,7 +86,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00002( assert instance.properties.get("ip_address") is None -def test_image_mgmt_switch_issu_details_by_ip_address_00020( +def test_image_upgrade_switch_issu_details_by_ip_address_00020( monkeypatch, issu_details_by_ip_address ) -> None: """ @@ -102,7 +102,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00020( """ instance = issu_details_by_ip_address - key = "test_image_mgmt_switch_issu_details_by_ip_address_00020a" + key = "test_image_upgrade_switch_issu_details_by_ip_address_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -118,7 +118,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert isinstance(instance.response_data, list) -def test_image_mgmt_switch_issu_details_by_ip_address_00021( +def test_image_upgrade_switch_issu_details_by_ip_address_00021( monkeypatch, issu_details_by_ip_address ) -> None: """ @@ -131,7 +131,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00021( """ instance = issu_details_by_ip_address - key = "test_image_mgmt_switch_issu_details_by_ip_address_00021a" + key = "test_image_upgrade_switch_issu_details_by_ip_address_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -198,7 +198,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert instance.vpc_role2 == "BAR" -def test_image_mgmt_switch_issu_details_by_ip_address_00022( +def test_image_upgrade_switch_issu_details_by_ip_address_00022( monkeypatch, issu_details_by_ip_address ) -> None: """ @@ -211,7 +211,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00022( """ instance = issu_details_by_ip_address - key = "test_image_mgmt_switch_issu_details_by_ip_address_00022a" + key = "test_image_upgrade_switch_issu_details_by_ip_address_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -226,7 +226,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_mgmt_switch_issu_details_by_ip_address_00023( +def test_image_upgrade_switch_issu_details_by_ip_address_00023( monkeypatch, issu_details_by_ip_address ) -> None: """ @@ -239,7 +239,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00023( """ instance = issu_details_by_ip_address - key = "test_image_mgmt_switch_issu_details_by_ip_address_00023a" + key = "test_image_upgrade_switch_issu_details_by_ip_address_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -252,7 +252,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_ip_address_00024( +def test_image_upgrade_switch_issu_details_by_ip_address_00024( monkeypatch, issu_details_by_ip_address ) -> None: """ @@ -265,7 +265,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00024( """ instance = issu_details_by_ip_address - key = "test_image_mgmt_switch_issu_details_by_ip_address_00024a" + key = "test_image_upgrade_switch_issu_details_by_ip_address_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -278,7 +278,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_ip_address_00025( +def test_image_upgrade_switch_issu_details_by_ip_address_00025( monkeypatch, issu_details_by_ip_address ) -> None: """ @@ -291,7 +291,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00025( """ instance = issu_details_by_ip_address - key = "test_image_mgmt_switch_issu_details_by_ip_address_00025a" + key = "test_image_upgrade_switch_issu_details_by_ip_address_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -305,7 +305,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_ip_address_00040( +def test_image_upgrade_switch_issu_details_by_ip_address_00040( monkeypatch, issu_details_by_ip_address ) -> None: """ @@ -325,7 +325,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00040( instance = issu_details_by_ip_address def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_issu_details_by_ip_address_00040a" + key = "test_image_upgrade_switch_issu_details_by_ip_address_00040a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -338,7 +338,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance._get("serialNumber") # pylint: disable=protected-access -def test_image_mgmt_switch_issu_details_by_ip_address_00041( +def test_image_upgrade_switch_issu_details_by_ip_address_00041( monkeypatch, issu_details_by_ip_address ) -> None: """ @@ -358,7 +358,7 @@ def test_image_mgmt_switch_issu_details_by_ip_address_00041( instance = issu_details_by_ip_address def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_issu_details_by_ip_address_00041a" + key = "test_image_upgrade_switch_issu_details_by_ip_address_00041a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 3df71f658..f95592dbe 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -38,11 +38,11 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_IMAGE_MGMT = PATCH_MODULE_UTILS + "image_mgmt." -DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_MGMT + "switch_issu_details.dcnm_send" +PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" -def test_image_mgmt_switch_issu_details_by_serial_number_00001( +def test_image_upgrade_switch_issu_details_by_serial_number_00001( issu_details_by_serial_number, ) -> None: """ @@ -58,7 +58,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00001( assert isinstance(instance.properties, dict) -def test_image_mgmt_switch_issu_details_by_serial_number_00002( +def test_image_upgrade_switch_issu_details_by_serial_number_00002( issu_details_by_serial_number, ) -> None: """ @@ -86,7 +86,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00002( assert instance.properties.get("serial_number") is None -def test_image_mgmt_switch_issu_details_by_serial_number_00020( +def test_image_upgrade_switch_issu_details_by_serial_number_00020( monkeypatch, issu_details_by_serial_number ) -> None: """ @@ -102,7 +102,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00020( """ instance = issu_details_by_serial_number - key = "test_image_mgmt_switch_issu_details_by_serial_number_00020a" + key = "test_image_upgrade_switch_issu_details_by_serial_number_00020a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: print(f"mock_dcnm_send_issu_details: {responses_switch_issu_details(key)}") @@ -118,7 +118,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert isinstance(instance.response_data, list) -def test_image_mgmt_switch_issu_details_by_serial_number_00021( +def test_image_upgrade_switch_issu_details_by_serial_number_00021( monkeypatch, issu_details_by_serial_number ) -> None: """ @@ -131,7 +131,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00021( """ instance = issu_details_by_serial_number - key = "test_image_mgmt_switch_issu_details_by_serial_number_00021a" + key = "test_image_upgrade_switch_issu_details_by_serial_number_00021a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -197,7 +197,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert instance.vpc_role2 == "BAR" -def test_image_mgmt_switch_issu_details_by_serial_number_00022( +def test_image_upgrade_switch_issu_details_by_serial_number_00022( monkeypatch, issu_details_by_serial_number ) -> None: """ @@ -210,7 +210,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00022( """ instance = issu_details_by_serial_number - key = "test_image_mgmt_switch_issu_details_by_serial_number_00022a" + key = "test_image_upgrade_switch_issu_details_by_serial_number_00022a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -223,7 +223,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_mgmt_switch_issu_details_by_serial_number_00023( +def test_image_upgrade_switch_issu_details_by_serial_number_00023( monkeypatch, issu_details_by_serial_number ) -> None: """ @@ -236,7 +236,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00023( """ instance = issu_details_by_serial_number - key = "test_image_mgmt_switch_issu_details_by_serial_number_00023a" + key = "test_image_upgrade_switch_issu_details_by_serial_number_00023a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -248,7 +248,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_serial_number_00024( +def test_image_upgrade_switch_issu_details_by_serial_number_00024( monkeypatch, issu_details_by_serial_number ) -> None: """ @@ -261,7 +261,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00024( """ instance = issu_details_by_serial_number - key = "test_image_mgmt_switch_issu_details_by_serial_number_00024a" + key = "test_image_upgrade_switch_issu_details_by_serial_number_00024a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -274,7 +274,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_serial_number_00025( +def test_image_upgrade_switch_issu_details_by_serial_number_00025( monkeypatch, issu_details_by_serial_number ) -> None: """ @@ -287,7 +287,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00025( """ instance = issu_details_by_serial_number - key = "test_image_mgmt_switch_issu_details_by_serial_number_00025a" + key = "test_image_upgrade_switch_issu_details_by_serial_number_00025a" def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -300,7 +300,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_mgmt_switch_issu_details_by_serial_number_00040( +def test_image_upgrade_switch_issu_details_by_serial_number_00040( monkeypatch, issu_details_by_serial_number ) -> None: """ @@ -320,7 +320,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00040( instance = issu_details_by_serial_number def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_issu_details_by_serial_number_00040a" + key = "test_image_upgrade_switch_issu_details_by_serial_number_00040a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -334,7 +334,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance._get("serialNumber") # pylint: disable=protected-access -def test_image_mgmt_switch_issu_details_by_serial_number_00041( +def test_image_upgrade_switch_issu_details_by_serial_number_00041( monkeypatch, issu_details_by_serial_number ) -> None: """ @@ -354,7 +354,7 @@ def test_image_mgmt_switch_issu_details_by_serial_number_00041( instance = issu_details_by_serial_number def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_mgmt_switch_issu_details_by_serial_number_00041a" + key = "test_image_upgrade_switch_issu_details_by_serial_number_00041a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) From 1be103ec06f991b877cbcd29aa356629958e6751 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 20 Feb 2024 17:23:26 -1000 Subject: [PATCH 256/300] ImageStage: Add unit test for successful commit, more... This commit brings ImageStage to 98% coverage. Added unit test "test_image_stage_00075" Also: - Run unit test scripts thru black/isort/pylint - Renumber ImageStage.commit() unit tests to be in range 00070 - 00079 - SwitchIssuDetails: Add debug message to .refresh() in all subclasses. - ImageStage.commit: modify debug message --- .../module_utils/image_upgrade/image_stage.py | 2 +- .../image_upgrade/switch_issu_details.py | 12 + ...e_upgrade_responses_ControllerVersion.json | 4 +- .../image_upgrade_responses_ImageStage.json | 31 +- ...e_upgrade_responses_SwitchIssuDetails.json | 324 ++++++----- ...est_image_upgrade_image_install_options.py | 24 +- .../test_image_upgrade_image_policy_action.py | 8 +- .../test_image_upgrade_image_stage.py | 524 ++++++++++-------- ...test_image_upgrade_image_upgrade_common.py | 4 +- 9 files changed, 539 insertions(+), 394 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py index 488881563..2da061b19 100644 --- a/plugins/module_utils/image_upgrade/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -172,7 +172,7 @@ def commit(self): """ method_name = inspect.stack()[0][3] - msg = "ENTERED commit()" + msg = f"ENTERED {self.class_name}.{method_name}" self.log.debug(msg) msg = f"self.serial_numbers: {self.serial_numbers}" diff --git a/plugins/module_utils/image_upgrade/switch_issu_details.py b/plugins/module_utils/image_upgrade/switch_issu_details.py index 9e21f92ed..2e3a80636 100644 --- a/plugins/module_utils/image_upgrade/switch_issu_details.py +++ b/plugins/module_utils/image_upgrade/switch_issu_details.py @@ -675,6 +675,10 @@ def refresh(self): for switch in self.response_current["DATA"]["lastOperDataObject"]: self.data_subclass[switch["ipAddress"]] = switch + msg = f"{self.class_name}.refresh(): self.data_subclass: " + msg += f"{json.dumps(self.data_subclass, indent=4, sort_keys=True)}" + self.log.debug(msg) + def _get(self, item): method_name = inspect.stack()[0][3] @@ -760,6 +764,10 @@ def refresh(self): for switch in self.response_current["DATA"]["lastOperDataObject"]: self.data_subclass[switch["serialNumber"]] = switch + msg = f"{self.class_name}.refresh(): self.data_subclass: " + msg += f"{json.dumps(self.data_subclass, indent=4, sort_keys=True)}" + self.log.debug(msg) + def _get(self, item): method_name = inspect.stack()[0][3] @@ -844,6 +852,10 @@ def refresh(self): for switch in self.response_current["DATA"]["lastOperDataObject"]: self.data_subclass[switch["deviceName"]] = switch + msg = f"{self.class_name}.refresh(): self.data_subclass: " + msg += f"{json.dumps(self.data_subclass, indent=4, sort_keys=True)}" + self.log.debug(msg) + def _get(self, item): method_name = inspect.stack()[0][3] diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json index 2ef2018a2..3bb2d335e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ControllerVersion.json @@ -31,7 +31,7 @@ "is_upgrade_inprogress": "false" } }, - "test_image_upgrade_stage_00006a": { + "test_image_upgrade_stage_00070a": { "TEST_NOTES": [ "Needed only for the 200 return code" ], @@ -50,7 +50,7 @@ "is_upgrade_inprogress": "false" } }, - "test_image_upgrade_stage_00007a": { + "test_image_upgrade_stage_00071a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/about/version", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json index 0ce8b8bed..187c589df 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json @@ -1,5 +1,5 @@ { - "test_image_upgrade_stage_00006a": { + "test_image_upgrade_stage_00070a": { "TEST_NOTES": [ "Needed only for the 200 return code" ], @@ -14,7 +14,7 @@ "message": "" } }, - "test_image_upgrade_stage_00007a": { + "test_image_upgrade_stage_00071a": { "TEST_NOTES": [ "RETURN_CODE == 200", "MESSAGE == OK" @@ -30,7 +30,7 @@ "message": "" } }, - "test_image_upgrade_stage_00008a": { + "test_image_upgrade_stage_00072a": { "TEST_NOTES": [ "RETURN_CODE == 200", "MESSAGE == OK" @@ -46,7 +46,7 @@ "message": "" } }, - "test_image_upgrade_stage_00010a": { + "test_image_upgrade_stage_00074a": { "TEST_NOTES": [ "RETURN_CODE == 500", "MESSAGE == NOK" @@ -61,5 +61,28 @@ ], "message": "" } + }, + "test_image_upgrade_stage_00070a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "MESSAGE == OK" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-info", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "deviceName": "leaf1", + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "serialNumber": "FDO21120U5D" + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 7d9d611dd..91a72c0a0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -606,7 +606,169 @@ "message": "" } }, - "test_image_upgrade_stage_00006a": { + "test_image_upgrade_stage_00020a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "DATA.lastOperDataObject.serialNumber is present and is this specific value", + "DATA.lastOperDataObject.imageStaged == Success", + "DATA.lastOperDataObject.imageStagedPercent is present", + "DATA.lastOperDataObject.ipAddress is present", + "DATA.lastOperDataObject.deviceName is present", + "Entries for both serial numbers FDO21120U5D FDO2112189M are present" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "deviceName": "leaf1" + }, + { + "deviceName": "cvd-2313-leaf", + "serialNumber": "FDO2112189M", + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.108" + } + ], + "message": "" + } + }, + "test_image_upgrade_stage_00021a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "Entries for both serial numbers FDO21120U5D FDO2112189M are present", + "FDO21120U5D imageStaged == Success", + "FDO2112189M imageStage == Failed", + "DATA.lastOperDataObject.imageStagedPercent is present", + "DATA.lastOperDataObject.ipAddress is present", + "DATA.lastOperDataObject.deviceName is present" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "deviceName": "leaf1" + }, + { + "deviceName": "cvd-2313-leaf", + "serialNumber": "FDO2112189M", + "imageStaged": "Failed", + "imageStagedPercent": 90, + "ipAddress": "172.22.150.108" + } + ], + "message": "" + } + }, + "test_image_upgrade_stage_00022a": { + "TEST_NOTES": [ + "RETURN_CODE == 200", + "Entries for both serial numbers FDO21120U5D FDO2112189M are present", + "FDO21120U5D imageStaged == Success", + "FDO2112189M imageStage == In-Progress", + "DATA.lastOperDataObject.imageStagedPercent is present", + "DATA.lastOperDataObject.ipAddress is present", + "DATA.lastOperDataObject.deviceName is present" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success", + "imageStagedPercent": 100, + "ipAddress": "172.22.150.102", + "deviceName": "leaf1" + }, + { + "deviceName": "cvd-2313-leaf", + "serialNumber": "FDO2112189M", + "imageStaged": "In-Progress", + "imageStagedPercent": 90, + "ipAddress": "172.22.150.108" + } + ], + "message": "" + } + }, + "test_image_upgrade_stage_00030a": { + "TEST_NOTES": [ + "FDO21120U5D upgrade, validated, imageStaged == Success", + "FDO2112189M upgrade, validated, imageStaged == Success" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success", + "upgrade": "Success", + "validated": "Success" + }, + { + "serialNumber": "FDO2112189M", + "imageStaged": "Success", + "upgrade": "Success", + "validated": "Success" + } + ], + "message": "" + } + }, + "test_image_upgrade_stage_00031a": { + "TEST_NOTES": [ + "FDO21120U5D upgrade, validated, imageStaged == Success", + "FDO2112189M upgrade, validated == Success", + "FDO2112189M imageStaged == In-Progress" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "imageStaged": "Success", + "upgrade": "Success", + "validated": "Success" + }, + { + "serialNumber": "FDO2112189M", + "imageStaged": "In-Progress", + "upgrade": "Success", + "validated": "Success" + } + ], + "message": "" + } + }, + "test_image_upgrade_stage_00070a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -696,7 +858,7 @@ "message": "" } }, - "test_image_upgrade_stage_00007a": { + "test_image_upgrade_stage_00071a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", @@ -786,7 +948,7 @@ "message": "" } }, - "test_image_upgrade_stage_00008a": { + "test_image_upgrade_stage_00072a": { "TEST_NOTES": [ "RETURN_CODE == 200", "DATA.lastOperDataObject.imageStaged == Success", @@ -807,7 +969,7 @@ "message": "" } }, - "test_image_upgrade_stage_00009a": { + "test_image_upgrade_stage_00073a": { "TEST_NOTES": [ "RETURN_CODE == 200", "Using only for RETURN_CODE == 200" @@ -827,7 +989,7 @@ "message": "" } }, - "test_image_upgrade_stage_00010a": { + "test_image_upgrade_stage_00074a": { "TEST_NOTES": [ "RETURN_CODE == 200", "Using only for RETURN_CODE == 200" @@ -847,15 +1009,16 @@ "message": "" } }, - "test_image_upgrade_stage_00020a": { + "test_image_upgrade_stage_00075a": { "TEST_NOTES": [ "RETURN_CODE == 200", - "DATA.lastOperDataObject.serialNumber is present and is this specific value", + "MESSAGE == OK", + "DATA.lastOperDataObject.deviceName == leaf1", "DATA.lastOperDataObject.imageStaged == Success", - "DATA.lastOperDataObject.imageStagedPercent is present", - "DATA.lastOperDataObject.ipAddress is present", - "DATA.lastOperDataObject.deviceName is present", - "Entries for both serial numbers FDO21120U5D FDO2112189M are present" + "DATA.lastOperDataObject.imageStagedPercent == 100", + "DATA.lastOperDataObject.ipAddress == 172.22.150.102", + "DATA.lastOperDataObject.policy == KR5M", + "DATA.lastOperDataObject.serialNumber == FDO21120U5D" ], "RETURN_CODE": 200, "METHOD": "GET", @@ -865,145 +1028,12 @@ "status": "SUCCESS", "lastOperDataObject": [ { - "serialNumber": "FDO21120U5D", - "imageStaged": "Success", - "imageStagedPercent": 100, - "ipAddress": "172.22.150.102", - "deviceName": "leaf1" - }, - { - "deviceName": "cvd-2313-leaf", - "serialNumber": "FDO2112189M", - "imageStaged": "Success", - "imageStagedPercent": 100, - "ipAddress": "172.22.150.108" - } - ], - "message": "" - } - }, - "test_image_upgrade_stage_00021a": { - "TEST_NOTES": [ - "RETURN_CODE == 200", - "Entries for both serial numbers FDO21120U5D FDO2112189M are present", - "FDO21120U5D imageStaged == Success", - "FDO2112189M imageStage == Failed", - "DATA.lastOperDataObject.imageStagedPercent is present", - "DATA.lastOperDataObject.ipAddress is present", - "DATA.lastOperDataObject.deviceName is present" - ], - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "imageStaged": "Success", - "imageStagedPercent": 100, - "ipAddress": "172.22.150.102", - "deviceName": "leaf1" - }, - { - "deviceName": "cvd-2313-leaf", - "serialNumber": "FDO2112189M", - "imageStaged": "Failed", - "imageStagedPercent": 90, - "ipAddress": "172.22.150.108" - } - ], - "message": "" - } - }, - "test_image_upgrade_stage_00022a": { - "TEST_NOTES": [ - "RETURN_CODE == 200", - "Entries for both serial numbers FDO21120U5D FDO2112189M are present", - "FDO21120U5D imageStaged == Success", - "FDO2112189M imageStage == In-Progress", - "DATA.lastOperDataObject.imageStagedPercent is present", - "DATA.lastOperDataObject.ipAddress is present", - "DATA.lastOperDataObject.deviceName is present" - ], - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", "imageStaged": "Success", "imageStagedPercent": 100, "ipAddress": "172.22.150.102", - "deviceName": "leaf1" - }, - { - "deviceName": "cvd-2313-leaf", - "serialNumber": "FDO2112189M", - "imageStaged": "In-Progress", - "imageStagedPercent": 90, - "ipAddress": "172.22.150.108" - } - ], - "message": "" - } - }, - "test_image_upgrade_stage_00030a": { - "TEST_NOTES": [ - "FDO21120U5D upgrade, validated, imageStaged == Success", - "FDO2112189M upgrade, validated, imageStaged == Success" - ], - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "imageStaged": "Success", - "upgrade": "Success", - "validated": "Success" - }, - { - "serialNumber": "FDO2112189M", - "imageStaged": "Success", - "upgrade": "Success", - "validated": "Success" - } - ], - "message": "" - } - }, - "test_image_upgrade_stage_00031a": { - "TEST_NOTES": [ - "FDO21120U5D upgrade, validated, imageStaged == Success", - "FDO2112189M upgrade, validated == Success", - "FDO2112189M imageStaged == In-Progress" - ], - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "imageStaged": "Success", - "upgrade": "Success", - "validated": "Success" - }, - { - "serialNumber": "FDO2112189M", - "imageStaged": "In-Progress", - "upgrade": "Success", - "validated": "Success" + "policy": "KR5M", + "serialNumber": "FDO21120U5D" } ], "message": "" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index 255f57eee..0d7a88b9c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -128,7 +128,9 @@ def test_image_upgrade_install_options_00004(image_install_options) -> None: image_install_options.refresh() -def test_image_upgrade_install_options_00005(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00005( + monkeypatch, image_install_options +) -> None: """ Function - refresh @@ -174,7 +176,9 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_upgrade_install_options_00006(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00006( + monkeypatch, image_install_options +) -> None: """ Function - refresh @@ -201,7 +205,9 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: instance.refresh() -def test_image_upgrade_install_options_00007(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00007( + monkeypatch, image_install_options +) -> None: """ Function - refresh @@ -258,7 +264,9 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_upgrade_install_options_00008(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00008( + monkeypatch, image_install_options +) -> None: """ Function - refresh @@ -319,7 +327,9 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_upgrade_install_options_00009(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00009( + monkeypatch, image_install_options +) -> None: """ Function - refresh @@ -380,7 +390,9 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: assert instance.result_current.get("success") is True -def test_image_upgrade_install_options_00010(monkeypatch, image_install_options) -> None: +def test_image_upgrade_install_options_00010( + monkeypatch, image_install_options +) -> None: """ Function - refresh diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 3b5de5866..0110bdc0c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -333,7 +333,9 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance.validate_request() -def test_image_upgrade_image_policy_action_00020(monkeypatch, image_policy_action) -> None: +def test_image_upgrade_image_policy_action_00020( + monkeypatch, image_policy_action +) -> None: """ Function - commit @@ -388,7 +390,9 @@ def mock_validate_request(*args) -> None: instance.commit() -def test_image_upgrade_image_policy_action_00021(monkeypatch, image_policy_action) -> None: +def test_image_upgrade_image_policy_action_00021( + monkeypatch, image_policy_action +) -> None: """ Function - commit diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index ff0aa70f7..7f3f39f75 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -30,6 +30,7 @@ __author__ = "Allen Robel" from typing import Any, Dict +from unittest.mock import MagicMock import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ @@ -219,237 +220,6 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: instance.validate_serial_numbers() -MATCH_00006 = "ImageStage.commit: call instance.serial_numbers " -MATCH_00006 += "before calling commit." - - -@pytest.mark.parametrize( - "serial_numbers_is_set, expected", - [ - (True, does_not_raise()), - (False, pytest.raises(AnsibleFailJson, match=MATCH_00006)), - ], -) -def test_image_upgrade_stage_00006( - monkeypatch, image_stage, serial_numbers_is_set, expected -) -> None: - """ - Function - commit - - Summary - Verify that commit raises fail_json appropriately based on value of - instance.serial_numbers. - - Test - - fail_json is called when serial_numbers is None - - fail_json is not called when serial_numbers is set - """ - key = "test_image_upgrade_stage_00006a" - - def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - return responses_image_stage(key) - - def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) - - instance = image_stage - if serial_numbers_is_set: - instance.serial_numbers = ["FDO21120U5D"] - with expected: - instance.commit() - - -def test_image_upgrade_stage_00007(monkeypatch, image_stage) -> None: - """ - Function - - commit - - Summary - Verify that verb is set to POST and path is set to the expected value. - - Test - - ImageStage.verb is set to POST - - ImageStage.path is set to: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image - """ - key = "test_image_upgrade_stage_00007a" - - def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - # Needed only for the 200 return code - def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - return responses_image_stage(key) - - def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) - - module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" - module_path += "stagingmanagement/stage-image" - - instance = image_stage - instance.serial_numbers = ["FDO21120U5D"] - instance.commit() - assert instance.path == module_path - assert instance.verb == "POST" - - -@pytest.mark.parametrize( - "controller_version, expected_serial_number_key", - [ - ("12.1.2e", "sereialNum"), - ("12.1.3b", "serialNumbers"), - ], -) -def test_image_upgrade_stage_00008( - monkeypatch, image_stage, controller_version, expected_serial_number_key -) -> None: - """ - Function - - commit - - Summary - Verify that the serial number key name in the payload is set correctly - based on the controller version. - - Test - - controller_version 12.1.2e -> key name "sereialNum" (yes, misspelled) - - controller_version 12.1.3b -> key name "serialNumbers - - Description - commit() will set the payload key name for the serial number - based on the controller version, per Expected Results below - """ - key = "test_image_upgrade_stage_00008a" - - def mock_controller_version(*args) -> None: - instance.controller_version = controller_version - - controller_version_patch = "ansible_collections.cisco.dcnm.plugins." - controller_version_patch += "modules.dcnm_image_upgrade." - controller_version_patch += "ImageStage._populate_controller_version" - monkeypatch.setattr(controller_version_patch, mock_controller_version) - - def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - return responses_image_stage(key) - - def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) - - instance = image_stage - instance.serial_numbers = ["FDO21120U5D"] - instance.commit() - print(f"instance.payload: {instance.payload.keys()}") - assert expected_serial_number_key in instance.payload.keys() - - -def test_image_upgrade_stage_00009(monkeypatch, image_stage) -> None: - """ - Function - - commit - - Summary - Verify that commit() sets result, response, and response_data - appropriately when serial_numbers is empty. - - Setup - - SwitchIssuDetailsBySerialNumber is mocked to return a successful response - - self.serial_numbers is set to [] (empty list) - - Test - - commit() sets the following to expected values: - - self.result, self.result_current - - self.response, self.response_current - - self.response_data - - Description - When len(serial_numbers) == 0, commit() will set result and - response properties, and return without doing anything else. - """ - key = "test_image_upgrade_stage_00009a" - - def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) - - response_msg = "No files to stage." - with does_not_raise(): - instance = image_stage - instance.serial_numbers = [] - instance.commit() - assert instance.result == [{"success": True, "changed": False}] - assert instance.result_current == {"success": True, "changed": False} - assert instance.response_current == { - "DATA": [{"key": "ALL", "value": response_msg}] - } - assert instance.response == [instance.response_current] - assert instance.response_data == [instance.response_current.get("DATA")] - - -def test_image_upgrade_stage_00010(monkeypatch, image_stage) -> None: - """ - Function - - commit - - Summary - Verify that commit() calls fail_json() on 500 response from the controller. - - Setup - - IssuDetailsBySerialNumber is mocked to return a successful response - - ImageStage is mocked to return a non-successful (500) response - - Test - - commit() will call fail_json() - - Description - commit() will call fail_json() on non-success response from the controller. - """ - key = "test_image_upgrade_stage_00010a" - - def mock_controller_version(*args) -> None: - instance.controller_version = "12.1.3b" - - def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - return responses_image_stage(key) - - monkeypatch.setattr( - PATCH_IMAGE_STAGE_POPULATE_CONTROLLER_VERSION, mock_controller_version - ) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": False}) - - instance = image_stage - instance.serial_numbers = ["FDO21120U5D"] - MATCH = "ImageStage.commit: failed" - with pytest.raises(AnsibleFailJson, match=MATCH): - instance.commit() - - def test_image_upgrade_stage_00020( monkeypatch, image_stage, issu_details_by_serial_number ) -> None: @@ -802,3 +572,295 @@ def test_image_upgrade_stage_00060(image_stage, input, output, context) -> None: instance.serial_numbers = input if output is not None: assert instance.serial_numbers == output + + +MATCH_00070 = "ImageStage.commit: call instance.serial_numbers " +MATCH_00070 += "before calling commit." + + +@pytest.mark.parametrize( + "serial_numbers_is_set, expected", + [ + (True, does_not_raise()), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00070)), + ], +) +def test_image_upgrade_stage_00070( + monkeypatch, image_stage, serial_numbers_is_set, expected +) -> None: + """ + Function + commit + + Summary + Verify that commit raises fail_json appropriately based on value of + instance.serial_numbers. + + Test + - fail_json is called when serial_numbers is None + - fail_json is not called when serial_numbers is set + """ + key = "test_image_upgrade_stage_00070a" + + def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + return responses_image_stage(key) + + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) + + instance = image_stage + if serial_numbers_is_set: + instance.serial_numbers = ["FDO21120U5D"] + with expected: + instance.commit() + + +def test_image_upgrade_stage_00071(monkeypatch, image_stage) -> None: + """ + Function + - commit + + Summary + Verify that verb is set to POST and path is set to the expected value. + + Test + - ImageStage.verb is set to POST + - ImageStage.path is set to: + /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image + """ + key = "test_image_upgrade_stage_00071a" + + def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: + return responses_controller_version(key) + + # Needed only for the 200 return code + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + return responses_image_stage(key) + + def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) + + module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" + module_path += "stagingmanagement/stage-image" + + instance = image_stage + instance.serial_numbers = ["FDO21120U5D"] + instance.commit() + assert instance.path == module_path + assert instance.verb == "POST" + + +@pytest.mark.parametrize( + "controller_version, expected_serial_number_key", + [ + ("12.1.2e", "sereialNum"), + ("12.1.3b", "serialNumbers"), + ], +) +def test_image_upgrade_stage_00072( + monkeypatch, image_stage, controller_version, expected_serial_number_key +) -> None: + """ + Function + - commit + + Summary + Verify that the serial number key name in the payload is set correctly + based on the controller version. + + Test + - controller_version 12.1.2e -> key name "sereialNum" (yes, misspelled) + - controller_version 12.1.3b -> key name "serialNumbers + + Description + commit() will set the payload key name for the serial number + based on the controller version, per Expected Results below + """ + key = "test_image_upgrade_stage_00072a" + + def mock_controller_version(*args) -> None: + instance.controller_version = controller_version + + controller_version_patch = "ansible_collections.cisco.dcnm.plugins." + controller_version_patch += "modules.dcnm_image_upgrade." + controller_version_patch += "ImageStage._populate_controller_version" + monkeypatch.setattr(controller_version_patch, mock_controller_version) + + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + return responses_image_stage(key) + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) + + instance = image_stage + instance.serial_numbers = ["FDO21120U5D"] + instance.commit() + print(f"instance.payload: {instance.payload.keys()}") + assert expected_serial_number_key in instance.payload.keys() + + +def test_image_upgrade_stage_00073(monkeypatch, image_stage) -> None: + """ + Function + - commit + + Summary + Verify that commit() sets result, response, and response_data + appropriately when serial_numbers is empty. + + Setup + - SwitchIssuDetailsBySerialNumber is mocked to return a successful response + - self.serial_numbers is set to [] (empty list) + + Test + - commit() sets the following to expected values: + - self.result, self.result_current + - self.response, self.response_current + - self.response_data + + Description + When len(serial_numbers) == 0, commit() will set result and + response properties, and return without doing anything else. + """ + key = "test_image_upgrade_stage_00073a" + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) + + response_msg = "No files to stage." + with does_not_raise(): + instance = image_stage + instance.serial_numbers = [] + instance.commit() + assert instance.result == [{"success": True, "changed": False}] + assert instance.result_current == {"success": True, "changed": False} + assert instance.response_current == { + "DATA": [{"key": "ALL", "value": response_msg}] + } + assert instance.response == [instance.response_current] + assert instance.response_data == [instance.response_current.get("DATA")] + + +def test_image_upgrade_stage_00074(monkeypatch, image_stage) -> None: + """ + Function + - commit + + Summary + Verify that commit() calls fail_json() on 500 response from the controller. + + Setup + - IssuDetailsBySerialNumber is mocked to return a successful response + - ImageStage is mocked to return a non-successful (500) response + + Test + - commit() will call fail_json() + + Description + commit() will call fail_json() on non-success response from the controller. + """ + key = "test_image_upgrade_stage_00074a" + + def mock_controller_version(*args) -> None: + instance.controller_version = "12.1.3b" + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + return responses_image_stage(key) + + monkeypatch.setattr( + PATCH_IMAGE_STAGE_POPULATE_CONTROLLER_VERSION, mock_controller_version + ) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": False}) + + instance = image_stage + instance.serial_numbers = ["FDO21120U5D"] + MATCH = "ImageStage.commit: failed" + with pytest.raises(AnsibleFailJson, match=MATCH): + instance.commit() + + +def test_image_upgrade_stage_00075(monkeypatch, image_stage) -> None: + """ + Function + - commit + + Summary + Verify that commit() sets self.diff to expected values on 200 response + from the controller. + + Setup + - IssuDetailsBySerialNumber is mocked to return a successful response + - ImageStage._populate_controller_version is mocked to 12.1.3b + - ImageStage.rest_send.commit is mocked to return a successful response + - ImageStage.rest_send.current_result is mocked to return a successful result + - ImageStage.validate_serial_numbers is tracked with MagicMock to ensure + it's called once + + Test + - commit() sets self.diff to the expected values + """ + key = "test_image_upgrade_stage_00075a" + + def mock_controller_version(*args) -> None: + instance.controller_version = "12.1.3b" + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: + return responses_image_stage(key) + + def mock_wait_for_image_stage_to_complete(*args) -> None: + instance.serial_numbers_done = {"FDO21120U5D"} + + monkeypatch.setattr( + PATCH_IMAGE_STAGE_POPULATE_CONTROLLER_VERSION, mock_controller_version + ) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) + monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) + monkeypatch.setattr( + PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True, "changed": True} + ) + validate_serial_numbers = MagicMock(name="validate_serial_numbers") + + instance = image_stage + instance.serial_numbers = ["FDO21120U5D"] + monkeypatch.setattr( + instance, + "_wait_for_image_stage_to_complete", + mock_wait_for_image_stage_to_complete, + ) + monkeypatch.setattr(instance, "validate_serial_numbers", validate_serial_numbers) + instance.commit() + assert validate_serial_numbers.assert_called_once + assert instance.serial_numbers_done == {"FDO21120U5D"} + assert instance.result_current == {"success": True, "changed": True} + assert instance.diff[0]["policy"] == "KR5M" + assert instance.diff[0]["ip_address"] == "172.22.150.102" + assert instance.diff[0]["serial_number"] == "FDO21120U5D" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 509fb9a7d..96105911b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -240,7 +240,9 @@ def test_image_upgrade_image_upgrade_common_00060(image_upgrade_common) -> None: """ instance = image_upgrade_common - data = responses_image_upgrade_common("test_image_upgrade_image_upgrade_common_00060a") + data = responses_image_upgrade_common( + "test_image_upgrade_image_upgrade_common_00060a" + ) with pytest.raises(AnsibleFailJson, match=r"Unknown request verb \(FOO\)"): instance._handle_response( # pylint: disable=protected-access data.get("response"), data.get("verb") From eb3bb39800e71e08473f5f510ef373653eee8361 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 08:13:05 -1000 Subject: [PATCH 257/300] Remove unused mocks --- .../test_image_upgrade_image_upgrade.py | 193 +----------------- 1 file changed, 10 insertions(+), 183 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 213605cc7..a9953ebfe 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -45,22 +45,22 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT = ( - PATCH_image_upgrade + "image_upgrade.RestSend.commit" + PATCH_IMAGE_UPGRADE + "image_upgrade.RestSend.commit" ) PATCH_IMAGE_UPGRADE_REST_SEND_RESPONSE_CURRENT = ( - PATCH_image_upgrade + "image_upgrade.RestSend.response_current" + PATCH_IMAGE_UPGRADE + "image_upgrade.RestSend.response_current" ) PATCH_IMAGE_UPGRADE_REST_SEND_RESULT_CURRENT = ( - PATCH_image_upgrade + "image_upgrade.RestSend.result_current" + PATCH_IMAGE_UPGRADE + "image_upgrade.RestSend.result_current" ) -REST_SEND_IMAGE_UPGRADE = PATCH_image_upgrade + "image_upgrade.RestSend" -DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_image_upgrade + "image_upgrade_common.dcnm_send" -DCNM_SEND_INSTALL_OPTIONS = PATCH_image_upgrade + "install_options.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" +REST_SEND_IMAGE_UPGRADE = PATCH_IMAGE_UPGRADE + "image_upgrade.RestSend" +DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_UPGRADE + "image_upgrade_common.dcnm_send" +DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_UPGRADE + "install_options.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_UPGRADE + "switch_issu_details.dcnm_send" def test_image_upgrade_upgrade_00001(image_upgrade) -> None: @@ -207,30 +207,16 @@ def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) - def mock_dcnm_send_image_upgrade_commit(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - monkeypatch.setattr( - DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_commit - ) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -451,8 +437,8 @@ def test_image_upgrade_upgrade_00021(monkeypatch, image_upgrade) -> None: to be upgraded - The methods called by commit are mocked to simulate that the device has not yet been upgraded to the desired version - - Methods called by commit that wait for current actions, and - image upgrade, to complete are mocked to do nothing + - Method called by commit, _wait_for_current_actions_to_complete + is mocked to do nothing - instance.devices is set to contain an invalid nxos.mode value Expected results: @@ -472,16 +458,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -489,11 +465,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -729,16 +700,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -746,11 +707,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -812,16 +768,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -829,11 +775,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -896,16 +837,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -913,11 +844,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -969,7 +895,6 @@ def test_image_upgrade_upgrade_00027(monkeypatch, image_upgrade) -> None: instance = image_upgrade key = "test_image_upgrade_upgrade_00027a" - image_upgrade_file = "image_upgrade_responses_ImageUpgrade" def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: return responses_image_install_options(key) @@ -980,19 +905,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -1000,11 +912,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -1065,16 +972,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -1082,11 +979,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -1148,16 +1040,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -1165,11 +1047,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -1231,16 +1108,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -1248,11 +1115,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -1319,16 +1181,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: - return responses_image_upgrade(key) - - monkeypatch.setattr( - PATCH_IMAGE_UPGRADE_REST_SEND_COMMIT, mock_rest_send_image_upgrade - ) - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( @@ -1336,11 +1188,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -1403,9 +1250,6 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: return responses_image_upgrade(key) @@ -1427,11 +1271,6 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { @@ -1489,30 +1328,18 @@ def test_image_upgrade_upgrade_00033(monkeypatch, image_upgrade) -> None: key = "test_image_upgrade_upgrade_00033a" - def mock_dcnm_send_install_options(*args, **kwargs) -> Dict[str, Any]: - return responses_image_install_options(key) - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: return responses_switch_issu_details(key) def mock_wait_for_current_actions_to_complete(*args, **kwargs): pass - def mock_wait_for_image_upgrade_to_complete(*args, **kwargs): - pass - - monkeypatch.setattr(DCNM_SEND_INSTALL_OPTIONS, mock_dcnm_send_install_options) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) monkeypatch.setattr( instance, "_wait_for_current_actions_to_complete", mock_wait_for_current_actions_to_complete, ) - monkeypatch.setattr( - instance, - "_wait_for_image_upgrade_to_complete", - mock_wait_for_image_upgrade_to_complete, - ) instance.devices = [ { From 903d86b5334ce1e84f22f3c4c4e45e071d10805e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 08:26:36 -1000 Subject: [PATCH 258/300] Remove unused mocks --- .../test_image_upgrade_image_upgrade_task.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index dfcd8324a..c585b43bd 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -50,11 +50,11 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_IMAGE_UPGRADE = PATCH_image_upgrade + "image_upgrade.dcnm_send" -DCNM_SEND_INSTALL_OPTIONS = PATCH_image_upgrade + "install_options.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" +DCNM_SEND_IMAGE_UPGRADE = PATCH_IMAGE_UPGRADE + "image_upgrade.dcnm_send" +DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_UPGRADE + "install_options.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_UPGRADE + "switch_issu_details.dcnm_send" @pytest.fixture(name="image_upgrade_task_bare") @@ -172,11 +172,6 @@ def test_image_upgrade_upgrade_task_00030(monkeypatch, image_upgrade_task_bare) """ key = "test_image_upgrade_upgrade_task_00030a" - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) instance = image_upgrade_task_bare(mock_ansible_module) @@ -243,11 +238,6 @@ def test_image_upgrade_upgrade_task_00031(monkeypatch, image_upgrade_task_bare) """ key = "test_image_upgrade_upgrade_task_00031a" - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - mock_ansible_module = MockAnsibleModule() mock_ansible_module.params = load_playbook_config(key) instance = image_upgrade_task_bare(mock_ansible_module) From b317424098e7a7b714894e91cc74faf633309afb Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 08:49:33 -1000 Subject: [PATCH 259/300] Remove unused mocks, more... Fix a couple pylint errors that were not showing up in sanity tests (redefined-builtin and invalid-name). --- .../test_image_upgrade_image_policy_action.py | 16 ----- .../test_image_upgrade_image_stage.py | 58 +++++++++---------- 2 files changed, 26 insertions(+), 48 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 0110bdc0c..4d4dbd3df 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -360,22 +360,10 @@ def test_image_upgrade_image_policy_action_00020( Since action == "FOO" is not covered in commit()'s if clauses, the else clause is taken and fail_json is called. """ - key = "test_image_upgrade_image_policy_action_00020a" - - def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: - return responses_image_policies(key) def mock_validate_request(*args) -> None: pass - monkeypatch.setattr( - DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details - ) - monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) - instance = image_policy_action monkeypatch.setattr(instance, "validate_request", mock_validate_request) monkeypatch.setattr(instance, "valid_actions", {"attach", "detach", "query", "FOO"}) @@ -415,9 +403,6 @@ def test_image_upgrade_image_policy_action_00021( def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) - def mock_dcnm_send_switch_details(*args) -> Dict[str, Any]: - return responses_switch_details(key) - def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: return responses_switch_issu_details(key) @@ -428,7 +413,6 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: monkeypatch.setattr( DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_common ) - monkeypatch.setattr(DCNM_SEND_SWITCH_DETAILS, mock_dcnm_send_switch_details) monkeypatch.setattr( DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 7f3f39f75..bb85fb508 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -48,11 +48,11 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." PATCH_COMMON = PATCH_MODULE_UTILS + "common." -PATCH_IMAGE_STAGE_REST_SEND_COMMIT = PATCH_image_upgrade + "image_stage.RestSend.commit" +PATCH_IMAGE_STAGE_REST_SEND_COMMIT = PATCH_IMAGE_UPGRADE + "image_stage.RestSend.commit" PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT = ( - PATCH_image_upgrade + "image_stage.RestSend.result_current" + PATCH_IMAGE_UPGRADE + "image_stage.RestSend.result_current" ) PATCH_IMAGE_STAGE_POPULATE_CONTROLLER_VERSION = ( "ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade." @@ -60,7 +60,7 @@ ) DCNM_SEND_CONTROLLER_VERSION = PATCH_COMMON + "controller_version.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_UPGRADE + "switch_issu_details.dcnm_send" def test_image_upgrade_stage_00001(image_stage) -> None: @@ -476,23 +476,23 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: @pytest.mark.parametrize( - "input, output, context", + "arg, value, context", [ (-1, None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), (10, 10, does_not_raise()), ("a", None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), ], ) -def test_image_upgrade_stage_00040(image_stage, input, output, context) -> None: +def test_image_upgrade_stage_00040(image_stage, arg, value, context) -> None: """ Function - check_interval Summary - Verify that check_interval input validation works as expected. + Verify that check_interval argument validation works as expected. Test - - Verify inputs to check_interval property + - Verify input arguments to check_interval property Description check_interval expects a positive integer value, or zero. @@ -500,32 +500,32 @@ def test_image_upgrade_stage_00040(image_stage, input, output, context) -> None: with does_not_raise(): instance = image_stage with context: - instance.check_interval = input - if output is not None: - assert instance.check_interval == output + instance.check_interval = arg + if value is not None: + assert instance.check_interval == value MATCH_00050 = "ImageStage.check_timeout: must be a positive integer or zero." @pytest.mark.parametrize( - "input, output, context", + "arg, value, context", [ (-1, None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), (10, 10, does_not_raise()), ("a", None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), ], ) -def test_image_upgrade_stage_00050(image_stage, input, output, context) -> None: +def test_image_upgrade_stage_00050(image_stage, arg, value, context) -> None: """ Function - check_interval Summary - Verify that check_timeout input validation works as expected. + Verify that check_timeout argument validation works as expected. Test - - Verify inputs to check_timeout property + - Verify input arguments to check_timeout property Description check_timeout expects a positive integer value, or zero. @@ -533,9 +533,9 @@ def test_image_upgrade_stage_00050(image_stage, input, output, context) -> None: with does_not_raise(): instance = image_stage with context: - instance.check_timeout = input - if output is not None: - assert instance.check_timeout == output + instance.check_timeout = arg + if value is not None: + assert instance.check_timeout == value MATCH_00060 = ( @@ -544,20 +544,20 @@ def test_image_upgrade_stage_00050(image_stage, input, output, context) -> None: @pytest.mark.parametrize( - "input, output, context", + "arg, value, context", [ ("foo", None, pytest.raises(AnsibleFailJson, match=MATCH_00060)), (10, None, pytest.raises(AnsibleFailJson, match=MATCH_00060)), (["DD001115F"], ["DD001115F"], does_not_raise()), ], ) -def test_image_upgrade_stage_00060(image_stage, input, output, context) -> None: +def test_image_upgrade_stage_00060(image_stage, arg, value, context) -> None: """ Function - serial_numbers Summary - Verify that serial_numbers input validation works as expected. + Verify that serial_numbers argument validation works as expected. Test - Verify inputs to serial_numbers property @@ -569,9 +569,9 @@ def test_image_upgrade_stage_00060(image_stage, input, output, context) -> None: with does_not_raise(): instance = image_stage with context: - instance.serial_numbers = input - if output is not None: - assert instance.serial_numbers == output + instance.serial_numbers = arg + if value is not None: + assert instance.serial_numbers == value MATCH_00070 = "ImageStage.commit: call instance.serial_numbers " @@ -740,12 +740,6 @@ def test_image_upgrade_stage_00073(monkeypatch, image_stage) -> None: When len(serial_numbers) == 0, commit() will set result and response properties, and return without doing anything else. """ - key = "test_image_upgrade_stage_00073a" - - def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) response_msg = "No files to stage." @@ -800,8 +794,8 @@ def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: instance = image_stage instance.serial_numbers = ["FDO21120U5D"] - MATCH = "ImageStage.commit: failed" - with pytest.raises(AnsibleFailJson, match=MATCH): + match = "ImageStage.commit: failed" + with pytest.raises(AnsibleFailJson, match=match): instance.commit() From 8ce2e6ae5046241eadfe56a41d3d9a231a77f9a6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 09:14:47 -1000 Subject: [PATCH 260/300] Add unit test for bad result due to 500 response --- ...image_upgrade_responses_ImagePolicies.json | 16 +++++++++ .../test_image_upgrade_image_policies.py | 35 +++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json index ab7391117..74cc1f79b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json @@ -177,6 +177,22 @@ "message": "" } }, + "test_image_upgrade_image_policies_00026a": { + "TEST_NOTES": [ + "RETURN_CODE 500", + "MESSAGE == NOK" + ], + "RETURN_CODE": 500, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "NOK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } + }, "test_image_upgrade_image_policy_action_00013a": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index 5609a054f..eb8d18ba6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -39,8 +39,8 @@ responses_image_policies) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_IMAGE_POLICIES = PATCH_image_upgrade + "image_policies.dcnm_send" +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_UPGRADE + "image_policies.dcnm_send" def test_image_upgrade_image_policies_00001(image_policies) -> None: @@ -201,7 +201,7 @@ def test_image_upgrade_image_policies_00023(monkeypatch, image_policies) -> None - refresh Test - - do not fail_json is called when DATA.lastOperDataObject length == 0 + - do not fail_json when DATA.lastOperDataObject length == 0 - 200 response Endpoint @@ -287,6 +287,35 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance.refresh() +def test_image_upgrade_image_policies_00026(monkeypatch, image_policies) -> None: + """ + Function + - refresh + + Summary + Verify that fail_json is called when _handle_response() returns a + non-successful result. + + Test + - fail_json is called when result["success"] is False. + + """ + key = "test_image_upgrade_image_policies_00026a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + + match = "ImagePolicies.refresh: Bad result when retrieving image policy " + match += r"information from the controller\." + + instance = image_policies + with pytest.raises(AnsibleFailJson, match=match): + instance.refresh() + + def test_image_upgrade_image_policies_00040(image_policies) -> None: """ Function From d932251d14bcba3ad5e12ef0a8d72c4ccdc2db60 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 09:27:53 -1000 Subject: [PATCH 261/300] Fix pylint invalid-name errors --- .../test_image_upgrade_image_install_options.py | 4 ++-- .../test_image_upgrade_image_policy_action.py | 10 +++++----- .../test_image_upgrade_image_validate.py | 10 +++++----- .../test_image_upgrade_switch_details.py | 6 +++--- ...image_upgrade_switch_issu_details_by_device_name.py | 4 ++-- ..._image_upgrade_switch_issu_details_by_ip_address.py | 4 ++-- ...age_upgrade_switch_issu_details_by_serial_number.py | 4 ++-- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index 0d7a88b9c..705ed5eeb 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -39,8 +39,8 @@ responses_image_install_options) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_INSTALL_OPTIONS = PATCH_image_upgrade + "install_options.dcnm_send" +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_INSTALL_OPTIONS = PATCH_IMAGE_UPGRADE + "install_options.dcnm_send" def test_image_upgrade_install_options_00001(image_install_options) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 4d4dbd3df..6953ab261 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -48,12 +48,12 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_IMAGE_POLICIES = PATCH_image_upgrade + "image_policies.dcnm_send" -DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_image_upgrade + "image_upgrade_common.dcnm_send" -DCNM_SEND_SWITCH_DETAILS = PATCH_image_upgrade + "switch_details.RestSend.commit" -DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" +DCNM_SEND_IMAGE_POLICIES = PATCH_IMAGE_UPGRADE + "image_policies.dcnm_send" +DCNM_SEND_IMAGE_UPGRADE_COMMON = PATCH_IMAGE_UPGRADE + "image_upgrade_common.dcnm_send" +DCNM_SEND_SWITCH_DETAILS = PATCH_IMAGE_UPGRADE + "switch_details.RestSend.commit" +DCNM_SEND_SWITCH_ISSU_DETAILS = PATCH_IMAGE_UPGRADE + "switch_issu_details.dcnm_send" def test_image_upgrade_image_policy_action_00001(image_policy_action) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index d5de5b9eb..705f50e2d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -44,14 +44,14 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_IMAGE_VALIDATE = PATCH_image_upgrade + "image_validate.dcnm_send" -DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_IMAGE_VALIDATE = PATCH_IMAGE_UPGRADE + "image_validate.dcnm_send" +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_UPGRADE + "switch_issu_details.dcnm_send" PATCH_IMAGE_VALIDATE_REST_SEND_COMMIT = ( - PATCH_image_upgrade + "image_validate.RestSend.commit" + PATCH_IMAGE_UPGRADE + "image_validate.RestSend.commit" ) PATCH_IMAGE_VALIDATE_REST_SEND_RESULT_CURRENT = ( - PATCH_image_upgrade + "image_validate.RestSend.result_current" + PATCH_IMAGE_UPGRADE + "image_validate.RestSend.result_current" ) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index e8558b417..127790ae3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -40,15 +40,15 @@ switch_details_fixture) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -PATCH_SWITCH_DETAILS = PATCH_image_upgrade + "switch_details." +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." +PATCH_SWITCH_DETAILS = PATCH_IMAGE_UPGRADE + "switch_details." PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT = ( PATCH_SWITCH_DETAILS + "RestSend.response_current" ) PATCH_SWITCH_DETAILS_REST_SEND_RESULT_CURRENT = ( PATCH_SWITCH_DETAILS + "RestSend.result_current" ) -REST_SEND_SWITCH_DETAILS = PATCH_image_upgrade + "switch_details.RestSend.commit" +REST_SEND_SWITCH_DETAILS = PATCH_IMAGE_UPGRADE + "switch_details.RestSend.commit" def test_image_upgrade_switch_details_00001(switch_details) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index a042ddff0..2c86fa4e1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -37,8 +37,8 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_UPGRADE + "switch_issu_details.dcnm_send" def test_image_upgrade_switch_issu_details_by_device_name_00001( diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 9fdf4d5c0..80fd65afb 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -37,8 +37,8 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_UPGRADE + "switch_issu_details.dcnm_send" def test_image_upgrade_switch_issu_details_by_ip_address_00001( diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index f95592dbe..846696301 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -38,8 +38,8 @@ responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." -PATCH_image_upgrade = PATCH_MODULE_UTILS + "image_upgrade." -DCNM_SEND_ISSU_DETAILS = PATCH_image_upgrade + "switch_issu_details.dcnm_send" +PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." +DCNM_SEND_ISSU_DETAILS = PATCH_IMAGE_UPGRADE + "switch_issu_details.dcnm_send" def test_image_upgrade_switch_issu_details_by_serial_number_00001( From 5fdb3bf626f6955d98eb61c9ff15771bf415bba8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 09:46:48 -1000 Subject: [PATCH 262/300] Remove unused fixture, more... controller_version_fixture was moved to tests/unit/module_utils/common many commits ago, and I forgot to remove the original. --- .../dcnm/dcnm_image_upgrade/image_upgrade_utils.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index c0efcb3b0..9d7f65e6c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -81,14 +81,6 @@ def public_method_for_pylint(self) -> Any: # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html -@pytest.fixture(name="controller_version") -def controller_version_fixture(): - """ - mock ControllerVersion - """ - return ControllerVersion(MockAnsibleModule) - - @pytest.fixture(name="image_install_options") def image_install_options_fixture(): """ From ad6bac1fcdb532f3807be821396c2e8c87eb71c0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 10:28:02 -1000 Subject: [PATCH 263/300] Fix test_log.py import of MockAnsibleModule --- tests/unit/module_utils/common/test_log.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/module_utils/common/test_log.py b/tests/unit/module_utils/common/test_log.py index cd22e21ef..f63115088 100644 --- a/tests/unit/module_utils/common/test_log.py +++ b/tests/unit/module_utils/common/test_log.py @@ -35,10 +35,8 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log -from ansible_collections.cisco.dcnm.plugins.module_utils.common.mock_ansible_module import \ - MockAnsibleModule from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - does_not_raise, log_fixture) + does_not_raise, log_fixture, MockAnsibleModule) def test_log_00010(tmp_path, log) -> None: From ae0ed594169dfec0856a0fcbce4529d121e579bf Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 10:34:56 -1000 Subject: [PATCH 264/300] Expand unit test execution to all files under tests/unit/. .github/workflows/main.yml is currently pointing to tests/unit/modules/dcnm/. Modified this to point to tests/unit/. instead so that unit tests in tests/unit/module_utils/common will be executed. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0d20cb71e..e26ffe9d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,7 +124,7 @@ jobs: run: ansible-galaxy collection install .cache/collection-tarballs/*.tar.gz - name: Run DCNM Unit tests - run: coverage run --source=. -m pytest tests/unit/modules/dcnm/. -vvvv + run: coverage run --source=. -m pytest tests/unit/. -vvvv working-directory: /home/runner/.ansible/collections/ansible_collections/cisco/dcnm env: PYTHONPATH: /home/runner/.ansible/collections From ff3efe3b0cc1bd00a853070fe17449598dbd91d2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 13:55:25 -1000 Subject: [PATCH 265/300] ImageStage.check_interval unit test: parameter for bool case This brings us to 99% coverage for image_stage.py --- .../dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index bb85fb508..f40c02d43 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -478,6 +478,7 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: @pytest.mark.parametrize( "arg, value, context", [ + (True, None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), (-1, None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), (10, 10, does_not_raise()), ("a", None, pytest.raises(AnsibleFailJson, match=MATCH_00040)), From 40b3351742003e6378316f534064560bd8d5fd0d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 21 Feb 2024 14:46:04 -1000 Subject: [PATCH 266/300] ImageStage.check_timeout unit test: parameter for bool case --- .../dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index f40c02d43..31a1a485e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -512,6 +512,7 @@ def test_image_upgrade_stage_00040(image_stage, arg, value, context) -> None: @pytest.mark.parametrize( "arg, value, context", [ + (True, None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), (-1, None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), (10, 10, does_not_raise()), ("a", None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), From 8481d1d68a75aecff3b02d6f83cd348553545391 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 22 Feb 2024 16:50:57 -1000 Subject: [PATCH 267/300] Fix comment typos --- .../test_image_upgrade_image_upgrade.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index a9953ebfe..1280961a9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -254,10 +254,10 @@ def test_image_upgrade_upgrade_00019(monkeypatch, image_upgrade, caplog) -> None - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded. - commit -> _build_payload -> issu_details is mocked to simulate - that the the image has already been staged and validated and the + that the image has already been staged and validated and the device has already been upgraded to the desired version. - commit -> _build_payload -> install_options is mocked to simulate - that the the image EPLD does not need upgrade. + that the EPLD image does not need upgrade. - The following methods, called by commit() are mocked to do nothing: - _wait_for_current_actions_to_complete - _wait_for_image_upgrade_to_complete @@ -347,10 +347,10 @@ def test_image_upgrade_upgrade_00020(monkeypatch, image_upgrade, caplog) -> None - ImageUpgrade.devices is set to a list of one dict for a device to be upgraded - commit -> _build_payload -> issu_details is mocked to simulate - that the the image has already been staged and validated and the + that the image has already been staged and validated and the device has already been upgraded to the desired version. - commit -> _build_payload -> install_options is mocked to simulate - that the the image EPLD does not need upgrade. + that the image EPLD does not need upgrade. - The following methods, called by commit() are mocked to do nothing: - _wait_for_current_actions_to_complete - _wait_for_image_upgrade_to_complete From 8d8eb49c8e40ad8a5638a3b4b00f675c2272f8a5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 23 Feb 2024 08:31:08 -1000 Subject: [PATCH 268/300] ImageUpgradeCommon: Add unit tests, more... ImageUpgradeCommon: Add unit tests for dcnm_send_with_retry. To facilitate the above, we've modified ImageUpgradeCommon.__init__() to add self.dcnm_send. This required also modifying the patch for ImageUpgradeCommon.dcnm_send in unit test test_image_upgrade_image_policy_action_00021 . Also: - Added Summary to a couple test cases and other docstring improvements. - Updated test_image_upgrade_image_upgrade_common_00001 with more asserts - Removed old commented logging unit test code that is no longer relevant. --- .../image_upgrade/image_upgrade_common.py | 11 +- .../test_image_upgrade_image_policy_action.py | 4 +- ...test_image_upgrade_image_upgrade_common.py | 163 ++++++++---------- 3 files changed, 80 insertions(+), 98 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_upgrade_common.py b/plugins/module_utils/image_upgrade/image_upgrade_common.py index ea10907f8..00534db3c 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_common.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_common.py @@ -66,6 +66,8 @@ def __init__(self, module): self.properties["timeout"] = 300 self.properties["unit_test"] = False + self.dcnm_send = dcnm_send + def dcnm_send_with_retry(self, verb: str, path: str, payload=None): """ Call dcnm_send() with retries until successful response or timeout is exceeded. @@ -90,21 +92,24 @@ def dcnm_send_with_retry(self, verb: str, path: str, payload=None): timeout = 300 success = False - msg = f"{caller}: Entering dcnm_send_with_retry loop. timeout {timeout}, send_interval {self.send_interval}, verb {verb}, path {path}" + msg = f"{caller}: Entering dcnm_send_with_retry loop. " + msg += f"timeout {timeout}, send_interval {self.send_interval}, " + msg += f"verb {verb}, path {path}" self.log.debug(msg) + # self.dcnm_send = dcnm_send while timeout > 0 and success is False: if payload is None: msg = f"{caller}: Calling dcnm_send: verb {verb}, path {path}" self.log.debug(msg) - response = dcnm_send(self.module, verb, path) + response = self.dcnm_send(self.module, verb, path) else: msg = ( f"{caller}: Calling dcnm_send: verb {verb}, path {path}, payload: " ) msg += f"{json.dumps(payload, indent=4, sort_keys=True)}" self.log.debug(msg) - response = dcnm_send(self.module, verb, path, data=json.dumps(payload)) + response = self.dcnm_send(self.module, verb, path, data=json.dumps(payload)) self.response_current = copy.deepcopy(response) self.result_current = self._handle_response(response, verb) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 6953ab261..0d6393251 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -410,14 +410,12 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: return responses_image_policy_action(key) monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) - monkeypatch.setattr( - DCNM_SEND_IMAGE_UPGRADE_COMMON, mock_dcnm_send_image_upgrade_common - ) monkeypatch.setattr( DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details ) instance = image_policy_action + monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send_image_upgrade_common) instance.policy_name = "KR5M" instance.serial_numbers = ["FDO2112189M"] instance.action = "detach" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 96105911b..7f13873c1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -46,18 +46,29 @@ def test_image_upgrade_image_upgrade_common_00001(image_upgrade_common) -> None: Function - __init__ + Summary + Verify that instance.params accepts well-formed input and that the + params getter returns the expected value. + Test - fail_json is not called - - image_upgrade_common.params is a dict - - image_upgrade_common.debug is False - - image_upgrade_common.fd is None - - image_upgrade_common.logfile is /tmp/ansible_dcnm.log + - image_upgrade_common.params is set to the expected value + - All other instance properties are initialized to expected values """ test_params = {"config": {"switches": [{"ip_address": "172.22.150.105"}]}} with does_not_raise(): instance = image_upgrade_common assert instance.params == test_params + assert instance.changed is False + assert instance.response == [] + assert instance.response_current == {} + assert instance.response_data == [] + assert instance.result == [] + assert instance.result_current == {} + assert instance.send_interval == 5 + assert instance.timeout == 300 + assert instance.unit_test is False @pytest.mark.parametrize( @@ -317,9 +328,15 @@ def test_image_upgrade_image_upgrade_common_00080( Function - _handle_post_put_delete_response + Summary + Verify that expected values are returned for POST requests. + Test - - return expected values for POST requests - fail_json is not called + - return expected values for POST requests, when: + - 00080a. MESSAGE == "OK" + - 00080b. MESSAGE != "OK" + - 00080c. MESSAGE field is missing """ instance = image_upgrade_common @@ -416,90 +433,52 @@ def test_image_upgrade_image_upgrade_common_00110(image_upgrade_common) -> None: assert instance.log.info(message) is None -# def test_image_upgrade_image_upgrade_common_00111(tmp_path, image_upgrade_common) -> None: -# """ -# Function -# - log.log_msg - -# Test -# - log_msg writes to the log.logfile when log.config is set -# """ -# instance = image_upgrade_common - -# directory = tmp_path / "test_log_msg" -# directory.mkdir() -# filename = directory / "test_log_msg.txt" - -# log = Log(instance.module) -# config = { -# "version": 1, -# "formatters": { -# "standard": { -# "class": "logging.Formatter", -# "format": "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s" -# } -# }, -# "handlers": { -# "file": { -# "class": "logging.handlers.RotatingFileHandler", -# "formatter": "standard", -# "level": "DEBUG", -# "filename": "foo", -# "mode": "a", -# "encoding": "utf-8", -# "maxBytes": 500000, -# "backupCount": 4 -# } -# }, -# "loggers": { -# "dcnm": { -# "handlers": [ -# "file" -# ], -# "level": "DEBUG", -# "propagate": False -# } -# }, -# "root": { -# "level": "INFO", -# "handlers": [ -# "file" -# ] -# } -# } - -# config["handlers"]["file"]["filename"] = filename -# log.config = config -# message = "This is a message" -# # instance.log.debug = True -# # instance.log.logfile = filename -# # instance.log.log_msg(message) -# instance.log.debug(message) - -# assert filename.read_text(encoding="UTF-8") == message + "\n" -# assert len(list(tmp_path.iterdir())) == 1 - - -# def test_image_upgrade_image_upgrade_common_00112(tmp_path, image_upgrade_common) -> None: -# """ -# Function -# - log.log_msg - -# Test -# - log.log_msg calls fail_json if the logfile cannot be opened - -# Description -# To ensure an error is generated, we attempt a write to a filename -# that is too long for the target OS. -# """ -# instance = image_upgrade_common - -# directory = tmp_path / "test_log_msg" -# directory.mkdir() -# filename = directory / f"test_{'a' * 2000}_log_msg.txt" - -# error_message = "This is an error message" -# instance.log.debug = True -# instance.log.logfile = filename -# with pytest.raises(AnsibleFailJson, match="error writing to logfile"): -# instance.log.log_msg(error_message) +def test_image_upgrade_image_upgrade_common_00120( + monkeypatch, image_upgrade_common +) -> None: + """ + Function + - dcnm_send_with_retry + + Summary + Verify that result and response are set to the expected values when + payload is None and the response is successful. + + """ + + def mock_dcnm_send(*args, **kwargs): + return {"MESSAGE": "OK", "RETURN_CODE": 200} + + instance = image_upgrade_common + instance.timeout = 1 + monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send) + + instance.dcnm_send_with_retry("PUT", "https://foo.bar.com/endpoint", None) + assert instance.response_current == {"MESSAGE": "OK", "RETURN_CODE": 200} + assert instance.result == [{"changed": True, "success": True}] + + +def test_image_upgrade_image_upgrade_common_00121( + monkeypatch, image_upgrade_common +) -> None: + """ + Function + - dcnm_send_with_retry + + Summary + Verify that result and response are set to the expected values when + payload is set and the response is successful. + + """ + + def mock_dcnm_send(*args, **kwargs): + return {"MESSAGE": "OK", "RETURN_CODE": 200} + + with does_not_raise(): + instance = image_upgrade_common + monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send) + instance.dcnm_send_with_retry( + "PUT", "https://foo.bar.com/endpoint", {"foo": "bar"} + ) + assert instance.response_current == {"MESSAGE": "OK", "RETURN_CODE": 200} + assert instance.result == [{"changed": True, "success": True}] From 4c8e743a3c41a23208e9a42070722b81d652f312 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 23 Feb 2024 14:38:34 -1000 Subject: [PATCH 269/300] Update version_added to "3.5.0" --- plugins/modules/dcnm_image_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index b5732b8c8..b71d47844 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -23,7 +23,7 @@ --- module: dcnm_image_upgrade short_description: Image management for Nexus switches -version_added: "0.9.0" +version_added: "3.5.0" description: - Stage, validate, upgrade images. - Attach, detach, image policies. From 17b81bd0b547abb30d92743a82985477a2ab9c9a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 24 Feb 2024 20:52:50 -1000 Subject: [PATCH 270/300] image_upgrade.py: 100% unit test coverage test_image_upgrade_image_upgrade.py: Rewrote some unit tests to simplify logic and provide greater coverage. test_image_upgrade_image_upgrade.py: Renumbered many unit tests. image_upgrade.py: Move issu_detail.refresh() out of tight for loops in: - ImageUpgrade()._wait_for_image_upgrade_to_complete - ImageUpgrade()_wait_for_current_actions_to_complete - ImageUpgrade().check_interval: Add check for bool due to isinstance() treating bool as int. - ImageUpgrade().check_timeout: Add check for bool due to isinstance() treating bool as int. Ran all module_utils/image_upgrade files through black/isort --- .../image_upgrade/image_upgrade.py | 37 +- .../image_upgrade/image_upgrade_common.py | 4 +- .../image_upgrade/switch_issu_details.py | 5 +- ...e_upgrade_responses_SwitchIssuDetails.json | 196 ++++- .../test_image_upgrade_image_upgrade.py | 782 +++++++++++++----- 5 files changed, 807 insertions(+), 217 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_upgrade.py b/plugins/module_utils/image_upgrade/image_upgrade.py index 3c6bfac90..4eae955a8 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade.py +++ b/plugins/module_utils/image_upgrade/image_upgrade.py @@ -490,7 +490,9 @@ def commit(self) -> None: self.result = self.rest_send.result_current self.result_current = self.rest_send.result_current - msg = f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" + msg = ( + f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" + ) self.log.debug(msg) msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" @@ -524,21 +526,21 @@ def _wait_for_current_actions_to_complete(self): """ method_name = inspect.stack()[0][3] - self.ipv4_done = set() + if self.unit_test is False: + # See unit test test_image_upgrade_upgrade_00205 + self.ipv4_done = set() self.ipv4_todo = set(copy.copy(self.ip_addresses)) timeout = self.check_timeout while self.ipv4_done != self.ipv4_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + self.issu_detail.refresh() for ipv4 in self.ip_addresses: if ipv4 in self.ipv4_done: continue - self.issu_detail.filter = ipv4 - self.log.debug("Calling issu_detail.refresh()") - self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: self.ipv4_done.add(ipv4) @@ -562,20 +564,21 @@ def _wait_for_image_upgrade_to_complete(self): method_name = inspect.stack()[0][3] self.ipv4_todo = set(copy.copy(self.ip_addresses)) - self.ipv4_done = set() + if self.unit_test is False: + # See unit test test_image_upgrade_upgrade_00240 + self.ipv4_done = set() timeout = self.check_timeout while self.ipv4_done != self.ipv4_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + self.issu_detail.refresh() for ipv4 in self.ip_addresses: if ipv4 in self.ipv4_done: continue - self.issu_detail.filter = ipv4 - self.log.debug("Calling issu_detail.refresh()") - self.issu_detail.refresh() + ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name upgrade_percent = self.issu_detail.upgrade_percent @@ -888,9 +891,13 @@ def check_interval(self): @check_interval.setter def check_interval(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.{method_name} must be an integer." + # isinstance(False, int) returns True, so we need first + # to test for this and fail_json specifically for bool values. + if isinstance(value, bool): + self.module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg = "instance.check_interval must be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @@ -904,8 +911,12 @@ def check_timeout(self): @check_timeout.setter def check_timeout(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.{method_name} must be an integer." + # isinstance(False, int) returns True, so we need first + # to test for this and fail_json specifically for bool values. + if isinstance(value, bool): + self.module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg = "instance.check_timeout must be an integer." self.module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_upgrade/image_upgrade_common.py b/plugins/module_utils/image_upgrade/image_upgrade_common.py index 00534db3c..fd79b4319 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_common.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_common.py @@ -109,7 +109,9 @@ def dcnm_send_with_retry(self, verb: str, path: str, payload=None): ) msg += f"{json.dumps(payload, indent=4, sort_keys=True)}" self.log.debug(msg) - response = self.dcnm_send(self.module, verb, path, data=json.dumps(payload)) + response = self.dcnm_send( + self.module, verb, path, data=json.dumps(payload) + ) self.response_current = copy.deepcopy(response) self.result_current = self._handle_response(response, verb) diff --git a/plugins/module_utils/image_upgrade/switch_issu_details.py b/plugins/module_utils/image_upgrade/switch_issu_details.py index 2e3a80636..612e8707d 100644 --- a/plugins/module_utils/image_upgrade/switch_issu_details.py +++ b/plugins/module_utils/image_upgrade/switch_issu_details.py @@ -129,7 +129,10 @@ def refresh_super(self) -> None: msg = f"self.result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) - if self.result_current["success"] is False or self.result_current["found"] is False: + if ( + self.result_current["success"] is False + or self.result_current["found"] is False + ): msg = f"{self.class_name}.{method_name}: " msg += "Bad result when retriving switch " msg += "information from the controller" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 91a72c0a0..104b07afb 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -3610,7 +3610,7 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", "RETURN_CODE": 200 }, - "test_image_upgrade_upgrade_00080a": { + "test_image_upgrade_upgrade_00200a": { "TEST_NOTES": [ "172.22.150.102 validated, upgrade, imageStaged: Success", "172.22.150.108 validated, upgrade, imageStaged: Success" @@ -3704,7 +3704,101 @@ "message": "" } }, - "test_image_upgrade_upgrade_00081a": { + "test_image_upgrade_upgrade_00205a": { + "TEST_NOTES": [ + "172.22.150.102 validated, upgrade, imageStaged: Success", + "172.22.150.108 validated, upgrade, imageStaged: Success" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_upgrade_upgrade_00210a": { "TEST_NOTES": [ "172.22.150.102 upgrade: Success", "172.22.150.108 upgrade: In-Progress", @@ -3799,7 +3893,7 @@ "message": "" } }, - "test_image_upgrade_upgrade_00090a": { + "test_image_upgrade_upgrade_00220a": { "TEST_NOTES": [ "172.22.150.102 upgrade: Success", "172.22.150.108 upgrade: Failed", @@ -3894,7 +3988,7 @@ "message": "" } }, - "test_image_upgrade_upgrade_00091a": { + "test_image_upgrade_upgrade_00230a": { "TEST_NOTES": [ "172.22.150.102 upgrade: Success", "172.22.150.108 upgrade: In-Progress", @@ -3989,6 +4083,100 @@ "message": "" } }, + "test_image_upgrade_upgrade_00240a": { + "TEST_NOTES": [ + "172.22.150.102 upgrade: Success", + "172.22.150.108 upgrade: Success" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + }, + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, "test_image_upgrade_upgrade_task_00020a": { "TEST_NOTES": [ "RETURN_CODE 200", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 1280961a9..3dbe2c1da 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -1365,24 +1365,8 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): # test getter properties - - -def test_image_upgrade_upgrade_00043(image_upgrade) -> None: - """ - Function - - check_interval - """ - instance = image_upgrade - assert instance.check_interval == 10 - - -def test_image_upgrade_upgrade_00044(image_upgrade) -> None: - """ - Function - - check_timeout - """ - instance = image_upgrade - assert instance.check_timeout == 1800 +# check_interval (see test_image_upgrade_upgrade_00070) +# check_timeout (see test_image_upgrade_upgrade_00075) def test_image_upgrade_upgrade_00045(monkeypatch, image_upgrade) -> None: @@ -1404,7 +1388,8 @@ def test_image_upgrade_upgrade_00045(monkeypatch, image_upgrade) -> None: 1. instance.response_data == 121 """ - instance = image_upgrade + with does_not_raise(): + instance = image_upgrade key = "test_image_upgrade_upgrade_00045a" @@ -1487,7 +1472,8 @@ def test_image_upgrade_upgrade_00046(monkeypatch, image_upgrade) -> None: 1. instance.result is a list: [{'success': True, 'changed': True}] """ - instance = image_upgrade + with does_not_raise(): + instance = image_upgrade key = "test_image_upgrade_upgrade_00046a" @@ -1568,7 +1554,8 @@ def test_image_upgrade_upgrade_00047(monkeypatch, image_upgrade) -> None: 1. instance.response is a list """ - instance = image_upgrade + with does_not_raise(): + instance = image_upgrade key = "test_image_upgrade_upgrade_00047a" @@ -1634,90 +1621,172 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: assert instance.response[0]["DATA"] == 121 -# setters +# test setter properties MATCH_00060 = "ImageUpgrade.bios_force: instance.bios_force must be a boolean." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060), True), ], ) -def test_image_upgrade_upgrade_00060(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00060( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - bios_force setter + + Verify that bios_force does not call fail_json if passed a boolean. + Verify that bios_force does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. """ - instance = image_upgrade + with does_not_raise(): + instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00060): - instance.bios_force = value - else: + with expected: instance.bios_force = value - assert instance.bios_force == expected + if raise_flag is False: + assert instance.bios_force == value + else: + assert instance.bios_force is False -MATCH_00061 = "ImageUpgrade.config_reload: " -MATCH_00061 += "instance.config_reload must be a boolean." +MATCH_00070 = r"ImageUpgrade\.check_interval: instance\.check_interval " +MATCH_00070 += r"must be an integer\." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00061)), + (1, does_not_raise(), False), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00070), True), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00070), True), ], ) -def test_image_upgrade_upgrade_00061(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00070( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - - config_reload setter + - check_interval setter + + Summary + Verify that check_interval does not call fail_json if the value is an integer + and does call fail_json if the value is not an integer. Verify that the + default value is set if fail_json is called. """ - instance = image_upgrade + with does_not_raise(): + instance = image_upgrade + with expected: + instance.check_interval = value + if raise_flag is False: + assert instance.check_interval == value + else: + assert instance.check_interval == 10 + + +MATCH_00075 = r"ImageUpgrade\.check_timeout: instance\.check_timeout " +MATCH_00075 += r"must be an integer\." + - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00061): - instance.config_reload = value +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + (1, does_not_raise(), False), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00075), True), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00075), True), + ], +) +def test_image_upgrade_upgrade_00075( + image_upgrade, value, expected, raise_flag +) -> None: + """ + Function + - check_timeout setter + + Summary + Verify that check_timeout does not call fail_json if the value is an integer + and does call fail_json if the value is not an integer. Verify that the + default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade + with expected: + instance.check_timeout = value + if raise_flag is False: + assert instance.check_timeout == value else: + assert instance.check_timeout == 1800 + + +MATCH_00080 = r"ImageUpgrade\.config_reload: " +MATCH_00080 += r"instance\.config_reload must be a boolean\." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00080), True), + ], +) +def test_image_upgrade_upgrade_00080( + image_upgrade, value, expected, raise_flag +) -> None: + """ + Function + - config_reload setter + """ + with does_not_raise(): + instance = image_upgrade + + with expected: instance.config_reload = value - assert instance.config_reload == expected + if raise_flag is False: + assert instance.config_reload == value + else: + assert instance.config_reload is False -MATCH_00062_COMMON = "ImageUpgrade.devices: " -MATCH_00062_COMMON += "instance.devices must be a python list of dict" +MATCH_00090_COMMON = "ImageUpgrade.devices: " +MATCH_00090_COMMON += "instance.devices must be a python list of dict" -MATCH_00062_FAIL_1 = f"{MATCH_00062_COMMON}. Got not a list." -MATCH_00062_FAIL_2 = rf"{MATCH_00062_COMMON}. Got \['not a dict'\]." +MATCH_00090_FAIL_1 = f"{MATCH_00090_COMMON}. Got not a list." +MATCH_00090_FAIL_2 = rf"{MATCH_00090_COMMON}. Got \['not a dict'\]." -MATCH_00062_FAIL_3 = f"{MATCH_00062_COMMON}, where each dict contains " -MATCH_00062_FAIL_3 += "the following keys: ip_address. " -MATCH_00062_FAIL_3 += r"Got \[\{'bad_key_ip_address': '192.168.1.1'\}\]." +MATCH_00090_FAIL_3 = f"{MATCH_00090_COMMON}, where each dict contains " +MATCH_00090_FAIL_3 += "the following keys: ip_address. " +MATCH_00090_FAIL_3 += r"Got \[\{'bad_key_ip_address': '192.168.1.1'\}\]." -DATA_00062_PASS = [{"ip_address": "192.168.1.1"}] -DATA_00062_FAIL_1 = "not a list" -DATA_00062_FAIL_2 = ["not a dict"] -DATA_00062_FAIL_3 = [{"bad_key_ip_address": "192.168.1.1"}] +DATA_00090_PASS = [{"ip_address": "192.168.1.1"}] +DATA_00090_FAIL_1 = "not a list" +DATA_00090_FAIL_2 = ["not a dict"] +DATA_00090_FAIL_3 = [{"bad_key_ip_address": "192.168.1.1"}] @pytest.mark.parametrize( "value, expected", [ - (DATA_00062_PASS, does_not_raise()), - (DATA_00062_FAIL_1, pytest.raises(AnsibleFailJson, match=MATCH_00062_FAIL_1)), - (DATA_00062_FAIL_2, pytest.raises(AnsibleFailJson, match=MATCH_00062_FAIL_2)), - (DATA_00062_FAIL_3, pytest.raises(AnsibleFailJson, match=MATCH_00062_FAIL_3)), + (DATA_00090_PASS, does_not_raise()), + (DATA_00090_FAIL_1, pytest.raises(AnsibleFailJson, match=MATCH_00090_FAIL_1)), + (DATA_00090_FAIL_2, pytest.raises(AnsibleFailJson, match=MATCH_00090_FAIL_2)), + (DATA_00090_FAIL_3, pytest.raises(AnsibleFailJson, match=MATCH_00090_FAIL_3)), ], ) -def test_image_upgrade_upgrade_00062(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00090(image_upgrade, value, expected) -> None: """ Function - devices setter + + Summary + Verify that devices does not call fail_json if passed a list of dicts + and does call fail_json if passed a non-list or a list of non-dicts. """ instance = image_upgrade @@ -1725,274 +1794,353 @@ def test_image_upgrade_upgrade_00062(image_upgrade, value, expected) -> None: instance.devices = value -MATCH_00063 = "ImageUpgrade.disruptive: " -MATCH_00063 += "instance.disruptive must be a boolean." +MATCH_00100 = "ImageUpgrade.disruptive: " +MATCH_00100 += "instance.disruptive must be a boolean." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00063)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00100), True), ], ) -def test_image_upgrade_upgrade_00063(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00100( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - disruptive setter + + Summary + Verify that disruptive does not call fail_json if passed a boolean. + Verify that disruptive does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. """ instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00063): - instance.disruptive = value - else: + with expected: instance.disruptive = value - assert instance.disruptive == expected + if raise_flag is False: + assert instance.disruptive == value + else: + assert instance.disruptive is True -MATCH_00064 = "ImageUpgrade.epld_golden: " -MATCH_00064 += "instance.epld_golden must be a boolean." +MATCH_00110 = "ImageUpgrade.epld_golden: " +MATCH_00110 += "instance.epld_golden must be a boolean." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00064)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00110), True), ], ) -def test_image_upgrade_upgrade_00064(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00110( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - epld_golden setter + + Summary + Verify that epld_golden does not call fail_json if passed a boolean. + Verify that epld_golden does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. """ instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00064): - instance.epld_golden = value - else: + with expected: instance.epld_golden = value - assert instance.epld_golden == expected + if raise_flag is False: + assert instance.epld_golden == value + else: + assert instance.epld_golden is False -MATCH_00065 = "ImageUpgrade.epld_upgrade: " -MATCH_00065 += "instance.epld_upgrade must be a boolean." +MATCH_00120 = "ImageUpgrade.epld_upgrade: " +MATCH_00120 += "instance.epld_upgrade must be a boolean." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00065)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00120), True), ], ) -def test_image_upgrade_upgrade_00065(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00120( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - epld_upgrade setter + + Summary + Verify that epld_upgrade does not call fail_json if passed a boolean. + Verify that epld_upgrade does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. """ instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00065): - instance.epld_upgrade = value - else: + with expected: instance.epld_upgrade = value - assert instance.epld_upgrade == expected + if raise_flag is False: + assert instance.epld_upgrade == value + else: + assert instance.epld_upgrade is False -MATCH_00066_FAIL_1 = "ImageUpgrade.epld_module: " -MATCH_00066_FAIL_1 += "instance.epld_module must be an integer or 'ALL'" +MATCH_00130 = "ImageUpgrade.epld_module: " +MATCH_00130 += "instance.epld_module must be an integer or 'ALL'" @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - ("ALL", does_not_raise()), - (1, does_not_raise()), - (27, does_not_raise()), - ("27", does_not_raise()), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00066_FAIL_1)), + ("ALL", does_not_raise(), False), + (1, does_not_raise(), False), + (27, does_not_raise(), False), + ("27", does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00130), True), ], ) -def test_image_upgrade_upgrade_00066(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00130( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - epld_module setter + + Summary + Verify that epld_module does not call fail_json if passed a valid value. + Verify that epld_module does call fail_json if passed an invalid value. + Verify that the default value is set if fail_json is called. + Verify that valid string values are converted to int() """ - instance = image_upgrade + with does_not_raise(): + instance = image_upgrade with expected: instance.epld_module = value + if raise_flag is False: + if value == "ALL": + assert instance.epld_module == value + else: + assert instance.epld_module == int(value) + else: + assert instance.epld_module == "ALL" -MATCH_00067 = "ImageUpgrade.force_non_disruptive: " -MATCH_00067 += "instance.force_non_disruptive must be a boolean." +MATCH_00140 = r"ImageUpgrade\.force_non_disruptive: " +MATCH_00140 += r"instance\.force_non_disruptive must be a boolean\." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00067)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00140), True), ], ) -def test_image_upgrade_upgrade_00067(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00140( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - force_non_disruptive setter + + Summary + Verify that force_non_disruptive does not call fail_json if passed a boolean. + Verify that force_non_disruptive does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. """ instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00067): - instance.force_non_disruptive = value - else: + with expected: instance.force_non_disruptive = value - assert instance.force_non_disruptive == expected + if raise_flag is False: + assert instance.force_non_disruptive == value + else: + assert instance.force_non_disruptive is False -MATCH_00068 = "ImageUpgrade.non_disruptive: " -MATCH_00068 += "instance.non_disruptive must be a boolean." +MATCH_00150 = r"ImageUpgrade\.non_disruptive: " +MATCH_00150 += r"instance\.non_disruptive must be a boolean\." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00068)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00150), True), ], ) -def test_image_upgrade_upgrade_00068(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00150( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - non_disruptive setter - """ - instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00068): - instance.non_disruptive = value - else: + Summary + Verify that non_disruptive does not call fail_json if passed a boolean. + Verify that non_disruptive does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade + with expected: instance.non_disruptive = value - assert instance.non_disruptive == expected + if raise_flag is False: + assert instance.non_disruptive == value + else: + assert instance.non_disruptive is False -MATCH_00069 = "ImageUpgrade.package_install: " -MATCH_00069 += "instance.package_install must be a boolean." +MATCH_00160 = r"ImageUpgrade\.package_install: " +MATCH_00160 += r"instance\.package_install must be a boolean\." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00069)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00160), True), ], ) -def test_image_upgrade_upgrade_00069(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00160( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - package_install setter - """ - instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00069): - instance.package_install = value - else: + Summary + Verify that package_install does not call fail_json if passed a boolean. + Verify that package_install does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade + with expected: instance.package_install = value - assert instance.package_install == expected + if raise_flag is False: + assert instance.package_install == value + else: + assert instance.package_install is False -MATCH_00070 = "ImageUpgrade.package_uninstall: " -MATCH_00070 += "instance.package_uninstall must be a boolean." +MATCH_00170 = "ImageUpgrade.package_uninstall: " +MATCH_00170 += "instance.package_uninstall must be a boolean." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00070)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00170), True), ], ) -def test_image_upgrade_upgrade_00070(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00170( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - package_uninstall setter - """ - instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00070): - instance.package_uninstall = value - else: + Summary + Verify that package_uninstall does not call fail_json if passed a boolean. + Verify that package_uninstall does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade + with expected: instance.package_uninstall = value - assert instance.package_uninstall == expected + if raise_flag is False: + assert instance.package_uninstall == value + else: + assert instance.package_uninstall is False -MATCH_00071 = "ImageUpgrade.reboot: " -MATCH_00071 += "instance.reboot must be a boolean." +MATCH_00180 = r"ImageUpgrade\.reboot: " +MATCH_00180 += r"instance\.reboot must be a boolean\." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00071)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00180), True), ], ) -def test_image_upgrade_upgrade_00071(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00180( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - reboot setter - """ - instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00071): - instance.reboot = value - else: + Summary + Verify that reboot does not call fail_json if passed a boolean. + Verify that reboot does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade + with expected: instance.reboot = value - assert instance.reboot == expected + if raise_flag is False: + assert instance.reboot == value + else: + assert instance.reboot is False -MATCH_00072 = "ImageUpgrade.write_erase: " -MATCH_00072 += "instance.write_erase must be a boolean." +MATCH_00190 = "ImageUpgrade.write_erase: " +MATCH_00190 += "instance.write_erase must be a boolean." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (True, True), - (False, False), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00072)), + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00190), True), ], ) -def test_image_upgrade_upgrade_00072(image_upgrade, value, expected) -> None: +def test_image_upgrade_upgrade_00190( + image_upgrade, value, expected, raise_flag +) -> None: """ Function - write_erase setter - """ - instance = image_upgrade - if value == "FOO": - with pytest.raises(AnsibleFailJson, match=MATCH_00072): - instance.write_erase = value - else: + Summary + Verify that write_erase does not call fail_json if passed a boolean. + Verify that write_erase does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade + with expected: instance.write_erase = value - assert instance.write_erase == expected + if raise_flag is False: + assert instance.write_erase == value + else: + assert instance.write_erase is False -def test_image_upgrade_upgrade_00080( +def test_image_upgrade_upgrade_00200( monkeypatch, image_upgrade, issu_details_by_ip_address ) -> None: """ @@ -2021,7 +2169,7 @@ def test_image_upgrade_upgrade_00080( instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_upgrade_upgrade_00080a" + key = "test_image_upgrade_upgrade_00200a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2040,7 +2188,69 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" in instance.ipv4_done -def test_image_upgrade_upgrade_00081( +def test_image_upgrade_upgrade_00205( + monkeypatch, image_upgrade, issu_details_by_ip_address +) -> None: + """ + Function + - _wait_for_current_actions_to_complete + + Summary + - Verify that ipv4_done contains two ip addresses since + issu_detail is mocked to indicate that no actions are in + progress for either ip address. + - Verify in post analysis that the continue statement is + hit in the for loop that iterates over ip addresses since + one of the ip addresses is manually added to ipv4_done. + + Setup + - Manually add one ip address to ipv4_done + - Set instance.unit_test to True so that instance.ipv4_done is not + initialized to an empty set in _wait_for_current_actions_to_complete + + Description + _wait_for_current_actions_to_complete waits until staging, validation, + and upgrade actions are complete for all ip addresses. It calls + SwitchIssuDetailsByIpAddress.actions_in_progress() and expects + this to return False. actions_in_progress() returns True until none of + the following keys has a value of "In-Progress": + + ["imageStaged", "upgrade", "validated"] + + Expectations: + 1. instance.ipv4_done is a set() + 2. instance.ipv4_done is length 2 + 3. instance.ipv4_done contains all ip addresses in + instance.ip_addresses + 4. fail_json is not called + 5. (Post analysis) converage tool indicates tha the continue + statement is hit. + """ + instance = image_upgrade + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_upgrade_upgrade_00205a" + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + instance.check_interval = 0 + instance.ipv4_done.add("172.22.150.102") + instance.unit_test = True + with does_not_raise(): + instance._wait_for_current_actions_to_complete() + assert isinstance(instance.ipv4_done, set) + assert len(instance.ipv4_done) == 2 + assert "172.22.150.102" in instance.ipv4_done + assert "172.22.150.108" in instance.ipv4_done + + +def test_image_upgrade_upgrade_00210( monkeypatch, image_upgrade, issu_details_by_ip_address ) -> None: """ @@ -2064,7 +2274,7 @@ def test_image_upgrade_upgrade_00081( instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_upgrade_upgrade_00081a" + key = "test_image_upgrade_upgrade_00210a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2091,7 +2301,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" not in instance.ipv4_done -def test_image_upgrade_upgrade_00090( +def test_image_upgrade_upgrade_00220( monkeypatch, image_upgrade, issu_details_by_ip_address ) -> None: """ @@ -2117,7 +2327,7 @@ def test_image_upgrade_upgrade_00090( instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_upgrade_upgrade_00090a" + key = "test_image_upgrade_upgrade_00220a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2142,7 +2352,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert "172.22.150.108" not in instance.ipv4_done -def test_image_upgrade_upgrade_00091( +def test_image_upgrade_upgrade_00230( monkeypatch, image_upgrade, issu_details_by_ip_address ) -> None: """ @@ -2171,7 +2381,7 @@ def test_image_upgrade_upgrade_00091( instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_upgrade_upgrade_00091a" + key = "test_image_upgrade_upgrade_00230a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) @@ -2197,3 +2407,179 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: assert len(instance.ipv4_done) == 1 assert "172.22.150.102" in instance.ipv4_done assert "172.22.150.108" not in instance.ipv4_done + + +def test_image_upgrade_upgrade_00240( + monkeypatch, image_upgrade, issu_details_by_ip_address +) -> None: + """ + Function + - _wait_for_image_upgrade_to_complete + + Summary + Verify that, when two ip addresses are checked, the method's + continue statement is reached. This is verified in post analysis + using the coverage report. + + Setup + - SwitchIssuDetails is mocked to indicate that both ip address + upgrade status == Success + - instance.ipv4_done is set manually to contain one of the ip addresses + - Set instance.unit_test to True so that instance.ipv4_done is not + initialized to an empty set in _wait_for_image_upgrade_to_complete + + Description + _wait_for_image_upgrade_to_complete looks at the upgrade status for each + ip address and waits for it to be "Success" or "Failed". + In the case where all ip addresses are "Success", the module returns. + Since instance.ipv4_done is manually populated with one of the ip addresses, + and instance.unit_test is set to True, the method's continue statement is + reached. This is verified in post analysis using the coverage report. + + Expectations: + - instance.ipv4_done will have length 2 + - instance.ipv4_done contains 172.22.150.102 and 172.22.150.108 + - fail_json is not called + """ + instance = image_upgrade + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_upgrade_upgrade_00240a" + return responses_switch_issu_details(key) + + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + instance.check_interval = 1 + instance.check_timeout = 1 + instance.ipv4_done.add("172.22.150.102") + instance.unit_test = True + with does_not_raise(): + instance._wait_for_image_upgrade_to_complete() + assert isinstance(instance.ipv4_done, set) + assert len(instance.ipv4_done) == 2 + assert "172.22.150.102" in instance.ipv4_done + assert "172.22.150.108" in instance.ipv4_done + + +def test_image_upgrade_upgrade_00250(image_upgrade) -> None: + """ + Function + - ImageUpgrade._build_payload_issu_upgrade + + Summary + Verify that fail_json is called when device.upgrade.nxos is not a boolean + + Setup + - device.upgrade.nxos is set to "FOO" + - device is passed to _build_payload_issu_upgrade + """ + match = r"ImageUpgrade\._build_payload_issu_upgrade: upgrade\.nxos must " + match += r"be a boolean\. Got FOO\." + + device = {"upgrade": {"nxos": "FOO"}} + + with does_not_raise(): + instance = image_upgrade + with pytest.raises(AnsibleFailJson, match=match): + instance._build_payload_issu_upgrade(device) + + +def test_image_upgrade_upgrade_00260(image_upgrade) -> None: + """ + Function + - ImageUpgrade._build_payload_issu_options_1 + + Summary + Verify that fail_json is called when device.options.nxos.mode is + set to an invalid value. + + Setup + - device.options.nxos.mode is set to invalid value "FOO" + - device is passed to _build_payload_issu_options_1 + """ + match = r"ImageUpgrade\._build_payload_issu_options_1: " + match += r"options\.nxos\.mode must be one of.*Got FOO\." + + device = {"options": {"nxos": {"mode": "FOO"}}} + + with does_not_raise(): + instance = image_upgrade + with pytest.raises(AnsibleFailJson, match=match): + instance._build_payload_issu_options_1(device) + + +def test_image_upgrade_upgrade_00270(image_upgrade) -> None: + """ + Function + - ImageUpgrade._build_payload_epld + + Summary + Verify that fail_json is called when device.upgrade.epld is not a boolean + + Setup + - device.upgrade.epld is set to "FOO" + - device is passed to _build_payload_epld + """ + match = r"ImageUpgrade\._build_payload_epld: upgrade.epld must be a " + match += r"boolean\. Got FOO\." + + device = {"upgrade": {"epld": "FOO"}} + + with does_not_raise(): + instance = image_upgrade + with pytest.raises(AnsibleFailJson, match=match): + instance._build_payload_epld(device) + + +def test_image_upgrade_upgrade_00280(image_upgrade) -> None: + """ + Function + - ImageUpgrade._build_payload_package + + Summary + Verify that fail_json is called when device.options.package.install + is not a boolean + + Setup + - device.options.package.install is set to "FOO" + - device is passed to _build_payload_package + """ + match = r"ImageUpgrade\._build_payload_package: options.package.install " + match += r"must be a boolean\. Got FOO\." + + device = {"options": {"package": {"install": "FOO"}}} + + with does_not_raise(): + instance = image_upgrade + with pytest.raises(AnsibleFailJson, match=match): + instance._build_payload_package(device) + + +def test_image_upgrade_upgrade_00281(image_upgrade) -> None: + """ + Function + - ImageUpgrade._build_payload_package + + Summary + Verify that fail_json is called when device.options.package.uninstall + is not a boolean + + Setup + - device.options.package.install is set to a boolean + - device.options.package.uninstall is set to "FOO" + - device is passed to _build_payload_package + """ + match = r"ImageUpgrade\._build_payload_package: options.package.uninstall " + match += r"must be a boolean\. Got FOO\." + + device = {"options": {"package": {"install": True, "uninstall": "FOO"}}} + + with does_not_raise(): + instance = image_upgrade + with pytest.raises(AnsibleFailJson, match=match): + instance._build_payload_package(device) From 0e32d5283e19aea1ad62743e26f7297a5aceabde Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 25 Feb 2024 08:37:48 -1000 Subject: [PATCH 271/300] Move issu_detail.refresh() out of inner for loops --- plugins/module_utils/image_upgrade/image_stage.py | 5 ++--- plugins/module_utils/image_upgrade/image_upgrade.py | 2 +- plugins/module_utils/image_upgrade/image_validate.py | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py index 2da061b19..bf1fd69a3 100644 --- a/plugins/module_utils/image_upgrade/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -194,7 +194,6 @@ def commit(self): self.result_current = {"changed": False, "success": True} return - # self.issu_detail.refresh() self.prune_serial_numbers() self.validate_serial_numbers() self._wait_for_current_actions_to_complete() @@ -267,13 +266,13 @@ def _wait_for_current_actions_to_complete(self): while self.serial_numbers_done != serial_numbers_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + self.issu_detail.refresh() for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue self.issu_detail.filter = serial_number - self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: self.serial_numbers_done.add(serial_number) @@ -300,13 +299,13 @@ def _wait_for_image_stage_to_complete(self): while self.serial_numbers_done != serial_numbers_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + self.issu_detail.refresh() for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue self.issu_detail.filter = serial_number - self.issu_detail.refresh() ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name staged_percent = self.issu_detail.image_staged_percent diff --git a/plugins/module_utils/image_upgrade/image_upgrade.py b/plugins/module_utils/image_upgrade/image_upgrade.py index 4eae955a8..50d2177f6 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade.py +++ b/plugins/module_utils/image_upgrade/image_upgrade.py @@ -207,9 +207,9 @@ def _validate_devices(self) -> None: msg += "call instance.devices before calling commit." self.module.fail_json(msg, **self.failed_result) + self.issu_detail.refresh() for device in self.devices: self.issu_detail.filter = device.get("ip_address") - self.issu_detail.refresh() # Any device validation from issu_detail would go here. # We used to fail_json if upgrade == "Failed" but that diff --git a/plugins/module_utils/image_upgrade/image_validate.py b/plugins/module_utils/image_upgrade/image_validate.py index 8a065dc03..7c0c6c63d 100644 --- a/plugins/module_utils/image_upgrade/image_validate.py +++ b/plugins/module_utils/image_upgrade/image_validate.py @@ -130,9 +130,9 @@ def validate_serial_numbers(self) -> None: """ self.method_name = inspect.stack()[0][3] + self.issu_detail.refresh() for serial_number in self.serial_numbers: self.issu_detail.filter = serial_number - self.issu_detail.refresh() if self.issu_detail.validated == "Failed": msg = f"{self.class_name}.{self.method_name}: " msg += "image validation is failing for the following switch: " @@ -243,13 +243,13 @@ def _wait_for_current_actions_to_complete(self) -> None: while self.serial_numbers_done != serial_numbers_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + self.issu_detail.refresh() for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue self.issu_detail.filter = serial_number - self.issu_detail.refresh() if self.issu_detail.actions_in_progress is False: self.serial_numbers_done.add(serial_number) @@ -276,13 +276,13 @@ def _wait_for_image_validate_to_complete(self) -> None: while self.serial_numbers_done != serial_numbers_todo and timeout > 0: sleep(self.check_interval) timeout -= self.check_interval + self.issu_detail.refresh() for serial_number in self.serial_numbers: if serial_number in self.serial_numbers_done: continue self.issu_detail.filter = serial_number - self.issu_detail.refresh() ip_address = self.issu_detail.ip_address device_name = self.issu_detail.device_name From c1ff802fd50438665f29bbea15c5c9c400aefc45 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 25 Feb 2024 10:01:41 -1000 Subject: [PATCH 272/300] Remove caplog from unit tests, update docstrings. Update unit test docstrings to include class name. Removing caplog from all unit tests where this was present (two tests). --- .../test_image_upgrade_image_upgrade.py | 111 ++++++++++-------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 3dbe2c1da..073e42eba 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -66,7 +66,7 @@ def test_image_upgrade_upgrade_00001(image_upgrade) -> None: """ Function - - __init__ + - ImageUpgrade.__init__ Test - Class attributes are initialized to expected values @@ -87,7 +87,7 @@ def test_image_upgrade_upgrade_00001(image_upgrade) -> None: def test_image_upgrade_upgrade_00003(image_upgrade) -> None: """ Function - - _init_properties + - ImageUpgrade._init_properties Test - Class properties are initialized to expected values @@ -124,7 +124,7 @@ def test_image_upgrade_upgrade_00003(image_upgrade) -> None: def test_image_upgrade_upgrade_00004(monkeypatch, image_upgrade) -> None: """ Function - - validate_devices + - ImageUpgrade.validate_devices Test - ip_addresses contains the ip addresses of the devices for which @@ -161,7 +161,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_upgrade_00005(image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - fail_json is called because devices is None @@ -239,7 +239,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): instance.commit() -def test_image_upgrade_upgrade_00019(monkeypatch, image_upgrade, caplog) -> None: +def test_image_upgrade_upgrade_00019(monkeypatch, image_upgrade) -> None: """ Function - ImageUpgrade._build_payload @@ -271,7 +271,6 @@ def test_image_upgrade_upgrade_00019(monkeypatch, image_upgrade, caplog) -> None ansible-playbook against the controller for this scenario, which verifies that the non-default values are included in the payload. """ - caplog.set_level(logging.DEBUG) instance = image_upgrade key = "test_image_upgrade_upgrade_00019a" @@ -334,7 +333,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: assert instance.payload == payloads_image_upgrade(key) -def test_image_upgrade_upgrade_00020(monkeypatch, image_upgrade, caplog) -> None: +def test_image_upgrade_upgrade_00020(monkeypatch, image_upgrade) -> None: """ Function - ImageUpgrade.commit @@ -362,7 +361,6 @@ def test_image_upgrade_upgrade_00020(monkeypatch, image_upgrade, caplog) -> None 1. instance.payload will equal a payload previously obtained by running ansible-playbook against the controller for this scenario """ - caplog.set_level(logging.DEBUG) instance = image_upgrade key = "test_image_upgrade_upgrade_00020a" @@ -427,7 +425,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_upgrade_00021(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Invalid value for nxos.mode @@ -496,7 +494,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00022(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Force code coverage of nxos.mode == "non_disruptive" path @@ -582,7 +580,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_upgrade_00023(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Force code coverage of nxos.mode == "force_non_disruptive" path @@ -668,7 +666,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_upgrade_00024(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Invalid value for options.nxos.bios_force @@ -736,7 +734,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00025(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Incompatible values for options.epld.golden and upgrade.nxos @@ -806,7 +804,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00026(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Invalid value for epld.module @@ -874,7 +872,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00027(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Invalid value for epld.golden @@ -941,7 +939,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00028(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Invalid value for reboot @@ -1008,7 +1006,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00029(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Invalid value for options.reboot.config_reload @@ -1076,7 +1074,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00030(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Invalid value for options.reboot.write_erase @@ -1144,7 +1142,8 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00031(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit + Test - Invalid value for options.package.uninstall @@ -1217,7 +1216,7 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00032(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Bad result code in image upgrade response @@ -1305,7 +1304,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_upgrade_00033(monkeypatch, image_upgrade) -> None: """ Function - - commit + - ImageUpgrade.commit Test - Invalid value for upgrade.epld @@ -1372,7 +1371,8 @@ def mock_wait_for_current_actions_to_complete(*args, **kwargs): def test_image_upgrade_upgrade_00045(monkeypatch, image_upgrade) -> None: """ Function - - response_data + - ImageUpgrade.commit + - ImageUpgradeCommon.response_data getter Setup: - ImageUpgrade.devices is set to a list of one dict for a device @@ -1448,15 +1448,16 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: "policy_changed": True, } ] - - instance.commit() + with does_not_raise(): + instance.commit() assert instance.response_data == [121] def test_image_upgrade_upgrade_00046(monkeypatch, image_upgrade) -> None: """ Function - - result + - ImageUpgradeCommon.result + - ImageUpgrade.commit Setup: - ImageUpgrade.devices is set to a list of one dict for a device @@ -1530,15 +1531,17 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: } ] - instance.unit_test = True - instance.commit() + with does_not_raise(): + instance.unit_test = True + instance.commit() assert instance.result == [{"success": True, "changed": True}] def test_image_upgrade_upgrade_00047(monkeypatch, image_upgrade) -> None: """ Function - - response + - ImageUpgradeCommon.response + - ImageUpgrade.commit Setup: - ImageUpgrade.devices is set to a list of one dict for a device @@ -1615,8 +1618,8 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: } ] - instance.commit() - print(f"instance.response: {instance.response}") + with does_not_raise(): + instance.commit() assert isinstance(instance.response, list) assert instance.response[0]["DATA"] == 121 @@ -1639,7 +1642,7 @@ def test_image_upgrade_upgrade_00060( ) -> None: """ Function - - bios_force setter + - ImageUpgrade.bios_force Verify that bios_force does not call fail_json if passed a boolean. Verify that bios_force does call fail_json if passed a non-boolean. @@ -1673,7 +1676,7 @@ def test_image_upgrade_upgrade_00070( ) -> None: """ Function - - check_interval setter + - ImageUpgrade.check_interval Summary Verify that check_interval does not call fail_json if the value is an integer @@ -1707,7 +1710,7 @@ def test_image_upgrade_upgrade_00075( ) -> None: """ Function - - check_timeout setter + - ImageUpgrade.check_timeout Summary Verify that check_timeout does not call fail_json if the value is an integer @@ -1741,7 +1744,12 @@ def test_image_upgrade_upgrade_00080( ) -> None: """ Function - - config_reload setter + - ImageUpgrade.config_reload + + Summary + Verify that config_reload does not call fail_json if passed a boolean. + Verify that config_reload does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. """ with does_not_raise(): instance = image_upgrade @@ -1782,7 +1790,7 @@ def test_image_upgrade_upgrade_00080( def test_image_upgrade_upgrade_00090(image_upgrade, value, expected) -> None: """ Function - - devices setter + - ImageUpgrade.devices Summary Verify that devices does not call fail_json if passed a list of dicts @@ -1811,7 +1819,7 @@ def test_image_upgrade_upgrade_00100( ) -> None: """ Function - - disruptive setter + - ImageUpgrade.disruptive Summary Verify that disruptive does not call fail_json if passed a boolean. @@ -1845,7 +1853,7 @@ def test_image_upgrade_upgrade_00110( ) -> None: """ Function - - epld_golden setter + - ImageUpgrade.epld_golden Summary Verify that epld_golden does not call fail_json if passed a boolean. @@ -1879,7 +1887,7 @@ def test_image_upgrade_upgrade_00120( ) -> None: """ Function - - epld_upgrade setter + - ImageUpgrade.epld_upgrade Summary Verify that epld_upgrade does not call fail_json if passed a boolean. @@ -1915,7 +1923,7 @@ def test_image_upgrade_upgrade_00130( ) -> None: """ Function - - epld_module setter + - ImageUpgrade.epld_module Summary Verify that epld_module does not call fail_json if passed a valid value. @@ -1953,7 +1961,7 @@ def test_image_upgrade_upgrade_00140( ) -> None: """ Function - - force_non_disruptive setter + - ImageUpgrade.force_non_disruptive Summary Verify that force_non_disruptive does not call fail_json if passed a boolean. @@ -1987,7 +1995,7 @@ def test_image_upgrade_upgrade_00150( ) -> None: """ Function - - non_disruptive setter + - ImageUpgrade.non_disruptive Summary Verify that non_disruptive does not call fail_json if passed a boolean. @@ -2021,7 +2029,7 @@ def test_image_upgrade_upgrade_00160( ) -> None: """ Function - - package_install setter + - ImageUpgrade.package_install Summary Verify that package_install does not call fail_json if passed a boolean. @@ -2055,7 +2063,7 @@ def test_image_upgrade_upgrade_00170( ) -> None: """ Function - - package_uninstall setter + - ImageUpgrade.package_uninstall Summary Verify that package_uninstall does not call fail_json if passed a boolean. @@ -2089,7 +2097,7 @@ def test_image_upgrade_upgrade_00180( ) -> None: """ Function - - reboot setter + - ImageUpgrade.reboot Summary Verify that reboot does not call fail_json if passed a boolean. @@ -2123,7 +2131,7 @@ def test_image_upgrade_upgrade_00190( ) -> None: """ Function - - write_erase setter + - ImageUpgrade.write_erase Summary Verify that write_erase does not call fail_json if passed a boolean. @@ -2145,7 +2153,7 @@ def test_image_upgrade_upgrade_00200( ) -> None: """ Function - - _wait_for_current_actions_to_complete + - ImageUpgrade._wait_for_current_actions_to_complete Test - Two switches are added to ipv4_done @@ -2193,7 +2201,7 @@ def test_image_upgrade_upgrade_00205( ) -> None: """ Function - - _wait_for_current_actions_to_complete + - ImageUpgrade._wait_for_current_actions_to_complete Summary - Verify that ipv4_done contains two ip addresses since @@ -2255,7 +2263,7 @@ def test_image_upgrade_upgrade_00210( ) -> None: """ Function - - _wait_for_current_actions_to_complete + - ImageUpgrade._wait_for_current_actions_to_complete Test - one switch is added to ipv4_done @@ -2306,7 +2314,7 @@ def test_image_upgrade_upgrade_00220( ) -> None: """ Function - - _wait_for_image_upgrade_to_complete + - ImageUpgrade._wait_for_image_upgrade_to_complete Test - One ip address is added to ipv4_done due to issu_detail.upgrade == "Success" @@ -2357,7 +2365,8 @@ def test_image_upgrade_upgrade_00230( ) -> None: """ Function - - _wait_for_image_upgrade_to_complete + - ImageUpgrade._wait_for_image_upgrade_to_complete + Test - One ip address is added to ipv4_done as issu_detail.upgrade == "Success" @@ -2414,7 +2423,7 @@ def test_image_upgrade_upgrade_00240( ) -> None: """ Function - - _wait_for_image_upgrade_to_complete + - ImageUpgrade._wait_for_image_upgrade_to_complete Summary Verify that, when two ip addresses are checked, the method's From 9bb1b50c2a6422eb473c0e44680aae06fd97743b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 25 Feb 2024 13:33:41 -1000 Subject: [PATCH 273/300] ImageUpgradeCommon: add unit tests Brings ImageUpgradeCommon unit test coverage to 99% - Updated setters which expect an int() to test first for bool(). - Added unit tests for: - changed - diff - failed - response_current - response - result_current - result - send_interval - timeout - unit_test --- .../image_upgrade/image_upgrade_common.py | 30 +- ...test_image_upgrade_image_upgrade_common.py | 373 +++++++++++++++++- 2 files changed, 379 insertions(+), 24 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_upgrade_common.py b/plugins/module_utils/image_upgrade/image_upgrade_common.py index fd79b4319..3434fcca6 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_common.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_common.py @@ -263,7 +263,7 @@ def changed(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " - msg += f"changed must be a bool. Got {value}" + msg += f"{method_name} must be a bool. Got {value}" self.module.fail_json(msg) self.properties["changed"] = value @@ -279,7 +279,7 @@ def diff(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += f"diff must be a dict. Got {value}" + msg += f"{method_name} must be a dict. Got {value}" self.module.fail_json(msg) self.properties["diff"].append(value) @@ -297,7 +297,7 @@ def failed(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " - msg += f"failed must be a bool. Got {value}" + msg += f"{method_name} must be a bool. Got {value}" self.module.fail_json(msg) self.properties["failed"] = value @@ -316,7 +316,7 @@ def response_current(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.response_current must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got {value}." self.module.fail_json(msg, **self.failed_result) self.properties["response_current"] = value @@ -336,7 +336,7 @@ def response(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.response must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got {value}." self.module.fail_json(msg, **self.failed_result) self.properties["response"].append(value) @@ -367,7 +367,7 @@ def result(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.result must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got {value}." self.module.fail_json(msg, **self.failed_result) self.properties["result"].append(value) @@ -387,7 +387,7 @@ def result_current(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.result_current must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got {value}." self.module.fail_json(msg, **self.failed_result) self.properties["result_current"] = value @@ -404,9 +404,13 @@ def send_interval(self): @send_interval.setter def send_interval(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an integer. Got {value}." + # isinstance(False, int) returns True, so we need first + # to test for this and fail_json specifically for bool values. + if isinstance(value, bool): + self.module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be an int(). Got {value}." self.module.fail_json(msg, **self.failed_result) self.properties["send_interval"] = value @@ -422,9 +426,13 @@ def timeout(self): @timeout.setter def timeout(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an integer. Got {value}." + # isinstance(False, int) returns True, so we need first + # to test for this and fail_json specifically for bool values. + if isinstance(value, bool): + self.module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be an int(). Got {value}." self.module.fail_json(msg, **self.failed_result) self.properties["timeout"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 7f13873c1..62d7e7ff8 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -44,7 +44,7 @@ def test_image_upgrade_image_upgrade_common_00001(image_upgrade_common) -> None: """ Function - - __init__ + - ImageUpgradeCommon.__init__ Summary Verify that instance.params accepts well-formed input and that the @@ -93,7 +93,7 @@ def test_image_upgrade_image_upgrade_common_00020( ) -> None: """ Function - - _handle_response + - ImageUpgradeCommon._handle_response Test - json_fail is not called @@ -136,7 +136,7 @@ def test_image_upgrade_image_upgrade_common_00030( ) -> None: """ Function - - _handle_response + - ImageUpgradeCommon._handle_response Test - json_fail is not called @@ -179,7 +179,7 @@ def test_image_upgrade_image_upgrade_common_00040( ) -> None: """ Function - - _handle_response + - ImageUpgradeCommon._handle_response Test - json_fail is not called @@ -226,7 +226,7 @@ def test_image_upgrade_image_upgrade_common_00050( ) -> None: """ Function - - _handle_response + - ImageUpgradeCommon._handle_response Test - _handle_reponse returns expected values for GET requests @@ -244,7 +244,7 @@ def test_image_upgrade_image_upgrade_common_00050( def test_image_upgrade_image_upgrade_common_00060(image_upgrade_common) -> None: """ Function - - _handle_response + - ImageUpgradeCommon._handle_response Test - fail_json is called because an unknown request verb is provided @@ -286,7 +286,7 @@ def test_image_upgrade_image_upgrade_common_00070( ) -> None: """ Function - - _handle_get_response + - ImageUpgradeCommon._handle_get_response Test - fail_json is not called @@ -326,7 +326,7 @@ def test_image_upgrade_image_upgrade_common_00080( ) -> None: """ Function - - _handle_post_put_delete_response + - ImageUpgradeCommon._handle_post_put_delete_response Summary Verify that expected values are returned for POST requests. @@ -374,7 +374,7 @@ def test_image_upgrade_image_upgrade_common_00090( ) -> None: """ Function - - make_boolean + - ImageUpgradeCommon.make_boolean Test - expected values are returned for all cases @@ -408,7 +408,7 @@ def test_image_upgrade_image_upgrade_common_00100( ) -> None: """ Function - - make_none + - ImageUpgradeCommon.make_none Test - expected values are returned for all cases @@ -420,7 +420,7 @@ def test_image_upgrade_image_upgrade_common_00100( def test_image_upgrade_image_upgrade_common_00110(image_upgrade_common) -> None: """ Function - - log.log_msg + - ImageUpgradeCommon.log.log_msg Test - log.debug returns None when the base logger is disabled @@ -438,7 +438,7 @@ def test_image_upgrade_image_upgrade_common_00120( ) -> None: """ Function - - dcnm_send_with_retry + - ImageUpgradeCommon.dcnm_send_with_retry Summary Verify that result and response are set to the expected values when @@ -463,7 +463,7 @@ def test_image_upgrade_image_upgrade_common_00121( ) -> None: """ Function - - dcnm_send_with_retry + - ImageUpgradeCommon.dcnm_send_with_retry Summary Verify that result and response are set to the expected values when @@ -482,3 +482,350 @@ def mock_dcnm_send(*args, **kwargs): ) assert instance.response_current == {"MESSAGE": "OK", "RETURN_CODE": 200} assert instance.result == [{"changed": True, "success": True}] + + +MATCH_00130 = "ImageUpgradeCommon.changed: changed must be a bool." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00130), True), + ], +) +def test_image_upgrade_upgrade_00130( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgradeCommon.changed + + Verify that changed does not call fail_json if passed a boolean. + Verify that changed does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + + with expected: + instance.changed = value + if raise_flag is False: + assert instance.changed == value + else: + assert instance.changed is False + + +MATCH_00140 = "ImageUpgradeCommon.diff: diff must be a dict." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + ({}, does_not_raise(), False), + ({"foo": "bar"}, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00140), True), + (True, pytest.raises(AnsibleFailJson, match=MATCH_00140), True), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00140), True), + ], +) +def test_image_upgrade_upgrade_00140( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgradeCommon.diff + + Verify that diff does not call fail_json if passed a dict. + Verify that diff does call fail_json if passed a non-dict. + Verify that diff returns list(value) when its getter is called. + Verify that the default value ([]) is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + + with expected: + instance.diff = value + if raise_flag is False: + assert instance.diff == [value] + else: + assert instance.diff == [] + + +MATCH_00150 = "ImageUpgradeCommon.failed: failed must be a bool." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00150), True), + ], +) +def test_image_upgrade_upgrade_00150( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgradeCommon.failed + + Verify that failed does not call fail_json if passed a boolean. + Verify that failed does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + + with expected: + instance.failed = value + if raise_flag is False: + assert instance.failed == value + else: + assert instance.failed is False + + +MATCH_00160 = "ImageUpgradeCommon.response_current: response_current must be a dict." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + ({}, does_not_raise(), False), + ({"foo": "bar"}, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00160), True), + (True, pytest.raises(AnsibleFailJson, match=MATCH_00160), True), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00160), True), + ], +) +def test_image_upgrade_upgrade_00160( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgradeCommon.response_current + + Verify that response_current does not call fail_json if passed a dict. + Verify that response_current does call fail_json if passed a non-dict. + Verify that response_current returns value when its getter is called. + Verify that the default value ({}) is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + + with expected: + instance.response_current = value + if raise_flag is False: + assert instance.response_current == value + else: + assert instance.response_current == {} + + +MATCH_00170 = "ImageUpgradeCommon.response: response must be a dict." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + ({}, does_not_raise(), False), + ({"foo": "bar"}, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00170), True), + (True, pytest.raises(AnsibleFailJson, match=MATCH_00170), True), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00170), True), + ], +) +def test_image_upgrade_upgrade_00170( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgradeCommon.response + + Verify that response does not call fail_json if passed a dict. + Verify that response does call fail_json if passed a non-dict. + Verify that response returns list(value) when its getter is called. + Verify that the default value ([]) is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + + with expected: + instance.response = value + if raise_flag is False: + assert instance.response == [value] + else: + assert instance.response == [] + + +MATCH_00180 = "ImageUpgradeCommon.result: result must be a dict." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + ({}, does_not_raise(), False), + ({"foo": "bar"}, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00180), True), + (True, pytest.raises(AnsibleFailJson, match=MATCH_00180), True), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00180), True), + ], +) +def test_image_upgrade_upgrade_00180( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgradeCommon.result + + Verify that result does not call fail_json if passed a dict. + Verify that result does call fail_json if passed a non-dict. + Verify that result returns list(value) when its getter is called. + Verify that the default value ([]) is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + + with expected: + instance.result = value + if raise_flag is False: + assert instance.result == [value] + else: + assert instance.result == [] + + +MATCH_00190 = "ImageUpgradeCommon.result_current: result_current must be a dict." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + ({}, does_not_raise(), False), + ({"foo": "bar"}, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00190), True), + (True, pytest.raises(AnsibleFailJson, match=MATCH_00190), True), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00190), True), + ], +) +def test_image_upgrade_upgrade_00190( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgradeCommon.result_current + + Verify that result_current does not call fail_json if passed a dict. + Verify that result_current does call fail_json if passed a non-dict. + Verify that result_current returns value when its getter is called. + Verify that the default value ({}) is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + + with expected: + instance.result_current = value + if raise_flag is False: + assert instance.result_current == value + else: + assert instance.result_current == {} + + +MATCH_00200 = r"ImageUpgradeCommon\.send_interval: send_interval " +MATCH_00200 += r"must be an integer\." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + (1, does_not_raise(), False), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00200), True), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00200), True), + ], +) +def test_image_upgrade_upgrade_00200( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgrade.send_interval + + Summary + Verify that send_interval does not call fail_json if the value is an integer + and does call fail_json if the value is not an integer. Verify that the + default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + with expected: + instance.send_interval = value + if raise_flag is False: + assert instance.send_interval == value + else: + assert instance.send_interval == 5 + + +MATCH_00210 = r"ImageUpgradeCommon\.timeout: timeout " +MATCH_00210 += r"must be an integer\." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + (1, does_not_raise(), False), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00210), True), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00210), True), + ], +) +def test_image_upgrade_upgrade_00210( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgrade.timeout + + Summary + Verify that timeout does not call fail_json if the value is an integer + and does call fail_json if the value is not an integer. Verify that the + default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + with expected: + instance.timeout = value + if raise_flag is False: + assert instance.timeout == value + else: + assert instance.timeout == 300 + + +MATCH_00220 = "ImageUpgradeCommon.unit_test: unit_test must be a bool." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + (True, does_not_raise(), False), + (False, does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00220), True), + ], +) +def test_image_upgrade_upgrade_00220( + image_upgrade_common, value, expected, raise_flag +) -> None: + """ + Function + - ImageUpgradeCommon.unit_test + + Verify that unit_test does not call fail_json if passed a boolean. + Verify that unit_test does call fail_json if passed a non-boolean. + Verify that the default value is set if fail_json is called. + """ + with does_not_raise(): + instance = image_upgrade_common + + with expected: + instance.unit_test = value + if raise_flag is False: + assert instance.unit_test == value + else: + assert instance.unit_test is False From db36e9ff1376371c5c8a64950af6494e2fd42c17 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 25 Feb 2024 18:01:41 -1000 Subject: [PATCH 274/300] ImagePolicies: Add unit tests, update docstrings, remove inherited properties Removed the following properties which are inherited from ImageUpgradeCommon: - response_data - response - result Add unit tests for: - ImagePolicies._get (two tests) - ImagePolicies.all_policies (two tests) Improve docstrings for existing tests. --- .../image_upgrade/image_policies.py | 27 +- ...image_upgrade_responses_ImagePolicies.json | 92 +++++++ .../test_image_upgrade_image_policies.py | 230 ++++++++++++++++-- 3 files changed, 303 insertions(+), 46 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_policies.py b/plugins/module_utils/image_upgrade/image_policies.py index 6256e4dd8..b78d3d8c9 100644 --- a/plugins/module_utils/image_upgrade/image_policies.py +++ b/plugins/module_utils/image_upgrade/image_policies.py @@ -38,7 +38,8 @@ class ImagePolicies(ImageUpgradeCommon): Usage (where module is an instance of AnsibleModule): - instance = ImagePolicies(module).refresh() + instance = ImagePolicies(module) + instance.refresh() instance.policy_name = "NR3F" if instance.name is None: print("policy NR3F does not exist on the controller") @@ -48,8 +49,6 @@ class ImagePolicies(ImageUpgradeCommon): epd_image_name = instance.epld_image_name etc... - Policies can be refreshed by calling instance.refresh(). - Endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ @@ -187,28 +186,6 @@ def name(self): """ return self._get("policyName") - @property - def response_data(self): - """ - Return the parsed data from the response as a dictionary, - keyed on policy_name. - """ - return self.properties["response_data"] - - @property - def response(self): - """ - Return the raw response from the controller. - """ - return self.properties["response"] - - @property - def result(self): - """ - Return the raw result. - """ - return self.properties["result"] - @property def policy_name(self): """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json index 74cc1f79b..4294ba34d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json @@ -270,5 +270,97 @@ ], "message": "" } + }, + "test_image_upgrade_image_policies_00041a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policies_00042a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policies_00051a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + }, + { + "policyName": "OR1F", + "policyType": "PLATFORM", + "nxosVersion": "10.4.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "OR1F EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.4.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.4.1.F.bin", + "agnostic": "false", + "ref_count": 0 + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index eb8d18ba6..06f917468 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -46,7 +46,7 @@ def test_image_upgrade_image_policies_00001(image_policies) -> None: """ Function - - __init__ + - ImagePolicies.__init__ Test - Class attributes are initialized to expected values @@ -61,7 +61,7 @@ def test_image_upgrade_image_policies_00001(image_policies) -> None: def test_image_upgrade_image_policies_00002(image_policies) -> None: """ Function - - _init_properties + - ImagePolicies._init_properties Test - Class properties are initialized to expected values @@ -78,12 +78,19 @@ def test_image_upgrade_image_policies_00002(image_policies) -> None: def test_image_upgrade_image_policies_00010(monkeypatch, image_policies) -> None: """ Function - - refresh + - ImagePolicies.refresh + - ImagePolicies.policy_name + + + Summary + Verify that refresh returns image policy info and that the filtered + properties associated with policy_name are the expected values. Test - - properties are initialized to expected values - - 200 RETURN_CODE - - fail_json is not called + - properties for policy_name are set to reflect the response from + the controller + - 200 RETURN_CODE + - fail_json is not called Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies @@ -117,10 +124,11 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: def test_image_upgrade_image_policies_00020(monkeypatch, image_policies) -> None: """ Function - - refresh + - ImagePolicies.refresh + - ImagePolicies.result Test - - result contains expected key/values on 200 response from endpoint. + - Imagepolicies.result contains expected key/values on 200 response from endpoint. Endpoint - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies @@ -144,7 +152,11 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: def test_image_upgrade_image_policies_00021(monkeypatch, image_policies) -> None: """ Function - - refresh + - ImagePolicies.refresh + + Summary + Verify that fail_json is called when the response from the controller + contains a 404 RETURN_CODE. Test - fail_json is called on 404 RETURN_CODE in response. @@ -171,7 +183,11 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: def test_image_upgrade_image_policies_00022(monkeypatch, image_policies) -> None: """ Function - - refresh + - ImagePolicies.refresh + + Summary + Verify that fail_json is called when the response from the controller + contains an empty DATA key. Test - fail_json is called on 200 RETURN_CODE with empty DATA key. @@ -198,7 +214,11 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: def test_image_upgrade_image_policies_00023(monkeypatch, image_policies) -> None: """ Function - - refresh + - ImagePolicies.refresh + + Summary + Verify that fail_json is not called when a 200 response from the controller + contains DATA.lastOperDataObject with length == 0. Test - do not fail_json when DATA.lastOperDataObject length == 0 @@ -229,11 +249,17 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: def test_image_upgrade_image_policies_00024(monkeypatch, image_policies) -> None: """ Function - - refresh - - policy_name + - ImagePolicies.refresh + - ImagePolicies.policy_name - Test + Summary + Verify when policy_name is set to a policy that does not exist on the + controller, instance.policy returns None. + + Setup - instance.policy_name is set to a policy that does not exist on the controller. + + Test - instance.policy returns None Endpoint @@ -258,7 +284,11 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: def test_image_upgrade_image_policies_00025(monkeypatch, image_policies) -> None: """ Function - - refresh + - ImagePolicies.refresh + + Summary + Verify that fail_json is called when the response from the controller + is missing the policyName key. Test - fail_json is called on response with missing policyName key. @@ -268,8 +298,7 @@ def test_image_upgrade_image_policies_00025(monkeypatch, image_policies) -> None NOTES - This is to cover a check in ImagePolicies.refresh() - - This scenario should never happen. - - Consider removing this check, and this testcase. + - This scenario should happen only with a bug, or API change, on the controller. """ key = "test_image_upgrade_image_policies_00025a" @@ -290,11 +319,12 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: def test_image_upgrade_image_policies_00026(monkeypatch, image_policies) -> None: """ Function - - refresh + - ImagePolicies.refresh + - ImageUpgradeCommon._handle_response Summary - Verify that fail_json is called when _handle_response() returns a - non-successful result. + Verify that fail_json is called when ImageUpgradeCommon._handle_response() + returns a non-successful result. Test - fail_json is called when result["success"] is False. @@ -319,10 +349,14 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: def test_image_upgrade_image_policies_00040(image_policies) -> None: """ Function - - _get + - ImagePolicies._get + + Summary + Verify that fail_json is called when _get() is called prior to setting policy_name. Test - fail_json is called when _get() is called prior to setting policy_name. + - Appropriate error message is provided. """ match = "ImagePolicies._get: instance.policy_name must be " match += "set before accessing property imageName." @@ -330,3 +364,157 @@ def test_image_upgrade_image_policies_00040(image_policies) -> None: instance = image_policies with pytest.raises(AnsibleFailJson, match=match): instance._get("imageName") # pylint: disable=protected-access + + +def test_image_upgrade_image_policies_00041(monkeypatch, image_policies) -> None: + """ + Function + - ImagePolicies._get + + Summary + Verify that fail_json is called when ImagePolicies._get is called + with an argument that does not match an item in the response data + for the policy_name returned by the controller. + + Setup + - instance.commit() is called and retrieves a response from the + controller containing informationi for policy KR5M. + - policy_name is set to KR5M. + + Test + - fail_json is called when _get() is called with a bad parameter FOO + - An appropriate error message is provided. + """ + key = "test_image_upgrade_image_policies_00041a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + + match = r"ImagePolicies\._get: KR5M does not have a key named FOO\." + + with does_not_raise(): + instance = image_policies + instance.refresh() + instance.policy_name = "KR5M" + + with pytest.raises(AnsibleFailJson, match=match): + instance._get("FOO") # pylint: disable=protected-access + + +def test_image_upgrade_image_policies_00042(monkeypatch, image_policies) -> None: + """ + Function + - ImagePolicies._get + + Summary + Verify that the correct image policy information is returned when + ImagePolicies._get is called with the "policy" arguement. + + Setup + - instance.commit() is called and retrieves a response from the + controller containing informationi for policy KR5M. + - policy_name is set to KR5M. + - _get("policy") is called. + + Test + - fail_json is not called + - The expected policy information is returned. + """ + key = "test_image_upgrade_image_policies_00042a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + + with does_not_raise(): + instance = image_policies + instance.refresh() + instance.policy_name = "KR5M" + value = instance._get("policy") # pylint: disable=protected-access + assert value["agnostic"] == "false" + assert value["epldImgName"] == "n9000-epld.10.2.5.M.img" + assert value["imageName"] == "nxos64-cs.10.2.5.M.bin" + assert value["nxosVersion"] == "10.2.5_nxos64-cs_64bit" + assert value["packageName"] == "" + assert value["platform"] == "N9K/N3K" + assert value["platformPolicies"] == "" + assert value["policyDescr"] == "10.2.(5) with EPLD" + assert value["policyName"] == "KR5M" + assert value["policyType"] == "PLATFORM" + assert value["ref_count"] == 10 + assert value["rpmimages"] == "" + + +def test_image_upgrade_image_policies_00050(image_policies) -> None: + """ + Function + - ImagePolicies.all_policies + + Summary + Verify that all_policies returns an empty dict when no policies exist + on the controller. + + Test + - fail_json is not called. + - all_policies returns an empty dict. + """ + with does_not_raise(): + instance = image_policies + foo = instance.all_policies + assert foo == {} + + +def test_image_upgrade_image_policies_00051(monkeypatch, image_policies) -> None: + """ + Function + - ImagePolicies.all_policies + + Summary + Verify that, when policies exist on the controller, all_policies returns a dict + containing these policies. + + Test + - fail_json is not called. + - all_policies returns a dict containing the controller's policies. + """ + key = "test_image_upgrade_image_policies_00051a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + print(f"mock_dcnm_send_image_policies: {responses_image_policies(key)}") + return responses_image_policies(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + + instance = image_policies + with does_not_raise(): + instance.refresh() + foo = instance.all_policies + assert foo["KR5M"]["agnostic"] == "false" + assert foo["KR5M"]["epldImgName"] == "n9000-epld.10.2.5.M.img" + assert foo["KR5M"]["imageName"] == "nxos64-cs.10.2.5.M.bin" + assert foo["KR5M"]["nxosVersion"] == "10.2.5_nxos64-cs_64bit" + assert foo["KR5M"]["packageName"] == "" + assert foo["KR5M"]["platform"] == "N9K/N3K" + assert foo["KR5M"]["platformPolicies"] == "" + assert foo["KR5M"]["policyDescr"] == "10.2.(5) with EPLD" + assert foo["KR5M"]["policyName"] == "KR5M" + assert foo["KR5M"]["policyType"] == "PLATFORM" + assert foo["KR5M"]["ref_count"] == 10 + assert foo["KR5M"]["rpmimages"] == "" + assert foo["OR1F"]["agnostic"] == "false" + assert foo["OR1F"]["epldImgName"] == "n9000-epld.10.4.1.F.img" + assert foo["OR1F"]["imageName"] == "nxos64-cs.10.4.1.F.bin" + assert foo["OR1F"]["nxosVersion"] == "10.4.1_nxos64-cs_64bit" + assert foo["OR1F"]["packageName"] == "" + assert foo["OR1F"]["platform"] == "N9K/N3K" + assert foo["OR1F"]["platformPolicies"] == "" + assert foo["OR1F"]["policyDescr"] == "OR1F EPLD" + assert foo["OR1F"]["policyName"] == "OR1F" + assert foo["OR1F"]["policyType"] == "PLATFORM" + assert foo["OR1F"]["ref_count"] == 0 + assert foo["OR1F"]["rpmimages"] == "" From d51a72170da49e2d0ef37aa1e73fffa897e255aa Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 25 Feb 2024 18:08:02 -1000 Subject: [PATCH 275/300] Replace (pylint) disallowed name "foo" --- .../test_image_upgrade_image_policies.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index 06f917468..ca3583cca 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -465,8 +465,8 @@ def test_image_upgrade_image_policies_00050(image_policies) -> None: """ with does_not_raise(): instance = image_policies - foo = instance.all_policies - assert foo == {} + value = instance.all_policies + assert value == {} def test_image_upgrade_image_policies_00051(monkeypatch, image_policies) -> None: @@ -493,28 +493,28 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance = image_policies with does_not_raise(): instance.refresh() - foo = instance.all_policies - assert foo["KR5M"]["agnostic"] == "false" - assert foo["KR5M"]["epldImgName"] == "n9000-epld.10.2.5.M.img" - assert foo["KR5M"]["imageName"] == "nxos64-cs.10.2.5.M.bin" - assert foo["KR5M"]["nxosVersion"] == "10.2.5_nxos64-cs_64bit" - assert foo["KR5M"]["packageName"] == "" - assert foo["KR5M"]["platform"] == "N9K/N3K" - assert foo["KR5M"]["platformPolicies"] == "" - assert foo["KR5M"]["policyDescr"] == "10.2.(5) with EPLD" - assert foo["KR5M"]["policyName"] == "KR5M" - assert foo["KR5M"]["policyType"] == "PLATFORM" - assert foo["KR5M"]["ref_count"] == 10 - assert foo["KR5M"]["rpmimages"] == "" - assert foo["OR1F"]["agnostic"] == "false" - assert foo["OR1F"]["epldImgName"] == "n9000-epld.10.4.1.F.img" - assert foo["OR1F"]["imageName"] == "nxos64-cs.10.4.1.F.bin" - assert foo["OR1F"]["nxosVersion"] == "10.4.1_nxos64-cs_64bit" - assert foo["OR1F"]["packageName"] == "" - assert foo["OR1F"]["platform"] == "N9K/N3K" - assert foo["OR1F"]["platformPolicies"] == "" - assert foo["OR1F"]["policyDescr"] == "OR1F EPLD" - assert foo["OR1F"]["policyName"] == "OR1F" - assert foo["OR1F"]["policyType"] == "PLATFORM" - assert foo["OR1F"]["ref_count"] == 0 - assert foo["OR1F"]["rpmimages"] == "" + value = instance.all_policies + assert value["KR5M"]["agnostic"] == "false" + assert value["KR5M"]["epldImgName"] == "n9000-epld.10.2.5.M.img" + assert value["KR5M"]["imageName"] == "nxos64-cs.10.2.5.M.bin" + assert value["KR5M"]["nxosVersion"] == "10.2.5_nxos64-cs_64bit" + assert value["KR5M"]["packageName"] == "" + assert value["KR5M"]["platform"] == "N9K/N3K" + assert value["KR5M"]["platformPolicies"] == "" + assert value["KR5M"]["policyDescr"] == "10.2.(5) with EPLD" + assert value["KR5M"]["policyName"] == "KR5M" + assert value["KR5M"]["policyType"] == "PLATFORM" + assert value["KR5M"]["ref_count"] == 10 + assert value["KR5M"]["rpmimages"] == "" + assert value["OR1F"]["agnostic"] == "false" + assert value["OR1F"]["epldImgName"] == "n9000-epld.10.4.1.F.img" + assert value["OR1F"]["imageName"] == "nxos64-cs.10.4.1.F.bin" + assert value["OR1F"]["nxosVersion"] == "10.4.1_nxos64-cs_64bit" + assert value["OR1F"]["packageName"] == "" + assert value["OR1F"]["platform"] == "N9K/N3K" + assert value["OR1F"]["platformPolicies"] == "" + assert value["OR1F"]["policyDescr"] == "OR1F EPLD" + assert value["OR1F"]["policyName"] == "OR1F" + assert value["OR1F"]["policyType"] == "PLATFORM" + assert value["OR1F"]["ref_count"] == 0 + assert value["OR1F"]["rpmimages"] == "" From 3557f544e2649bebb3ae81ed1ebc247d700e959d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 26 Feb 2024 07:20:13 -1000 Subject: [PATCH 276/300] Move rest_send.py to module_utils/common RestSend will be used by both dcnm_image_upgrade and dcnm_image_policy in the future, so moving it into module_utils/common for sharing between these modules. --- .../{image_upgrade => common}/rest_send.py | 21 ++++++++++++------- .../module_utils/image_upgrade/image_stage.py | 2 +- .../image_upgrade/image_upgrade.py | 2 +- .../image_upgrade/image_validate.py | 2 +- .../image_upgrade/switch_details.py | 2 +- 5 files changed, 18 insertions(+), 11 deletions(-) rename plugins/module_utils/{image_upgrade => common}/rest_send.py (95%) diff --git a/plugins/module_utils/image_upgrade/rest_send.py b/plugins/module_utils/common/rest_send.py similarity index 95% rename from plugins/module_utils/image_upgrade/rest_send.py rename to plugins/module_utils/common/rest_send.py index f01c6d424..cc72e45ce 100644 --- a/plugins/module_utils/image_upgrade/rest_send.py +++ b/plugins/module_utils/common/rest_send.py @@ -37,13 +37,20 @@ class RestSend: Usage (where ansible_module is an instance of AnsibleModule): - send_rest = RestSend(ansible_module) - send_rest.path = "/rest/top-down/fabrics" - send_rest.verb = "GET" - send_rest.commit() - - response = send_rest.response - result = send_rest.result + rest_send = RestSend(ansible_module) + rest_send.path = "/rest/top-down/fabrics" + rest_send.verb = "GET" + rest_send.payload = my_payload # Optional + rest_send.commit() + + # list of responses from the controller for this session + response = rest_send.response + # dict with current controller response + response_current = rest_send.response_current + # list of results from the controller for this session + result = rest_send.result + # dict with current controller result + result_current = rest_send.result_current """ def __init__(self, ansible_module): diff --git a/plugins/module_utils/image_upgrade/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py index bf1fd69a3..f8a579766 100644 --- a/plugins/module_utils/image_upgrade/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -30,7 +30,7 @@ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber diff --git a/plugins/module_utils/image_upgrade/image_upgrade.py b/plugins/module_utils/image_upgrade/image_upgrade.py index 50d2177f6..737584d5b 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade.py +++ b/plugins/module_utils/image_upgrade/image_upgrade.py @@ -31,7 +31,7 @@ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.install_options import \ ImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsByIpAddress diff --git a/plugins/module_utils/image_upgrade/image_validate.py b/plugins/module_utils/image_upgrade/image_validate.py index 7c0c6c63d..c02c90515 100644 --- a/plugins/module_utils/image_upgrade/image_validate.py +++ b/plugins/module_utils/image_upgrade/image_validate.py @@ -29,7 +29,7 @@ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber diff --git a/plugins/module_utils/image_upgrade/switch_details.py b/plugins/module_utils/image_upgrade/switch_details.py index b03bd13f3..37a64d612 100644 --- a/plugins/module_utils/image_upgrade/switch_details.py +++ b/plugins/module_utils/image_upgrade/switch_details.py @@ -26,7 +26,7 @@ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend From c0d28bc0fce1ce26235d04cc80ffc8f95a1f6b80 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 26 Feb 2024 07:40:56 -1000 Subject: [PATCH 277/300] Add path and verb properties, fix whitespace issue. Also fix some debug lines that were too long. --- plugins/module_utils/common/rest_send.py | 50 ++++++++++++++++++++---- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/common/rest_send.py b/plugins/module_utils/common/rest_send.py index cc72e45ce..d6b07fadf 100644 --- a/plugins/module_utils/common/rest_send.py +++ b/plugins/module_utils/common/rest_send.py @@ -47,7 +47,7 @@ class RestSend: response = rest_send.response # dict with current controller response response_current = rest_send.response_current - # list of results from the controller for this session + # list of results from the controller for this session result = rest_send.result # dict with current controller result result_current = rest_send.result_current @@ -63,6 +63,7 @@ def __init__(self, ansible_module): self.ansible_module = ansible_module self.params = ansible_module.params + self._valid_verbs = {"GET", "POST", "PUT", "DELETE"} self.properties = {} self.properties["response"] = [] self.properties["response_current"] = {} @@ -140,21 +141,24 @@ def commit(self): self.response = copy.deepcopy(response) self.result = copy.deepcopy(self.result_current) - msg = f"{caller}: Exiting dcnm_send_with_retry loop. success {success}. verb {self.verb}, path {self.path}." + msg = f"{caller}: Exiting dcnm_send_with_retry loop." + msg += f"success {success}. verb {self.verb}, path {self.path}." self.log.debug(msg) - msg = f"{caller}: self.response_current {json.dumps(self.response_current, indent=4, sort_keys=True)}" + msg = f"{caller}: self.response_current " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"{caller}: self.response {json.dumps(self.response, indent=4, sort_keys=True)}" + msg = f"{caller}: self.response " + msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"{caller}: self.result_current {json.dumps(self.result_current, indent=4, sort_keys=True)}" + msg = f"{caller}: self.result_current " + msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = ( - f"{caller}: self.result {json.dumps(self.result, indent=4, sort_keys=True)}" - ) + msg = f"{caller}: self.result " + msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) def _handle_response(self, response): @@ -242,6 +246,18 @@ def failed_result(self): """ return ImageUpgradeTaskResult(None).failed_result + @property + def path(self): + """ + Endpoint path for the REST request. + e.g. "/appcenter/cisco/ndfc/api/v1/...etc..." + """ + return self.properties.get("path") + + @path.setter + def path(self, value): + self.properties["path"] = value + @property def payload(self): """ @@ -386,3 +402,21 @@ def unit_test(self, value): msg += f"{method_name} must be a bool(). Got {value}." self.ansible_module.fail_json(msg, **self.failed_result) self.properties["unit_test"] = value + + @property + def verb(self): + """ + Verb for the REST request. + One of "GET", "POST", "PUT", "DELETE" + """ + return self.properties.get("verb") + + @verb.setter + def verb(self, value): + method_name = inspect.stack()[0][3] + if value not in self._valid_verbs: + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be one of {sorted(self._valid_verbs)}. " + msg += f"Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["verb"] = value From 99042cc7281ddbeafe53de1c9dd71061228f907f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 26 Feb 2024 13:38:16 -1000 Subject: [PATCH 278/300] Use do_not_raise() to verify that fail_json isn't called. Also: test_image_upgrade_switch_details.py: - Improve test docstrings switch_details.py (SwitchDetails class): - initialize self.path and self.verb in __init__() and use these in refresh() --- .../image_upgrade/switch_details.py | 11 +- .../test_image_upgrade_switch_details.py | 115 ++++++++++++------ 2 files changed, 86 insertions(+), 40 deletions(-) diff --git a/plugins/module_utils/image_upgrade/switch_details.py b/plugins/module_utils/image_upgrade/switch_details.py index 37a64d612..02507843f 100644 --- a/plugins/module_utils/image_upgrade/switch_details.py +++ b/plugins/module_utils/image_upgrade/switch_details.py @@ -58,7 +58,11 @@ def __init__(self, module): self.log.debug("ENTERED SwitchDetails()") self.endpoints = ApiEndpoints() + self.path = self.endpoints.switches_info.get("path") + self.verb = self.endpoints.switches_info.get("verb") + self.rest_send = RestSend(self.module) + self._init_properties() def _init_properties(self): @@ -75,11 +79,8 @@ def refresh(self): """ method_name = inspect.stack()[0][3] - path = self.endpoints.switches_info.get("path") - verb = self.endpoints.switches_info.get("verb") - - self.rest_send.verb = verb - self.rest_send.path = path + self.rest_send.verb = self.verb + self.rest_send.path = self.path self.rest_send.commit() msg = f"self.rest_send.response_current: {self.rest_send.response_current}" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 127790ae3..5d44f1fed 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -56,12 +56,19 @@ def test_image_upgrade_switch_details_00001(switch_details) -> None: Function - __init__ + Summary + Verify that the class attributes are initialized to expected values. + Test - Class attributes are initialized to expected values + - fail_json is not called """ - instance = switch_details + with does_not_raise(): + instance = switch_details assert isinstance(instance, SwitchDetails) assert instance.class_name == "SwitchDetails" + assert instance.verb == "GET" + assert instance.path == "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches" def test_image_upgrade_switch_details_00002(switch_details) -> None: @@ -69,13 +76,18 @@ def test_image_upgrade_switch_details_00002(switch_details) -> None: Function - _init_properties + Summary + Verify that the class properties are initialized to expected values. + Test - Class properties are initialized to expected values + - fail_json is not called """ - instance = switch_details - + with does_not_raise(): + instance = switch_details assert isinstance(instance.properties, dict) assert instance.properties.get("ip_address") is None + assert instance.properties.get("info") == {} assert instance.properties.get("response_data") == [] assert instance.properties.get("response") == [] assert instance.properties.get("response_current") == {} @@ -120,14 +132,27 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_switch_details_00021(monkeypatch, switch_details) -> None: """ Function - - refresh + - SwitchDetails.refresh + - SwitchDetails.ip_address.setter + - SwitchDetails.fabric_name + - SwitchDetails.hostname + - SwitchDetails.logical_name + - SwitchDetails.model + - SwitchDetails.platform + - SwitchDetails.role + - SwitchDetails.sserial_number + + Summary + Verify that, after refresh() is called, and the ip_address setter + property is set, the getter properties return values specific to the + ip_address that was set. Test - response_data is a dictionary - ip_address is set - getter properties will return values specific to ip_address + - fail_json is not called """ - instance = switch_details def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_switch_details_00021a" @@ -140,12 +165,17 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr( PATCH_SWITCH_DETAILS_REST_SEND_RESULT_CURRENT, {"success": True, "found": True} ) - - instance.refresh() + with does_not_raise(): + instance = switch_details + instance.refresh() assert isinstance(instance.response_data, list) - instance.ip_address = "172.22.150.110" + + with does_not_raise(): + instance.ip_address = "172.22.150.110" assert instance.hostname == "cvd-1111-bgw" - instance.ip_address = "172.22.150.111" + + with does_not_raise(): + instance.ip_address = "172.22.150.111" # We use the above IP address to test the remaining properties assert instance.fabric_name == "easy" assert instance.hostname == "cvd-1112-bgw" @@ -179,9 +209,13 @@ def test_image_upgrade_switch_details_00022( ) -> None: """ Function - - switch_details.refresh + - SwitchDetails.refresh - RestSend._handle_response + Summary + Verify that RestSend._handle_responmse() returns an appropriate result + when SwitchDetails.refresh() is called. + Test - test_image_upgrade_switch_details_00022a - 200 RETURN_CODE, MESSAGE == "OK" @@ -227,12 +261,15 @@ def test_image_upgrade_switch_details_00023( ) -> None: """ Function - - switch_details.refresh - - switch_details.ip_address - - switch_details._get + - SwitchDetails.refresh + - SwitchDetails.ip_address + - SwitchDetails._get + + Summary + Verify that SwitchDetails._get returns expected property values. Test - - _get returns correct property values + - _get returns property values consistent with the controller response. Description @@ -260,20 +297,25 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT, mock_rest_send_switch_details() ) - instance.refresh() - instance.ip_address = "172.22.150.110" + with does_not_raise(): + instance.refresh() + instance.ip_address = "172.22.150.110" assert instance._get(item) == expected def test_image_upgrade_switch_details_00024(monkeypatch, switch_details) -> None: """ Function - - switch_details.refresh - - switch_details.ip_address - - switch_details._get + - SwitchDetails.refresh + - SwitchDetails.ip_address + - SwitchDetails._get + + Summary + Verify that fail_json is called when SwitchDetails.ip_address does not exist + on the controller and a property associated with ip_address is queried. Test - - _get calls fail_json when switch_details.ip_address is unknown + - _get calls fail_json when SwitchDetails.ip_address is unknown Description SwitchDetails._get is called by all getter properties. @@ -282,8 +324,6 @@ def test_image_upgrade_switch_details_00024(monkeypatch, switch_details) -> None It returns the value of the requested property if the user has set a known ip_address. """ - instance = switch_details - def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_switch_details_00024a" return responses_switch_details(key) @@ -296,8 +336,10 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: match = "SwitchDetails._get: 1.1.1.1 does not exist " match += "on the controller." - instance.refresh() - instance.ip_address = "1.1.1.1" + with does_not_raise(): + instance = switch_details + instance.refresh() + instance.ip_address = "1.1.1.1" with pytest.raises(AnsibleFailJson, match=match): instance._get("hostName") @@ -305,9 +347,12 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_switch_details_00025(monkeypatch, switch_details) -> None: """ Function - - switch_details.refresh - - switch_details.ip_address - - switch_details._get + - SwitchDetails.refresh + - SwitchDetails.ip_address + - SwitchDetails._get + + Summary + Verify that fail_json is called when an unknown property name is queried. Test - _get calls fail_json when an unknown property name is queried @@ -317,8 +362,6 @@ def test_image_upgrade_switch_details_00025(monkeypatch, switch_details) -> None It raises AnsibleFailJson if the user has not set ip_address or if the ip_address is unknown, or if an unknown property name is queried. """ - instance = switch_details - def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_switch_details_00025a" return responses_switch_details(key) @@ -330,8 +373,10 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: match = "SwitchDetails._get: 172.22.150.110 does not have a key named FOO." - instance.refresh() - instance.ip_address = "172.22.150.110" + with does_not_raise(): + instance = switch_details + instance.refresh() + instance.ip_address = "172.22.150.110" with pytest.raises(AnsibleFailJson, match=match): instance._get("FOO") @@ -357,8 +402,8 @@ def test_image_upgrade_switch_details_00060( - return IP address, if set - return None, if not set """ - instance = switch_details - - if ip_address_is_set: - instance.ip_address = "1.2.3.4" + with does_not_raise(): + instance = switch_details + if ip_address_is_set: + instance.ip_address = "1.2.3.4" assert instance.ip_address == expected From 18ae3cdce01e05eb3812c81dfbb8cbd2b695b4bc Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 28 Feb 2024 09:10:13 -1000 Subject: [PATCH 279/300] ImageUpgradeTaskResult: 100% unit test coverage Also: test_image_upgrade_switch_details.py: Run thru black --- .../image_upgrade_task_result.py | 3 + .../dcnm_image_upgrade/image_upgrade_utils.py | 10 + ...image_upgrade_image_upgrade_task_result.py | 373 ++++++++++++++++++ .../test_image_upgrade_switch_details.py | 7 +- 4 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py diff --git a/plugins/module_utils/image_upgrade/image_upgrade_task_result.py b/plugins/module_utils/image_upgrade/image_upgrade_task_result.py index 0d5e28105..0518db6ca 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_task_result.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_task_result.py @@ -34,6 +34,8 @@ def __init__(self, ansible_module): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED ImageUpgradeTaskResult()") + # Used in did_anything_change() to determine if any diffs have been + # appended to the diff lists. self.diff_properties = {} self.diff_properties["diff_attach_policy"] = "attach_policy" self.diff_properties["diff_detach_policy"] = "detach_policy" @@ -41,6 +43,7 @@ def __init__(self, ansible_module): self.diff_properties["diff_stage"] = "stage" self.diff_properties["diff_upgrade"] = "upgrade" self.diff_properties["diff_validate"] = "validate" + # Used in failed_result() and module_result() to build the result dict() self.response_properties = {} self.response_properties["response_attach_policy"] = "attach_policy" self.response_properties["response_detach_policy"] = "detach_policy" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index 9d7f65e6c..9ad8f11aa 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -37,6 +37,8 @@ ImageUpgrade from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_task_result import \ + ImageUpgradeTaskResult from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_validate import \ ImageValidate from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.install_options import \ @@ -137,6 +139,14 @@ def image_upgrade_task_fixture(): return ImageUpgradeTask(MockAnsibleModule) +@pytest.fixture(name="image_upgrade_task_result") +def image_upgrade_task_result_fixture(): + """ + mock ImageUpgradeTaskResult + """ + return ImageUpgradeTaskResult(MockAnsibleModule) + + @pytest.fixture(name="image_validate") def image_validate_fixture(): """ diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py new file mode 100644 index 000000000..ba4452ea0 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py @@ -0,0 +1,373 @@ +# Copyright (c) 2024 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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +from typing import Any, Dict + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ + ImageUpgradeTaskResult +from .image_upgrade_utils import (does_not_raise, image_upgrade_task_result_fixture) + + +def test_image_upgrade_upgrade_task_result_00010(image_upgrade_task_result) -> None: + """ + Function + - ImageUpgradeTaskResult.__init__ + - ImageUpgradeTaskResult._build_properties + + Test + - Class attributes and properties are initialized to expected values + """ + instance = image_upgrade_task_result + assert isinstance(instance, ImageUpgradeTaskResult) + assert instance.class_name == "ImageUpgradeTaskResult" + assert isinstance(instance.diff_properties, dict) + assert instance.diff_attach_policy == [] + assert instance.diff_detach_policy == [] + assert instance.diff_issu_status == [] + assert instance.diff_stage == [] + assert instance.diff_upgrade == [] + assert instance.diff_validate == [] + assert isinstance(instance.response_properties, dict) + assert instance.response_attach_policy == [] + assert instance.response_detach_policy == [] + assert instance.response_issu_status == [] + assert instance.response_stage == [] + assert instance.response_upgrade == [] + assert instance.response_validate == [] + assert isinstance(instance.properties, dict) + + +@pytest.mark.parametrize( + "state, return_value", + [ + ("no_change", False), + ("attach_policy", True), + ("detach_policy", True), + ("issu_status", False), + ("stage", True), + ("upgrade", True), + ("validate", True), + ], +) +def test_image_upgrade_upgrade_task_result_00020( + image_upgrade_task_result, state, return_value +) -> None: + """ + Function + - ImageUpgradeTaskResult.__init__ + - ImageUpgradeTaskResult.did_anything_change + + Summary + Verify that did_anything_change: + - returns False when no changes have been made + - returns True when changes have been made to attach_policy, + detach_policy, stage, upgrade, or validate + - returns False when changes have been made to issu_status + """ + diff = {"diff": "diff"} + with does_not_raise(): + instance = image_upgrade_task_result + if state == "attach_policy": + instance.diff_attach_policy = diff + elif state == "detach_policy": + instance.diff_detach_policy = diff + elif state == "issu_status": + instance.diff_issu_status = diff + elif state == "stage": + instance.diff_stage = diff + elif state == "upgrade": + instance.diff_upgrade = diff + elif state == "validate": + instance.diff_validate = diff + elif state == "no_change": + pass + assert instance.did_anything_change() == return_value + + +MATCH_00030 = r"ImageUpgradeTaskResult\._verify_is_dict: value must be a dict\." + + +@pytest.mark.parametrize( + "value, expected", + [ + ({}, does_not_raise()), + ("not a dict", pytest.raises(AnsibleFailJson, match=MATCH_00030)), + ], +) +def test_image_upgrade_upgrade_task_result_00030( + image_upgrade_task_result, value, expected +) -> None: + """ + Function + - ImageUpgradeTaskResult._verify_is_dict + + Summary + Verify that _verify_is_dict does not call fail_json when value is a dict + and does call fail_json value is not a dict. + """ + with does_not_raise(): + instance = image_upgrade_task_result + with expected: + instance._verify_is_dict(value) + + +def test_image_upgrade_upgrade_task_result_00040(image_upgrade_task_result) -> None: + """ + Function + - ImageUpgradeTaskResult.failed_result + + Summary + Verify that failed_result returns a dict with expected values + """ + test_diff_keys = [ + "diff_attach_policy", + "diff_detach_policy", + "diff_issu_status", + "diff_stage", + "diff_upgrade", + "diff_validate", + ] + test_response_keys = [ + "response_attach_policy", + "response_detach_policy", + "response_issu_status", + "response_stage", + "response_upgrade", + "response_validate", + ] + with does_not_raise(): + instance = image_upgrade_task_result + result = instance.failed_result + assert isinstance(result, dict) + assert result["changed"] == False + assert result["failed"] == True + for key in test_diff_keys: + assert result["diff"][key] == [] + for key in test_response_keys: + assert result["response"][key] == [] + + +def test_image_upgrade_upgrade_task_result_00050(image_upgrade_task_result) -> None: + """ + Function + - ImageUpgradeTaskResult.module_result + + Summary + Verify that module_result returns a dict with expected values when + no changes have been made. + """ + test_keys = [ + "attach_policy", + "detach_policy", + "issu_status", + "stage", + "upgrade", + "validate", + ] + with does_not_raise(): + instance = image_upgrade_task_result + result = instance.module_result + assert isinstance(result, dict) + assert result["changed"] == False + for key in test_keys: + assert result["diff"][key] == [] + for key in test_keys: + assert result["response"][key] == [] + + +@pytest.mark.parametrize( + "state, changed", + [ + ("attach_policy", True), + ("detach_policy", True), + ("issu_status", False), + ("stage", True), + ("upgrade", True), + ("validate", True), + ], +) +def test_image_upgrade_upgrade_task_result_00051( + image_upgrade_task_result, state, changed +) -> None: + """ + Function + - ImageUpgradeTaskResult.module_result + - ImageUpgradeTaskResult.did_anything_change + - ImageUpgradeTaskResult.diff_* + - ImageUpgradeTaskResult.response_* + + Summary + Verify that module_result returns a dict with expected values when + changes have been made to each of the supported states. + + Test + - For non-query-state properties, "changed" should be True + - The diff should be a list containing the dict passed to + the state's diff property (e.g. diff_stage, diff_issu_status, etc) + - The response should be a list containing the dict passed to + the state's response property (e.g. response_stage, response_issu_status, etc) + - All other diffs should be empty lists + - All other responses should be empty lists + """ + test_key = state + test_keys = [ + "attach_policy", + "detach_policy", + "issu_status", + "stage", + "upgrade", + "validate", + ] + diff = {"diff": "diff"} + response = {"response": "response"} + + with does_not_raise(): + instance = image_upgrade_task_result + if state == "attach_policy": + instance.diff_attach_policy = diff + instance.response_attach_policy = response + elif state == "detach_policy": + instance.diff_detach_policy = diff + instance.response_detach_policy = response + elif state == "issu_status": + instance.diff_issu_status = diff + instance.response_issu_status = response + elif state == "stage": + instance.diff_stage = diff + instance.response_stage = response + elif state == "upgrade": + instance.diff_upgrade = diff + instance.response_upgrade = response + elif state == "validate": + instance.diff_validate = diff + instance.response_validate = response + result = instance.module_result + assert isinstance(result, dict) + assert result["changed"] == changed + for key in test_keys: + if key == test_key: + assert result["diff"][key] == [diff] + assert result["response"][key] == [response] + else: + assert result["diff"][key] == [] + assert result["response"][key] == [] + + +@pytest.mark.parametrize( + "state", + [ + ("attach_policy"), + ("detach_policy"), + ("issu_status"), + ("stage"), + ("upgrade"), + ("validate"), + ], +) +def test_image_upgrade_upgrade_task_result_00060( + image_upgrade_task_result, state +) -> None: + """ + Function + - ImageUpgradeTaskResult.module_result + - ImageUpgradeTaskResult.did_anything_change + - ImageUpgradeTaskResult.diff_* + + Summary + Verify that fail_json is called by instance.diff_* when the diff + is not a dict. + """ + diff = "not a dict" + match = r"ImageUpgradeTaskResult\._verify_is_dict: value must be a dict\." + with does_not_raise(): + instance = image_upgrade_task_result + with pytest.raises(AnsibleFailJson, match=match): + if state == "attach_policy": + instance.diff_attach_policy = diff + elif state == "detach_policy": + instance.diff_detach_policy = diff + elif state == "issu_status": + instance.diff_issu_status = diff + elif state == "stage": + instance.diff_stage = diff + elif state == "upgrade": + instance.diff_upgrade = diff + elif state == "validate": + instance.diff_validate = diff + else: + pass + + +@pytest.mark.parametrize( + "state", + [ + ("attach_policy"), + ("detach_policy"), + ("issu_status"), + ("stage"), + ("upgrade"), + ("validate"), + ], +) +def test_image_upgrade_upgrade_task_result_00070( + image_upgrade_task_result, state +) -> None: + """ + Function + - ImageUpgradeTaskResult.module_result + - ImageUpgradeTaskResult.did_anything_change + - ImageUpgradeTaskResult.response_* + + Summary + Verify that fail_json is called by instance.response_* when the response + is not a dict. + """ + response = "not a dict" + match = r"ImageUpgradeTaskResult\._verify_is_dict: value must be a dict\." + with does_not_raise(): + instance = image_upgrade_task_result + with pytest.raises(AnsibleFailJson, match=match): + if state == "attach_policy": + instance.response_attach_policy = response + elif state == "detach_policy": + instance.response_detach_policy = response + elif state == "issu_status": + instance.response_issu_status = response + elif state == "stage": + instance.response_stage = response + elif state == "upgrade": + instance.response_upgrade = response + elif state == "validate": + instance.response_validate = response + else: + pass diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 5d44f1fed..7a1d6d75b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -68,7 +68,10 @@ def test_image_upgrade_switch_details_00001(switch_details) -> None: assert isinstance(instance, SwitchDetails) assert instance.class_name == "SwitchDetails" assert instance.verb == "GET" - assert instance.path == "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches" + assert ( + instance.path + == "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches" + ) def test_image_upgrade_switch_details_00002(switch_details) -> None: @@ -324,6 +327,7 @@ def test_image_upgrade_switch_details_00024(monkeypatch, switch_details) -> None It returns the value of the requested property if the user has set a known ip_address. """ + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_switch_details_00024a" return responses_switch_details(key) @@ -362,6 +366,7 @@ def test_image_upgrade_switch_details_00025(monkeypatch, switch_details) -> None It raises AnsibleFailJson if the user has not set ip_address or if the ip_address is unknown, or if an unknown property name is queried. """ + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_switch_details_00025a" return responses_switch_details(key) From c3d8ae27f9aa240c3dcc6ff7498e4e17fcd14769 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 28 Feb 2024 09:16:51 -1000 Subject: [PATCH 280/300] Fix PEP8 sanity issue --- .../test_image_upgrade_image_upgrade_task_result.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py index ba4452ea0..a7319bcc0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py @@ -169,8 +169,8 @@ def test_image_upgrade_upgrade_task_result_00040(image_upgrade_task_result) -> N instance = image_upgrade_task_result result = instance.failed_result assert isinstance(result, dict) - assert result["changed"] == False - assert result["failed"] == True + assert result["changed"] is False + assert result["failed"] is True for key in test_diff_keys: assert result["diff"][key] == [] for key in test_response_keys: @@ -198,7 +198,7 @@ def test_image_upgrade_upgrade_task_result_00050(image_upgrade_task_result) -> N instance = image_upgrade_task_result result = instance.module_result assert isinstance(result, dict) - assert result["changed"] == False + assert result["changed"] is False for key in test_keys: assert result["diff"][key] == [] for key in test_keys: From 838b1d7eec391d3588c445379c329f883adaa539 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 28 Feb 2024 14:31:18 -1000 Subject: [PATCH 281/300] ImagePolicyAction: 100% unit test coverage Also: ImagePolicyAction._attach_policy: fix KeyError in fail_json message. ImagePolicyAction.query_result: remove checking for value == dict since we don't know 100% what the controller will return. ImagePolicyAction.serial_numbers: enforce list must be at least one element. --- .../image_upgrade/image_policy_action.py | 20 +- ...image_upgrade_responses_ImagePolicies.json | 144 +++++- ...e_upgrade_responses_ImagePolicyAction.json | 52 +- ...image_upgrade_responses_SwitchDetails.json | 46 +- ...e_upgrade_responses_SwitchIssuDetails.json | 308 +++++++++++- .../test_image_upgrade_image_policy_action.py | 458 ++++++++++++++++-- ...image_upgrade_image_upgrade_task_result.py | 4 +- 7 files changed, 977 insertions(+), 55 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_policy_action.py b/plugins/module_utils/image_upgrade/image_policy_action.py index 7fdd92526..0ae8c2754 100644 --- a/plugins/module_utils/image_upgrade/image_policy_action.py +++ b/plugins/module_utils/image_upgrade/image_policy_action.py @@ -161,7 +161,7 @@ def validate_request(self): if self.image_policies.name is None: msg = f"{self.class_name}.{method_name}: " msg += f"policy {self.policy_name} does not exist on " - msg += "the controller" + msg += "the controller." self.module.fail_json(msg) for serial_number in self.serial_numbers: @@ -223,10 +223,15 @@ def _attach_policy(self): payload["mappingList"] = self.payloads self.dcnm_send_with_retry(self.verb, self.path, payload) + msg = f"result_current: {json.dumps(self.result_current, indent=4)}" + self.log.debug(msg) + msg = f"response_current: {json.dumps(self.response_current, indent=4)}" + self.log.debug(msg) + if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " - msg += f"to switch {payload['ipAddr']}." + msg += f"to switch. Payload: {payload}." self.module.fail_json(msg, **self.failed_result) for payload in self.payloads: @@ -320,12 +325,6 @@ def query_result(self): @query_result.setter def query_result(self, value): - method_name = inspect.stack()[0][3] - if isinstance(value, dict): - msg = f"{self.class_name}.{method_name}: " - msg += "instance.query_result must be a dict. " - msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) self.properties["query_result"] = value @property @@ -382,4 +381,9 @@ def serial_numbers(self, value): msg += "python list of switch serial numbers. " msg += f"Got {value}." self.module.fail_json(msg, **self.failed_result) + if len(value) == 0: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.serial_numbers must contain at least one " + msg += "switch serial number." + self.module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json index 4294ba34d..3fe92bb5b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicies.json @@ -219,6 +219,18 @@ "message": "" } }, + "test_image_upgrade_image_policy_action_00014a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } + }, "test_image_upgrade_image_policy_action_00020a": { "RETURN_CODE": 200, "METHOD": "GET", @@ -245,7 +257,137 @@ "message": "" } }, - "test_image_upgrade_image_policy_action_00021a": { + "test_image_upgrade_image_policy_action_00030a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00031a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00040a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00041a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00050a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "10.2.(5) with EPLD", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "ref_count": 10 + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00051a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json index 59c2203a3..e0d8b44a0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImagePolicyAction.json @@ -1,9 +1,59 @@ { - "test_image_upgrade_image_policy_action_00021a": { + "test_image_upgrade_image_policy_action_00030a": { "RETURN_CODE": 200, "METHOD": "DELETE", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy?serialNumber=FDO21120U5D,FDO211218GC,FOX2109PGD0", "MESSAGE": "OK", "DATA": "Successfully detach the policy from device." + }, + "test_image_upgrade_image_policy_action_00031a": { + "RETURN_CODE": 500, + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy?serialNumber=FDO21120U5D,FDO211218GC,FOX2109PGD0", + "MESSAGE": "NOK", + "DATA": "Failed to detach the policy from device." + }, + "test_image_upgrade_image_policy_action_00040a": { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy", + "MESSAGE": "OK", + "DATA": "[cvd-1313-leaf:Success]" + }, + "test_image_upgrade_image_policy_action_00041a": { + "RETURN_CODE": 500, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy", + "MESSAGE": "NOK", + "DATA": "[cvd-1313-leaf:Failed to attach policy to device.]" + }, + "test_image_upgrade_image_policy_action_00050a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443//appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/KR5M", + "MESSAGE": "OK", + "DATA": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": false, + "fabricPolicyName": "", + "unInstall": false + } + ] + }, + "test_image_upgrade_image_policy_action_00051a": { + "RETURN_CODE": 500, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443//appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/KR5M", + "MESSAGE": "NOK", + "DATA": [] } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json index 780d62bf2..b637979df 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json @@ -120,7 +120,51 @@ } ] }, - "test_image_upgrade_image_policy_action_00021a": { + "test_image_upgrade_image_policy_action_00030a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [{}] + }, + "test_image_upgrade_image_policy_action_00031a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [{}] + }, + "test_image_upgrade_image_policy_action_00040a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [{}] + }, + "test_image_upgrade_image_policy_action_00050a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [{}] + }, + "test_image_upgrade_image_policy_action_00051a": { "TEST_NOTES": [ "RETURN_CODE: 200", "MESSAGE: OK" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 104b07afb..9f30da082 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -2402,6 +2402,57 @@ "message": "" } }, + "test_image_upgrade_image_policy_action_00014a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "none", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "TEST_UNKNOWN_PLATFORM", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, "test_image_upgrade_image_policy_action_00020a": { "RETURN_CODE": 200, "METHOD": "GET", @@ -2453,7 +2504,262 @@ "message": "" } }, - "test_image_upgrade_image_policy_action_00021a": { + "test_image_upgrade_image_policy_action_00030a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00031a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00040a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00041a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00050a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO2112189M", + "deviceName": "cvd-2313-leaf", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "none", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-06 03:43", + "model": "N9K-C93180YC-EX", + "fabric": "hard", + "ipAddress": "172.22.150.108", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 100, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 39890, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.108", + "peer": "null", + "vdc_id": -1, + "sys_name": "cvd-2313-leaf", + "id": 2, + "group": "hard", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, + "test_image_upgrade_image_policy_action_00051a": { "RETURN_CODE": 200, "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 0d6393251..72cde8064 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -59,7 +59,7 @@ def test_image_upgrade_image_policy_action_00001(image_policy_action) -> None: """ Function - - __init__ + - ImagePolicyAction.__init__ Test - Class attributes initialized to expected values @@ -80,7 +80,7 @@ def test_image_upgrade_image_policy_action_00001(image_policy_action) -> None: def test_image_upgrade_image_policy_action_00002(image_policy_action) -> None: """ Function - - _init_properties + - ImagePolicyAction._init_properties Test - Class properties are initialized to expected values @@ -102,7 +102,7 @@ def test_image_upgrade_image_policy_action_00003( ) -> None: """ Function - - build_payload + - ImagePolicyAction.build_payload Test - fail_json is not called @@ -143,7 +143,7 @@ def test_image_upgrade_image_policy_action_00004( ) -> None: """ Function - - build_payload + - ImagePolicyAction.build_payload Test - fail_json is called since deviceName is null in the issu_details_by_serial_number response @@ -182,14 +182,14 @@ def test_image_upgrade_image_policy_action_00010( ) -> None: """ Function - - validate_request + - ImagePolicyAction.validate_request Test - fail_json is called because image_policy_action.action is None - The error message is matched Description - validate_request performs a number of validations prior to calling commit + validate_request performs a number of validations prior to calling commit. If any of these validations fail, the function calls fail_json with a validation-specific error message. """ @@ -222,14 +222,14 @@ def test_image_upgrade_image_policy_action_00011( ) -> None: """ Function - - validate_request + - ImagePolicyAction.validate_request Test - fail_json is called because image_policy_action.policy_name is None - The error message is matched Description - validate_request performs a number of validations prior to calling commit + validate_request performs a number of validations prior to calling commit. If any of these validations fail, the function calls fail_json with a validation-specific error message. """ @@ -261,7 +261,7 @@ def test_image_upgrade_image_policy_action_00012( ) -> None: """ Function - - validate_request + - ImagePolicyAction.validate_request Test - fail_json is called for action == attach because @@ -273,7 +273,7 @@ def test_image_upgrade_image_policy_action_00012( - The error message, if any, is matched Description - validate_request performs a number of validations prior to calling commit + validate_request performs a number of validations prior to calling commit, If any of these validations fail, the function calls fail_json with a validation-specific error message. """ @@ -291,7 +291,7 @@ def test_image_upgrade_image_policy_action_00013( ) -> None: """ Function - - validate_request + - ImagePolicyAction.validate_request Test - fail_json is called because policy KR5M supports playform N9K/N3K @@ -300,8 +300,7 @@ def test_image_upgrade_image_policy_action_00013( - The error message is matched Description - validate_request performs a number of validations prior to calling commit - validate_request performs a number of validations prior to calling commit + validate_request performs a number of validations prior to calling commit. If any of these validations fail, the function calls fail_json with a validation-specific error message. """ @@ -333,12 +332,58 @@ def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: instance.validate_request() +def test_image_upgrade_image_policy_action_00014( + monkeypatch, image_policy_action, issu_details_by_serial_number, image_policies +) -> None: + """ + Function + - ImagePolicyAction.validate_request + + Summary + fail_json is called because policy KR5M does not exist on the controller + + Test + - fail_json is called because ImagePolicies returns no policies + - The error message is matched + + Description + validate_request performs a number of validations prior to calling commit. + If any of these validations fail, the function calls fail_json with a + validation-specific error message. + """ + key = "test_image_upgrade_image_policy_action_00014a" + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + return responses_image_policies(key) + + monkeypatch.setattr( + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details + ) + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + + instance = image_policy_action + instance.switch_issu_details = issu_details_by_serial_number + instance.image_policies = image_policies + instance.action = "attach" + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] + + match = r"ImagePolicyAction.validate_request: " + match += r"policy KR5M does not exist on the controller\." + + with pytest.raises(AnsibleFailJson, match=match): + instance.validate_request() + + def test_image_upgrade_image_policy_action_00020( monkeypatch, image_policy_action ) -> None: """ Function - - commit + - ImagePolicyAction.commit Test - fail_json is called because action is unknown @@ -378,15 +423,21 @@ def mock_validate_request(*args) -> None: instance.commit() -def test_image_upgrade_image_policy_action_00021( +def test_image_upgrade_image_policy_action_00030( monkeypatch, image_policy_action ) -> None: """ Function - - commit + - ImagePolicyAction.commit + - ImagePolicyAction._detach_policy + + Summary + Verify that commit behaves as expected when action is "detach" + and ImagePolicyAction receives a success (200) response + from the controller. Test - - action is "detach", so ImagePolicyAction._detach_policy is called + - ImagePolicyAction._detach_policy is called - commit is successful given a 200 response from the controller in ImagePolicyAction._detach_policy - ImagePolicyAction.response contains RESULT_CODE 200 @@ -398,7 +449,7 @@ def test_image_upgrade_image_policy_action_00021( action == "detach" : _detach_policy action == "query" : _query_policy """ - key = "test_image_upgrade_image_policy_action_00021a" + key = "test_image_upgrade_image_policy_action_00030a" def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: return responses_image_policies(key) @@ -433,38 +484,296 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: assert instance.result_current.get("changed") is True +def test_image_upgrade_image_policy_action_00031( + monkeypatch, image_policy_action +) -> None: + """ + Function + - ImagePolicyAction.commit + - ImagePolicyAction._detach_policy + + Summary + Verify that commit behaves as expected when action is "detach" + and ImagePolicyAction receives a failure (500) response + from the controller. + + Test + - ImagePolicyAction._detach_policy is called + - commit is unsuccessful given a 500 response from the controller in + ImagePolicyAction._detach_policy + - fail_json is called and the error message is matched + """ + key = "test_image_upgrade_image_policy_action_00031a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + return responses_image_policies(key) + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: + return responses_image_policy_action(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + monkeypatch.setattr( + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details + ) + + with does_not_raise(): + instance = image_policy_action + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] + instance.action = "detach" + instance.unit_test = True + monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send_image_upgrade_common) + + match = r"ImagePolicyAction\._detach_policy: " + match += r"Bad result when detaching policy KR5M " + match += r"from the following device\(s\):" + + with pytest.raises(AnsibleFailJson, match=match): + instance.commit() + + +def test_image_upgrade_image_policy_action_00040( + monkeypatch, image_policy_action +) -> None: + """ + Function + - ImagePolicyAction.commit + - ImagePolicyAction._attach_policy + + Summary + Verify that commit behaves as expected when action is "attach" + and ImagePolicyAction receives a success (200) response + from the controller. + + Test + - ImagePolicyAction._attach_policy is called + - commit is successful given a 200 response from the controller in + ImagePolicyAction._attach_policy + - ImagePolicyAction.response contains RESULT_CODE 200 + + Description + commit calls validate_request() and then calls one of the following + functions based on the value of action: + action == "attach" : _attach_policy + action == "detach" : _detach_policy + action == "query" : _query_policy + """ + key = "test_image_upgrade_image_policy_action_00040a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + return responses_image_policies(key) + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_upgrade_common(*args, **kwargs) -> Dict[str, Any]: + return responses_image_policy_action(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + monkeypatch.setattr( + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details + ) + + instance = image_policy_action + monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send_image_upgrade_common) + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] + instance.action = "attach" + + instance.commit() + assert isinstance(instance.response_current, dict) + assert instance.response_current.get("RETURN_CODE") == 200 + assert instance.response_current.get("METHOD") == "POST" + assert instance.response_current.get("MESSAGE") == "OK" + assert instance.response_current.get("DATA") == "[cvd-1313-leaf:Success]" + assert instance.result_current.get("success") is True + assert instance.result_current.get("changed") is True + + +def test_image_upgrade_image_policy_action_00041( + monkeypatch, image_policy_action +) -> None: + """ + Function + - ImagePolicyAction.commit + - ImagePolicyAction._attach_policy + + Summary + Verify that commit behaves as expected when action is "attach" + and ImagePolicyAction receives a failure (500) response + from the controller. + + Test + - ImagePolicyAction._attach_policy is called + - commit is unsuccessful given a 500 response from the controller in + ImagePolicyAction._attach_policy + - ImagePolicyAction.response contains RESULT_CODE 500 + """ + key = "test_image_upgrade_image_policy_action_00041a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + return responses_image_policies(key) + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_upgrade_common(*args, **kwargs) -> Dict[str, Any]: + return responses_image_policy_action(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + monkeypatch.setattr( + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details + ) + with does_not_raise(): + instance = image_policy_action + monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send_image_upgrade_common) + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] + instance.action = "attach" + instance.unit_test = True + + match = r"ImagePolicyAction\._attach_policy: " + match += r"Bad result when attaching policy KR5M to switch\. Payload:" + with pytest.raises(AnsibleFailJson, match=match): + instance.commit() + + +def test_image_upgrade_image_policy_action_00050( + monkeypatch, image_policy_action +) -> None: + """ + Function + - ImagePolicyAction.commit + - ImagePolicyAction._query_policy + + Summary + Verify that commit behaves as expected when action is "query" + and ImagePolicyAction receives a success (200) response + from the controller. + + Test + - ImagePolicyAction._query_policy is called + - commit is successful given a 200 response from the controller in + ImagePolicyAction._query_policy + - ImagePolicyAction.response contains RESULT_CODE 200 + + Description + commit calls validate_request() and then calls one of the following + functions based on the value of action: + action == "attach" : _attach_policy + action == "detach" : _detach_policy + action == "query" : _query_policy + """ + key = "test_image_upgrade_image_policy_action_00050a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + return responses_image_policies(key) + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: + return responses_image_policy_action(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + monkeypatch.setattr( + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details + ) + + instance = image_policy_action + monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send_image_upgrade_common) + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] + instance.action = "query" + + instance.commit() + assert isinstance(instance.response_current, dict) + assert instance.response_current.get("RETURN_CODE") == 200 + assert instance.response_current.get("METHOD") == "GET" + assert instance.response_current.get("MESSAGE") == "OK" + assert instance.result_current.get("success") is True + assert instance.result_current.get("found") is True + + +def test_image_upgrade_image_policy_action_00051( + monkeypatch, image_policy_action +) -> None: + """ + Function + - ImagePolicyAction.commit + - ImagePolicyAction._query_policy + + Summary + Verify that commit behaves as expected when action is "query" + and ImagePolicyAction receives a failure (500) response + from the controller. + + Test + - fail_json is called and the error message is matched + """ + key = "test_image_upgrade_image_policy_action_00051a" + + def mock_dcnm_send_image_policies(*args) -> Dict[str, Any]: + return responses_image_policies(key) + + def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: + return responses_image_policy_action(key) + + monkeypatch.setattr(DCNM_SEND_IMAGE_POLICIES, mock_dcnm_send_image_policies) + monkeypatch.setattr( + DCNM_SEND_SWITCH_ISSU_DETAILS, mock_dcnm_send_switch_issu_details + ) + + instance = image_policy_action + monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send_image_upgrade_common) + instance.policy_name = "KR5M" + instance.serial_numbers = ["FDO2112189M"] + instance.action = "query" + instance.unit_test = True + + match = r"ImagePolicyAction\._query_policy: " + match += r"Bad result when querying image policy KR5M\." + with pytest.raises(AnsibleFailJson, match=match): + instance.commit() + + MATCH_00060 = "ImagePolicyAction.action: instance.action must be " MATCH_00060 += "one of attach,detach,query. Got FOO." @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - ("attach", "attach"), - ("detach", "detach"), - ("query", "query"), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060)), + ("attach", does_not_raise(), False), + ("detach", does_not_raise(), False), + ("query", does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060), True), ], ) def test_image_upgrade_image_policy_action_00060( - image_policy_action, value, expected + image_policy_action, value, expected, raise_flag ) -> None: """ Function - - action setter + - ImagePolicyAction.action setter Test - Expected values are set - fail_json is called when value is not a valid action - fail_json error message is matched """ - instance = image_policy_action - if value == "FOO": - with expected: - instance.action = value - else: + with does_not_raise(): + instance = image_policy_action + with expected: instance.action = value - assert instance.action == expected + if not raise_flag: + assert instance.action == value MATCH_00061 = "ImagePolicyAction.serial_numbers: instance.serial_numbers " @@ -472,28 +781,93 @@ def test_image_upgrade_image_policy_action_00060( @pytest.mark.parametrize( - "value, expected", + "value, expected, raise_flag", [ - (["FDO2112189M", "FDO21120U5D"], ["FDO2112189M", "FDO21120U5D"]), - ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00061)), + (["FDO2112189M", "FDO21120U5D"], does_not_raise(), False), + ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00061), True), ], ) def test_image_upgrade_image_policy_action_00061( - image_policy_action, value, expected + image_policy_action, value, expected, raise_flag ) -> None: """ Function - - serial_numbers setter + - ImagePolicyAction.serial_numbers setter Test - fail_json is not called with value is a list - fail_json is called when value is not a list - fail_json error message is matched """ - instance = image_policy_action - if value == "FOO": - with expected: - instance.serial_numbers = value - else: + with does_not_raise(): + instance = image_policy_action + with expected: instance.serial_numbers = value - assert instance.serial_numbers == expected + if not raise_flag: + assert instance.serial_numbers == value + + +def test_image_upgrade_image_policy_action_00062(image_policy_action) -> None: + """ + Function + - ImagePolicyAction.serial_numbers setter + + Test + - fail_json is called when value is an empty list + - fail_json error message is matched + """ + with does_not_raise(): + instance = image_policy_action + match = r"ImagePolicyAction\.serial_numbers: instance.serial_numbers " + match += r"must contain at least one switch serial number\." + with pytest.raises(AnsibleFailJson, match=match): + instance.serial_numbers = [] + + +MATCH_00070 = r"ImagePolicyAction\.query_result: instance.query_result must be a dict\." + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + ({"found": "true", "success": "true"}, does_not_raise(), False), + ("FOO", does_not_raise(), False), + ], +) +def test_image_upgrade_image_policy_action_00070( + image_policy_action, value, expected, raise_flag +) -> None: + """ + Function + - ImagePolicyAction.query_result setter + + Summary + Verify correct behavior of ImagePolicyAction.query_result + + Test + - fail_json is never called + """ + with does_not_raise(): + instance = image_policy_action + with expected: + instance.query_result = value + if not raise_flag: + assert instance.query_result == value + + +def test_image_upgrade_image_policy_action_00080(image_policy_action) -> None: + """ + Function + - ImagePolicyAction.diff_null getter + + Summary + Verify ImagePolicyAction.diff_null returns the expected value + """ + with does_not_raise(): + instance = image_policy_action + diff_null = instance.diff_null + assert diff_null["action"] is None + assert diff_null["ip_address"] is None + assert diff_null["logical_name"] is None + assert diff_null["policy"] is None + assert diff_null["serial_number"] is None diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py index a7319bcc0..cc95369fe 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py @@ -35,7 +35,9 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ ImageUpgradeTaskResult -from .image_upgrade_utils import (does_not_raise, image_upgrade_task_result_fixture) + +from .image_upgrade_utils import (does_not_raise, + image_upgrade_task_result_fixture) def test_image_upgrade_upgrade_task_result_00010(image_upgrade_task_result) -> None: From c2621cb5ab796526559a623a5e118761de669172 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 28 Feb 2024 16:21:43 -1000 Subject: [PATCH 282/300] ImageValidate: remove unused unit test We moved initialization of self.path and self.verb to __init__(), which is covered by test_image_upgrade_validate_00001 Hence, we no longer need test_image_upgrade_validate_00021 --- ...image_upgrade_responses_ImageValidate.json | 17 ---- ...e_upgrade_responses_SwitchIssuDetails.json | 90 ------------------- .../test_image_upgrade_image_validate.py | 38 -------- 3 files changed, 145 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json index ee9557d91..2a9c7b9c3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json @@ -14,23 +14,6 @@ "message": "" } }, - "test_image_upgrade_validate_00021a": { - "TEST_NOTES": [ - "Needed only for the 200 return code", - "RETURN_CODE == 200", - "MESSAGE == OK" - ], - "RETURN_CODE": 200, - "METHOD": "POST", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - ], - "message": "" - } - }, "test_image_upgrade_validate_00023a": { "TEST_NOTES": [ "RETURN_CODE == 501", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 9f30da082..2784bd138 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -1910,96 +1910,6 @@ "message": "" } }, - "test_image_upgrade_validate_00021a": { - "RETURN_CODE": 200, - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", - "MESSAGE": "OK", - "DATA": { - "status": "SUCCESS", - "lastOperDataObject": [ - { - "serialNumber": "FDO21120U5D", - "deviceName": "leaf1", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Success", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-19 02:20", - "model": "N9K-C93180YC-EX", - "fabric": "easy", - "ipAddress": "172.22.150.102", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 145740, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.102", - "peer": "null", - "vdc_id": -1, - "sys_name": "leaf1", - "id": 1, - "group": "easy", - "fcoEEnabled": "False", - "mds": "False" - }, - { - "serialNumber": "FDO2112189M", - "deviceName": "cvd-2313-leaf", - "version": "10.2(5)", - "policy": "KR5M", - "status": "In-Sync", - "reason": "Upgrade", - "imageStaged": "Failed", - "validated": "Success", - "upgrade": "Success", - "upgGroups": "null", - "mode": "Normal", - "systemMode": "Normal", - "vpcRole": "null", - "vpcPeer": "null", - "role": "leaf", - "lastUpgAction": "2023-Oct-06 03:43", - "model": "N9K-C93180YC-EX", - "fabric": "hard", - "ipAddress": "172.22.150.108", - "issuAllowed": "", - "statusPercent": 100, - "imageStagedPercent": 100, - "validatedPercent": 100, - "upgradePercent": 100, - "modelType": 0, - "vdcId": 0, - "ethswitchid": 39890, - "platform": "N9K", - "vpc_role": "null", - "ip_address": "172.22.150.108", - "peer": "null", - "vdc_id": -1, - "sys_name": "cvd-2313-leaf", - "id": 2, - "group": "hard", - "fcoEEnabled": "False", - "mds": "False" - } - ], - "message": "" - } - }, "test_image_upgrade_validate_00023a": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 705f50e2d..446bb1a28 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -407,44 +407,6 @@ def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: assert "FDO2112189M" not in instance.serial_numbers_done -def test_image_upgrade_validate_00021(monkeypatch, image_validate) -> None: - """ - Function - - commit - - Test - - ImageValidate.verb is set to POST - - ImageValidate.path is set to: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image - """ - - # Needed only for the 200 return code - def mock_rest_send_image_validate(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_upgrade_validate_00021a" - return responses_image_validate(key) - - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_upgrade_validate_00021a" - return responses_switch_issu_details(key) - - monkeypatch.setattr( - PATCH_IMAGE_VALIDATE_REST_SEND_COMMIT, mock_rest_send_image_validate - ) - monkeypatch.setattr( - PATCH_IMAGE_VALIDATE_REST_SEND_RESULT_CURRENT, {"success": True} - ) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - - module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" - module_path += "stagingmanagement/validate-image" - - instance = image_validate - instance.serial_numbers = ["FDO21120U5D"] - instance.commit() - assert instance.path == module_path - assert instance.verb == "POST" - - def test_image_upgrade_validate_00022(image_validate) -> None: """ Function From e7a57c247abcd72d35c65a813dbc670bf310294d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 08:07:37 -1000 Subject: [PATCH 283/300] SwitchDetails: 100% unit test coverage Also: SwitchDetails.info: self.propertiies should be self.properties test_image_upgrade_switch_details.py: Fix a few docstring typos --- .../image_upgrade/switch_details.py | 2 +- ...image_upgrade_responses_SwitchDetails.json | 26 +++++++ .../test_image_upgrade_switch_details.py | 70 ++++++++++++++++++- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/image_upgrade/switch_details.py b/plugins/module_utils/image_upgrade/switch_details.py index 02507843f..d79dcb312 100644 --- a/plugins/module_utils/image_upgrade/switch_details.py +++ b/plugins/module_utils/image_upgrade/switch_details.py @@ -190,7 +190,7 @@ def info(self): NOTE: Keyed on ip_address """ - return self.propertiies["info"] + return self.properties["info"] @property def platform(self): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json index b637979df..7d21b2906 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchDetails.json @@ -120,6 +120,32 @@ } ] }, + "test_image_upgrade_switch_details_00030a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "model: null", + "DATA: key/values removed which are not needed by the test" + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "MESSAGE": "OK", + "DATA": [ + { + "ipAddress": "172.22.150.110", + "hostName": "cvd-1111-bgw" + }, + { + "fabricName": "easy", + "hostName": "cvd-1112-bgw", + "ipAddress": "172.22.150.111", + "logicalName": "cvd-1112-bgw", + "model": null, + "serialNumber": "FOX2109PGD1", + "switchRole": "border gateway" + } + ] + }, "test_image_upgrade_image_policy_action_00030a": { "TEST_NOTES": [ "RETURN_CODE: 200", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 7a1d6d75b..3916fe1a1 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -139,11 +139,12 @@ def test_image_upgrade_switch_details_00021(monkeypatch, switch_details) -> None - SwitchDetails.ip_address.setter - SwitchDetails.fabric_name - SwitchDetails.hostname + - SwitchDetails.info - SwitchDetails.logical_name - SwitchDetails.model - SwitchDetails.platform - SwitchDetails.role - - SwitchDetails.sserial_number + - SwitchDetails.serial_number Summary Verify that, after refresh() is called, and the ip_address setter @@ -188,6 +189,8 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: assert instance.platform == "N9K" assert instance.role == "border gateway" assert instance.serial_number == "FOX2109PGD1" + assert "172.22.150.110" in instance.info.keys() + assert instance.info["172.22.150.110"]["hostName"] == "cvd-1111-bgw" MATCH_00022 = "Unable to retrieve switch information from the controller." @@ -216,7 +219,7 @@ def test_image_upgrade_switch_details_00022( - RestSend._handle_response Summary - Verify that RestSend._handle_responmse() returns an appropriate result + Verify that RestSend._handle_response() returns an appropriate result when SwitchDetails.refresh() is called. Test @@ -386,6 +389,66 @@ def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: instance._get("FOO") +def test_image_upgrade_switch_details_00026(switch_details) -> None: + """ + Function + - SwitchDetails.fabric_name + - SwitchDetails._get + + Summary + Verify that SwitchDetails.fabric_name calls SwitchDetails._get() + which then calls fail_json when ip_address has not been set. + + Test + - _get calls fail_json when ip_address is None + + Description + SwitchDetails._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set ip_address or if + the ip_address is unknown, or if an unknown property name is queried. + """ + match = r"SwitchDetails\._get: " + match += r"set instance\.ip_address before accessing property fabricName\." + + with does_not_raise(): + instance = switch_details + with pytest.raises(AnsibleFailJson, match=match): + instance.fabric_name + + +def test_image_upgrade_switch_details_00030(monkeypatch, switch_details) -> None: + """ + Function + - SwitchDetails.platform + + Summary + Verify that, SwitchDetails.platform returns None if SwitchDetails.model is None. + + Test + - platform returns None + - fail_json is not called + """ + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: + key = "test_image_upgrade_switch_details_00030a" + return responses_switch_details(key) + + monkeypatch.setattr(REST_SEND_SWITCH_DETAILS, mock_rest_send_switch_details) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESPONSE_CURRENT, mock_rest_send_switch_details() + ) + monkeypatch.setattr( + PATCH_SWITCH_DETAILS_REST_SEND_RESULT_CURRENT, {"success": True, "found": True} + ) + with does_not_raise(): + instance = switch_details + instance.refresh() + + with does_not_raise(): + instance.ip_address = "172.22.150.111" + platform = instance.platform + assert platform is None + + # setters @@ -403,6 +466,9 @@ def test_image_upgrade_switch_details_00060( Function - ip_address.setter + Summary + Verify proper behavior of ip_address setter + Test - return IP address, if set - return None, if not set From 650239854db92c1cb00da8bd99aba08bd6c19bac Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 08:56:05 -1000 Subject: [PATCH 284/300] SwitchIssuDetailsBy*: 100% unit test coverage --- .../image_upgrade/switch_issu_details.py | 2 +- ...rade_switch_issu_details_by_device_name.py | 77 +++++++++++---- ...grade_switch_issu_details_by_ip_address.py | 94 ++++++++++++++----- ...de_switch_issu_details_by_serial_number.py | 94 ++++++++++++++----- 4 files changed, 200 insertions(+), 67 deletions(-) diff --git a/plugins/module_utils/image_upgrade/switch_issu_details.py b/plugins/module_utils/image_upgrade/switch_issu_details.py index 612e8707d..9bc924eb4 100644 --- a/plugins/module_utils/image_upgrade/switch_issu_details.py +++ b/plugins/module_utils/image_upgrade/switch_issu_details.py @@ -799,7 +799,7 @@ def _get(self, item): def filtered_data(self): """ Return a dictionary of the switch matching self.serial_number. - Return None if the switch does not exist in NDFC. + Return None if the switch does not exist on the controller. """ return self.data_subclass.get(self.filter) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index 2c86fa4e1..7cf3a54d0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -46,7 +46,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00001( ) -> None: """ Function - - __init__ + - SwitchIssuDetailsByDeviceName.__init__ Test - fail_json is not called @@ -62,7 +62,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00002( ) -> None: """ Function - - _init_properties + - SwitchIssuDetailsByDeviceName._init_properties Test - Class properties initialized to expected values @@ -89,7 +89,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00020( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByDeviceName.refresh Test - instance.response is a dict @@ -114,7 +114,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00021( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByDeviceName.refresh Test - Properties are set based on device_name @@ -187,6 +187,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: # NOTE: Values are synthesized in the response for this test assert instance.vpc_role == "FOO" assert instance.vpc_role2 == "BAR" + assert isinstance(instance.filtered_data, dict) + assert instance.filtered_data.get("deviceName") == "cvd-2313-leaf" def test_image_upgrade_switch_issu_details_by_device_name_00022( @@ -194,7 +196,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00022( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByDeviceName.refresh Test - instance.result is a dict @@ -222,7 +224,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00023( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByDeviceName.refresh Test - refresh calls handle_response, which calls json_fail on 404 response @@ -247,7 +249,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00024( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByDeviceName.refresh Test - fail_json is called on 200 response with empty DATA key @@ -273,7 +275,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00025( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByDeviceName.refresh Test - fail_json is called on 200 response with DATA.lastOperDataObject length 0 @@ -300,17 +302,26 @@ def test_image_upgrade_switch_issu_details_by_device_name_00040( ) -> None: """ Function - - _get + - SwitchIssuDetailsByDeviceName._get + + Summary + Verify that _get() calls fail_json because filter is set to an + unknown device_name Test - - fail_json is called due to unknown device_name is set + - fail_json is called because filter is set to an unknown device_name - Error message matches expectation + Description SwitchIssuDetailsByDeviceName._get is called by all getter properties. - It raises AnsibleFailJson if the user has not set device_name or if - device_name is unknown, or if an unknown property name is queried. - It returns the value of the requested property if the user has set a known - device_name. + It raises AnsibleFailJson if the user has not set filter or if + filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to a device_name that exists on the controller. + + Expected result + 1. fail_json is called with appropriate error message since filter + is set to an unknown device_name. """ instance = issu_details_by_device_name @@ -335,16 +346,19 @@ def test_image_upgrade_switch_issu_details_by_device_name_00041( Function - _get + Summary + Verify that _get() calls fail_json because an unknown property is queried + Test - fail_json is called on access of unknown property name - Error message matches expectation Description SwitchIssuDetailsByDeviceName._get is called by all getter properties. - It raises AnsibleFailJson if the user has not set device_name or if - device_name is unknown, or if an unknown property name is queried. - It returns the value of the requested property if the user has set a known - ip_address. + It raises AnsibleFailJson if the user has not set filter or if + filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to a device_name that exists on the controller. """ instance = issu_details_by_device_name @@ -360,3 +374,30 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "property name: FOO" with pytest.raises(AnsibleFailJson, match=match): instance._get("FOO") # pylint: disable=protected-access + + +def test_image_upgrade_switch_issu_details_by_device_name_00042( + issu_details_by_device_name +) -> None: + """ + Function + - _get + + Test + - fail_json is called because instance.filter is not set + - Error message matches expectation + + Description + SwitchIssuDetailsByDeviceName._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set filter or if + filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to a device_name that exists on the controller. + """ + with does_not_raise(): + instance = issu_details_by_device_name + match = r"SwitchIssuDetailsByDeviceName\._get: " + match += r"set instance\.filter to a switch deviceName " + match += r"before accessing property role\." + with pytest.raises(AnsibleFailJson, match=match): + instance.role diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 80fd65afb..dc5e9ac9f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -46,7 +46,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00001( ) -> None: """ Function - - __init__ + - SwitchIssuDetailsByIpAddress.__init__ Test - fail_json is not called @@ -62,7 +62,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00002( ) -> None: """ Function - - _init_properties + - SwitchIssuDetailsByIpAddress._init_properties Test - Class properties initialized to expected values @@ -91,7 +91,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00020( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByIpAddress.refresh Test - instance.response is a list @@ -123,7 +123,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00021( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByIpAddress.refresh Test - Properties are set based on device_name @@ -196,6 +196,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: # NOTE: Values are synthesized in the response for this test assert instance.vpc_role == "FOO" assert instance.vpc_role2 == "BAR" + assert isinstance(instance.filtered_data, dict) + assert instance.filtered_data.get("deviceName") == "cvd-2313-leaf" def test_image_upgrade_switch_issu_details_by_ip_address_00022( @@ -203,7 +205,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00022( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByIpAddress.refresh Test - instance.result is a dict @@ -231,7 +233,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00023( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByIpAddress.refresh Test - refresh calls handle_response, which calls json_fail on 404 response @@ -257,7 +259,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00024( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByIpAddress.refresh Test - fail_json is called on 200 response with empty DATA key @@ -283,7 +285,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00025( ) -> None: """ Function - - refresh + - SwitchIssuDetailsByIpAddress.refresh Test - fail_json is called on 200 response with DATA.lastOperDataObject length 0 @@ -309,18 +311,27 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00040( monkeypatch, issu_details_by_ip_address ) -> None: """ - Function description: + Function + - SwitchIssuDetailsByIpAddress._get - SwitchIssuDetailsByIpAddress._get is called by all getter properties. - It raises AnsibleFailJson if the user has not set ip_address or if - the ip_address is unknown, or if an unknown property name is queried. - It returns the value of the requested property if the user has set a known - ip_address. + Summary + Verify that _get() calls fail_json because filter is set to an + unknown ip_address - Expected results: + Test + - fail_json is called because filter is set to an unknown ip_address + - Error message matches expectation - 1. fail_json is called with appropriate error message since an unknown - ip_address is set. + Description + SwitchIssuDetailsByIpAddress._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set filter or if + the filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to an ip_address that exists on the controller. + + Expected result: + 1. fail_json is called with appropriate error message since filter + is set to an unknown ip_address. """ instance = issu_details_by_ip_address @@ -342,16 +353,24 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00041( monkeypatch, issu_details_by_ip_address ) -> None: """ - Function description: + Function + SwitchIssuDetailsByIpAddress._get + + Summary + Verify that _get() calls fail_json because an unknown property is queried - SwitchIssuDetailsByIpAddress._get is called by all getter properties. - It raises AnsibleFailJson if the user has not set ip_address or if - the ip_address is unknown, or if an unknown property name is queried. - It returns the value of the requested property if the user has set a - known ip_address and the property name is valid. + Test + - fail_json is called on access of unknown property name + - Error message matches expectation - Expected results: + Description + SwitchIssuDetailsByIpAddress._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set filter or if + the filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to an ip_address that exists on the controller. + Expected results 1. fail_json is called with appropriate error message since an unknown property is queried. """ @@ -369,3 +388,30 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: match += "property name: FOO" with pytest.raises(AnsibleFailJson, match=match): instance._get("FOO") # pylint: disable=protected-access + + +def test_image_upgrade_switch_issu_details_by_ip_address_00042( + issu_details_by_ip_address +) -> None: + """ + Function + - SwitchIssuDetailsByIpAddress._get + + Test + - _get() calls fail_json because instance.filter is not set + - Error message matches expectation + + Description + SwitchIssuDetailsByIpAddress._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set filter or if + filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to an ip_address that exists on the controller. + """ + with does_not_raise(): + instance = issu_details_by_ip_address + match = r"SwitchIssuDetailsByIpAddress\._get: " + match += r"set instance\.filter to a switch ipAddress " + match += r"before accessing property role\." + with pytest.raises(AnsibleFailJson, match=match): + instance.role diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 846696301..426978ab4 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -47,7 +47,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00001( ) -> None: """ Function - - __init__ + - SwitchIssuDetailsBySerialNumber.__init__ Test - fail_json is not called @@ -63,7 +63,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00002( ) -> None: """ Function - - _init_properties + - SwitchIssuDetailsBySerialNumber._init_properties Test - Class properties initialized to expected values @@ -91,7 +91,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00020( ) -> None: """ Function - - refresh + - SwitchIssuDetailsBySerialNumber.refresh Test - instance.response is a list @@ -123,7 +123,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00021( ) -> None: """ Function - - refresh + - SwitchIssuDetailsBySerialNumber.refresh Test - Properties are set based on device_name @@ -195,6 +195,8 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: # NOTE: Values are synthesized in the response for this test assert instance.vpc_role == "FOO" assert instance.vpc_role2 == "BAR" + assert isinstance(instance.filtered_data, dict) + assert instance.filtered_data.get("deviceName") == "cvd-2313-leaf" def test_image_upgrade_switch_issu_details_by_serial_number_00022( @@ -202,7 +204,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00022( ) -> None: """ Function - - refresh + - SwitchIssuDetailsBySerialNumber.refresh Test - instance.result_current is a dict @@ -228,7 +230,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00023( ) -> None: """ Function - - refresh + - SwitchIssuDetailsBySerialNumber.refresh Test - refresh calls handle_response, which calls json_fail on 404 response @@ -253,7 +255,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00024( ) -> None: """ Function - - refresh + - SwitchIssuDetailsBySerialNumber.refresh Test - fail_json is called on 200 response with empty DATA key @@ -279,7 +281,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00025( ) -> None: """ Function - - refresh + - SwitchIssuDetailsBySerialNumber.refresh Test - fail_json is called on 200 response with DATA.lastOperDataObject length 0 @@ -304,18 +306,27 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00040( monkeypatch, issu_details_by_serial_number ) -> None: """ - Function description: + Function + - SwitchIssuDetailsBySerialNumber._get - SwitchIssuDetailsBySerialNumber._get is called by all getter properties. - It raises AnsibleFailJson if the user has not set serial_number or if - serial_number is unknown, or if an unknown property name is queried. - It returns the value of the requested property if the user has set a known - serial_number and the property name is valid. + Summary + Verify that _get() calls fail_json because filter is set to an + unknown serial_number - Expected results: + Test + - fail_json is called because filter is set to an unknown serial_number + - Error message matches expectation - 1. fail_json is called with appropriate error message since an unknown - serial_number is set. + Description + SwitchIssuDetailsBySerialNumber._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set filter or if + filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to a serial_number that exists on the controller. + + Expected result: + 1. fail_json is called with appropriate error message since filter + is set to an unknown serial_number. """ instance = issu_details_by_serial_number @@ -338,16 +349,24 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00041( monkeypatch, issu_details_by_serial_number ) -> None: """ - Function description: + Function + SwitchIssuDetailsBySerialNumber._get - SwitchIssuDetailsBySerialNumber._get is called by all getter properties. - It raises AnsibleFailJson if the user has not set serial_number or if - serial_number is unknown, or if an unknown property name is queried. - It returns the value of the requested property if the user has set a known - serial_number and the property name is valid. + Summary + Verify that _get() calls fail_json because an unknown property is queried + + Test + - fail_json is called on access of unknown property name + - Error message matches expectation - Expected results: + Description + SwitchIssuDetailsBySerialNumber._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set filter or if + filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to a serial_number that exists on the controller. + Expected results 1. fail_json is called with appropriate error message since an unknown property is queried. """ @@ -366,3 +385,30 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.filter = "FDO21120U5D" with pytest.raises(AnsibleFailJson, match=match): instance._get("FOO") # pylint: disable=protected-access + + +def test_image_upgrade_switch_issu_details_by_serial_number_00042( + issu_details_by_serial_number +) -> None: + """ + Function + - SwitchIssuDetailsBySerialNumber._get + + Test + - _get() calls fail_json because instance.filter is not set + - Error message matches expectation + + Description + SwitchIssuDetailsBySerialNumber._get is called by all getter properties. + It raises AnsibleFailJson if the user has not set filter or if + filter is unknown, or if an unknown property name is queried. + It returns the value of the requested property if the user has filter + to a serial_number that exists on the controller. + """ + with does_not_raise(): + instance = issu_details_by_serial_number + match = r"SwitchIssuDetailsBySerialNumber\._get: " + match += r"set instance\.filter to a switch serialNumber " + match += r"before accessing property role\." + with pytest.raises(AnsibleFailJson, match=match): + instance.role From 63ac119b74bae18a8ed8126abdd921daebf505fe Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 09:34:43 -1000 Subject: [PATCH 285/300] Shaved a couple seconds off unit test runtime --- .../module_utils/image_upgrade/image_stage.py | 6 +- .../image_upgrade/image_validate.py | 6 +- .../test_image_upgrade_image_stage.py | 85 +++++++------- .../test_image_upgrade_image_upgrade.py | 110 +++++++++--------- .../test_image_upgrade_image_validate.py | 95 ++++++++------- 5 files changed, 158 insertions(+), 144 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py index f8a579766..1f2d96f59 100644 --- a/plugins/module_utils/image_upgrade/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -264,7 +264,8 @@ def _wait_for_current_actions_to_complete(self): timeout = self.check_timeout while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) + if self.unit_test is False: + sleep(self.check_interval) timeout -= self.check_interval self.issu_detail.refresh() @@ -297,7 +298,8 @@ def _wait_for_image_stage_to_complete(self): serial_numbers_todo = set(copy.copy(self.serial_numbers)) while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) + if self.unit_test is False: + sleep(self.check_interval) timeout -= self.check_interval self.issu_detail.refresh() diff --git a/plugins/module_utils/image_upgrade/image_validate.py b/plugins/module_utils/image_upgrade/image_validate.py index c02c90515..7de22613e 100644 --- a/plugins/module_utils/image_upgrade/image_validate.py +++ b/plugins/module_utils/image_upgrade/image_validate.py @@ -241,7 +241,8 @@ def _wait_for_current_actions_to_complete(self) -> None: timeout = self.check_timeout while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) + if self.unit_test is False: + sleep(self.check_interval) timeout -= self.check_interval self.issu_detail.refresh() @@ -274,7 +275,8 @@ def _wait_for_image_validate_to_complete(self) -> None: serial_numbers_todo = set(copy.copy(self.serial_numbers)) while self.serial_numbers_done != serial_numbers_todo and timeout > 0: - sleep(self.check_interval) + if self.unit_test is False: + sleep(self.check_interval) timeout -= self.check_interval self.issu_detail.refresh() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index 31a1a485e..eedbc1077 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -251,14 +251,15 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - instance = image_stage - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 0 - instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access + with does_not_raise(): + instance = image_stage + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + instance._wait_for_image_stage_to_complete() # pylint: disable=protected-access assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 2 assert "FDO21120U5D" in instance.serial_numbers_done @@ -299,14 +300,16 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - instance = image_stage - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 0 - match = "Seconds remaining 1800: stage image failed for " + with does_not_raise(): + instance = image_stage + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + + match = "Seconds remaining 1790: stage image failed for " match += "cvd-2313-leaf, FDO2112189M, 172.22.150.108. image " match += "staged percent: 90" with pytest.raises(AnsibleFailJson, match=match): @@ -349,14 +352,14 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - instance = image_stage - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 1 - instance.check_timeout = 1 + with does_not_raise(): + instance = image_stage + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] match = "ImageStage._wait_for_image_stage_to_complete: " match += "Timed out waiting for image stage to complete. " @@ -406,14 +409,15 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - instance = image_stage - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 0 - instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access + with does_not_raise(): + instance = image_stage + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 2 assert "FDO21120U5D" in instance.serial_numbers_done @@ -455,14 +459,14 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: match += "serial_numbers_done: FDO21120U5D, " match += "serial_numbers_todo: FDO21120U5D,FDO2112189M" - instance = image_stage - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 1 - instance.check_timeout = 1 + with does_not_raise(): + instance = image_stage + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] with pytest.raises(AnsibleFailJson, match=match): instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access @@ -715,7 +719,6 @@ def mock_dcnm_send_switch_issu_details(*args) -> Dict[str, Any]: instance = image_stage instance.serial_numbers = ["FDO21120U5D"] instance.commit() - print(f"instance.payload: {instance.payload.keys()}") assert expected_serial_number_key in instance.payload.keys() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 073e42eba..ad0347169 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -2174,21 +2174,21 @@ def test_image_upgrade_upgrade_00200( instance.ip_addresses 4. fail_json is not called """ - instance = image_upgrade - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00200a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_ip_address - instance.ip_addresses = [ - "172.22.150.102", - "172.22.150.108", - ] - instance.check_interval = 0 with does_not_raise(): + instance = image_upgrade + instance.unit_test = True + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + instance.check_interval = 0 instance._wait_for_current_actions_to_complete() assert isinstance(instance.ipv4_done, set) assert len(instance.ipv4_done) == 2 @@ -2234,23 +2234,22 @@ def test_image_upgrade_upgrade_00205( 5. (Post analysis) converage tool indicates tha the continue statement is hit. """ - instance = image_upgrade - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00205a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_ip_address - instance.ip_addresses = [ - "172.22.150.102", - "172.22.150.108", - ] - instance.check_interval = 0 - instance.ipv4_done.add("172.22.150.102") - instance.unit_test = True with does_not_raise(): + instance = image_upgrade + instance.unit_test = True + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + instance.check_interval = 0 + instance.ipv4_done.add("172.22.150.102") instance._wait_for_current_actions_to_complete() assert isinstance(instance.ipv4_done, set) assert len(instance.ipv4_done) == 2 @@ -2279,21 +2278,22 @@ def test_image_upgrade_upgrade_00210( - fail_json is called due to timeout - fail_json error message is matched """ - instance = image_upgrade - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00210a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_ip_address - instance.ip_addresses = [ - "172.22.150.102", - "172.22.150.108", - ] - instance.check_interval = 1 - instance.check_timeout = 1 + with does_not_raise(): + instance = image_upgrade + instance.unit_test = True + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + instance.check_interval = 1 + instance.check_timeout = 1 match = "ImageUpgrade._wait_for_current_actions_to_complete: " match += "Timed out waiting for actions to complete. " @@ -2332,7 +2332,6 @@ def test_image_upgrade_upgrade_00220( - instance.ipv4_done contains 172.22.150.102, upgrade is "Success" - Call fail_json on ip address 172.22.150.108, upgrade is "Failed" """ - instance = image_upgrade def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00220a" @@ -2340,12 +2339,15 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_ip_address - instance.ip_addresses = [ - "172.22.150.102", - "172.22.150.108", - ] - instance.check_interval = 0 + with does_not_raise(): + instance = image_upgrade + instance.unit_test = True + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + instance.check_interval = 0 match = "ImageUpgrade._wait_for_image_upgrade_to_complete: " match += "Seconds remaining 1800: " match += "upgrade image Failed for cvd-2313-leaf, FDO2112189M, " @@ -2387,21 +2389,22 @@ def test_image_upgrade_upgrade_00230( - instance.ipv4_done contains 172.22.150.102, upgrade is "Success" - fail_json is called due to timeout exceeded """ - instance = image_upgrade - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00230a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_ip_address - instance.ip_addresses = [ - "172.22.150.102", - "172.22.150.108", - ] - instance.check_interval = 1 - instance.check_timeout = 1 + with does_not_raise(): + instance = image_upgrade + instance.unit_test = True + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + instance.check_interval = 1 + instance.check_timeout = 1 match = "ImageUpgrade._wait_for_image_upgrade_to_complete: " match += r"The following device\(s\) did not complete upgrade: " @@ -2450,24 +2453,23 @@ def test_image_upgrade_upgrade_00240( - instance.ipv4_done contains 172.22.150.102 and 172.22.150.108 - fail_json is not called """ - instance = image_upgrade - def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00240a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_ip_address - instance.ip_addresses = [ - "172.22.150.102", - "172.22.150.108", - ] - instance.check_interval = 1 - instance.check_timeout = 1 - instance.ipv4_done.add("172.22.150.102") - instance.unit_test = True with does_not_raise(): + instance = image_upgrade + instance.unit_test = True + instance.issu_detail = issu_details_by_ip_address + instance.ip_addresses = [ + "172.22.150.102", + "172.22.150.108", + ] + instance.check_interval = 1 + instance.check_timeout = 1 + instance.ipv4_done.add("172.22.150.102") instance._wait_for_image_upgrade_to_complete() assert isinstance(instance.ipv4_done, set) assert len(instance.ipv4_done) == 2 diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 446bb1a28..9a637f5ef 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -197,21 +197,21 @@ def test_image_upgrade_validate_00005( In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ - instance = image_validate - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00005a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 0 - instance._wait_for_image_validate_to_complete() + with does_not_raise(): + instance = image_validate + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + instance._wait_for_image_validate_to_complete() assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 2 assert "FDO21120U5D" in instance.serial_numbers_done @@ -239,21 +239,22 @@ def test_image_upgrade_validate_00006( In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ - instance = image_validate - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00006a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 0 - match = "Seconds remaining 1800: validate image Failed for " + with does_not_raise(): + instance = image_validate + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + + match = "Seconds remaining 1790: validate image Failed for " match += "cvd-2313-leaf, 172.22.150.108, FDO2112189M, " match += "image validated percent: 100. Check the switch e.g. " match += "show install log detail, show incompatibility-all nxos " @@ -287,21 +288,22 @@ def test_image_upgrade_validate_00007( Description See test_wait_for_image_stage_to_complete for functional details. """ - instance = image_validate - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00007a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 1 - instance.check_timeout = 1 + with does_not_raise(): + instance = image_validate + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + instance.check_interval = 1 + instance.check_timeout = 1 match = "ImageValidate._wait_for_image_validate_to_complete: " match += "Timed out waiting for image validation to complete. " @@ -339,21 +341,21 @@ def test_image_upgrade_validate_00008( ["imageStaged", "upgrade", "validated"] """ - instance = image_validate - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00008a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 0 - instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access + with does_not_raise(): + instance = image_validate + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + instance._wait_for_current_actions_to_complete() # pylint: disable=protected-access assert isinstance(instance.serial_numbers_done, set) assert len(instance.serial_numbers_done) == 2 assert "FDO21120U5D" in instance.serial_numbers_done @@ -378,21 +380,22 @@ def test_image_upgrade_validate_00009( Description See test_wait_for_current_actions_to_complete for functional details. """ - instance = image_validate - def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00009a" return responses_switch_issu_details(key) monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) - instance.issu_detail = issu_details_by_serial_number - instance.serial_numbers = [ - "FDO21120U5D", - "FDO2112189M", - ] - instance.check_interval = 1 - instance.check_timeout = 1 + with does_not_raise(): + instance = image_validate + instance.unit_test = True + instance.issu_detail = issu_details_by_serial_number + instance.serial_numbers = [ + "FDO21120U5D", + "FDO2112189M", + ] + instance.check_interval = 1 + instance.check_timeout = 1 match = "ImageValidate._wait_for_current_actions_to_complete: " match += "Timed out waiting for actions to complete. " @@ -538,6 +541,7 @@ def test_image_upgrade_validate_00040(image_validate, value, expected) -> None: "value, expected", [ (10, does_not_raise()), + (-10, pytest.raises(AnsibleFailJson, match=MATCH_00050)), ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00050)), (False, pytest.raises(AnsibleFailJson, match=MATCH_00050)), (None, pytest.raises(AnsibleFailJson, match=MATCH_00050)), @@ -570,6 +574,7 @@ def test_image_upgrade_validate_00050(image_validate, value, expected) -> None: "value, expected", [ (10, does_not_raise()), + (-10, pytest.raises(AnsibleFailJson, match=MATCH_00060)), ("FOO", pytest.raises(AnsibleFailJson, match=MATCH_00060)), (False, pytest.raises(AnsibleFailJson, match=MATCH_00060)), (None, pytest.raises(AnsibleFailJson, match=MATCH_00060)), From e8c838f250f2bc7e5e0e30856dc4808ad6290f33 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 09:37:32 -1000 Subject: [PATCH 286/300] Fix PEP8 whitespace errors --- .../test_image_upgrade_switch_issu_details_by_device_name.py | 2 +- .../test_image_upgrade_switch_issu_details_by_ip_address.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index 7cf3a54d0..f9a9c53c7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -396,7 +396,7 @@ def test_image_upgrade_switch_issu_details_by_device_name_00042( """ with does_not_raise(): instance = issu_details_by_device_name - match = r"SwitchIssuDetailsByDeviceName\._get: " + match = r"SwitchIssuDetailsByDeviceName\._get: " match += r"set instance\.filter to a switch deviceName " match += r"before accessing property role\." with pytest.raises(AnsibleFailJson, match=match): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index dc5e9ac9f..29632054a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -355,7 +355,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00041( """ Function SwitchIssuDetailsByIpAddress._get - + Summary Verify that _get() calls fail_json because an unknown property is queried @@ -410,7 +410,7 @@ def test_image_upgrade_switch_issu_details_by_ip_address_00042( """ with does_not_raise(): instance = issu_details_by_ip_address - match = r"SwitchIssuDetailsByIpAddress\._get: " + match = r"SwitchIssuDetailsByIpAddress\._get: " match += r"set instance\.filter to a switch ipAddress " match += r"before accessing property role\." with pytest.raises(AnsibleFailJson, match=match): From fc3adc9676897aa4e3e665626a203a0de7c13655 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 09:41:17 -1000 Subject: [PATCH 287/300] Fix PEP8 whitespace error --- .../test_image_upgrade_switch_issu_details_by_serial_number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 426978ab4..b1a05068d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -407,7 +407,7 @@ def test_image_upgrade_switch_issu_details_by_serial_number_00042( """ with does_not_raise(): instance = issu_details_by_serial_number - match = r"SwitchIssuDetailsBySerialNumber\._get: " + match = r"SwitchIssuDetailsBySerialNumber\._get: " match += r"set instance\.filter to a switch serialNumber " match += r"before accessing property role\." with pytest.raises(AnsibleFailJson, match=match): From 6b517e87bfaa9c8f621273152690ebf1f796cf73 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 13:42:31 -1000 Subject: [PATCH 288/300] ImageStage: Instantiate path and verb in __init__() test_image_upgrade_stage_00001: add path,verb asserts test_image_upgrade_stage_00071: remove this test case now that it's covered in 00001 above --- .../module_utils/image_upgrade/image_stage.py | 7 +-- .../test_image_upgrade_image_stage.py | 48 ++----------------- 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py index 1f2d96f59..d18957e4e 100644 --- a/plugins/module_utils/image_upgrade/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -106,13 +106,14 @@ def __init__(self, module): self.log.debug("ENTERED ImageStage()") self.endpoints = ApiEndpoints() + self.path = self.endpoints.image_stage.get("path") + self.verb = self.endpoints.image_stage.get("verb") + self.payload = None + self.rest_send = RestSend(self.module) self._init_properties() self.serial_numbers_done = set() self.controller_version = None - self.path = None - self.verb = None - self.payload = None self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) def _init_properties(self): diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index eedbc1077..fdb6068c9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -77,12 +77,15 @@ def test_image_upgrade_stage_00001(image_stage) -> None: assert isinstance(instance.properties, dict) assert isinstance(instance.serial_numbers_done, set) assert instance.controller_version is None - assert instance.path is None - assert instance.verb is None assert instance.payload is None assert isinstance(instance.issu_detail, SwitchIssuDetailsBySerialNumber) assert isinstance(instance.endpoints, ApiEndpoints) + module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" + module_path += "stagingmanagement/stage-image" + assert instance.path == module_path + assert instance.verb == "POST" + def test_image_upgrade_stage_00002(image_stage) -> None: """ @@ -629,47 +632,6 @@ def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.commit() -def test_image_upgrade_stage_00071(monkeypatch, image_stage) -> None: - """ - Function - - commit - - Summary - Verify that verb is set to POST and path is set to the expected value. - - Test - - ImageStage.verb is set to POST - - ImageStage.path is set to: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image - """ - key = "test_image_upgrade_stage_00071a" - - def mock_dcnm_send_controller_version(*args, **kwargs) -> Dict[str, Any]: - return responses_controller_version(key) - - # Needed only for the 200 return code - def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: - return responses_image_stage(key) - - def mock_dcnm_send_switch_issu_details(*args, **kwargs) -> Dict[str, Any]: - return responses_switch_issu_details(key) - - monkeypatch.setattr(DCNM_SEND_CONTROLLER_VERSION, mock_dcnm_send_controller_version) - monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_switch_issu_details) - - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_COMMIT, mock_rest_send_image_stage) - monkeypatch.setattr(PATCH_IMAGE_STAGE_REST_SEND_RESULT_CURRENT, {"success": True}) - - module_path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/" - module_path += "stagingmanagement/stage-image" - - instance = image_stage - instance.serial_numbers = ["FDO21120U5D"] - instance.commit() - assert instance.path == module_path - assert instance.verb == "POST" - - @pytest.mark.parametrize( "controller_version, expected_serial_number_key", [ From 3987a0401df7503630c4f3f8b323639aa4f8c82b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 15:41:40 -1000 Subject: [PATCH 289/300] ImageValidate: 98% unit test coverage ImageValidate.commit: Add unit test to verify diff ImageValidate: Improve unit test docstrings image_upgrade_responses_imageStage.json: Fix unit test 00075 key ImageStage.commit: remove self.path and self.verb initializations (these are initialized in __init__() ImageStage.commit: add return value type hint --- .../module_utils/image_upgrade/image_stage.py | 5 +- .../image_upgrade/image_validate.py | 11 ++-- .../image_upgrade_responses_ImageStage.json | 2 +- ...image_upgrade_responses_ImageValidate.json | 13 ++++ ...e_upgrade_responses_SwitchIssuDetails.json | 51 ++++++++++++++++ .../test_image_upgrade_image_validate.py | 61 ++++++++++++++++++- 6 files changed, 129 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py index d18957e4e..6e0bc0399 100644 --- a/plugins/module_utils/image_upgrade/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -166,7 +166,7 @@ def validate_serial_numbers(self): msg += "and try again." self.module.fail_json(msg, **self.failed_result) - def commit(self): + def commit(self) -> None: """ Commit the image staging request to the controller and wait for the images to be staged. @@ -199,9 +199,6 @@ def commit(self): self.validate_serial_numbers() self._wait_for_current_actions_to_complete() - self.path = self.endpoints.image_stage.get("path") - self.verb = self.endpoints.image_stage.get("verb") - self.payload = {} self._populate_controller_version() diff --git a/plugins/module_utils/image_upgrade/image_validate.py b/plugins/module_utils/image_upgrade/image_validate.py index 7de22613e..91d2cf668 100644 --- a/plugins/module_utils/image_upgrade/image_validate.py +++ b/plugins/module_utils/image_upgrade/image_validate.py @@ -193,18 +193,12 @@ def commit(self) -> None: self.result = self.rest_send.result_current self.result_current = self.rest_send.result_current - msg = f"self.rest_send.result_current: {self.rest_send.result_current}" - self.log.debug(msg) msg = f"self.payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) msg = f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" - self.log.debug(msg) msg = f"self.result: {json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}" - self.log.debug(msg) msg = f"self.response_data: {self.response_data}" self.log.debug(msg) @@ -227,6 +221,8 @@ def commit(self) -> None: diff["serial_number"] = serial_number # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(diff) + msg = f"self.diff: {json.dumps(self.diff, indent=4, sort_keys=True)}" + self.log.debug(msg) def _wait_for_current_actions_to_complete(self) -> None: """ @@ -236,7 +232,8 @@ def _wait_for_current_actions_to_complete(self) -> None: """ self.method_name = inspect.stack()[0][3] - self.serial_numbers_done: Set[str] = set() + if self.unit_test is False: + self.serial_numbers_done: Set[str] = set() serial_numbers_todo = set(copy.copy(self.serial_numbers)) timeout = self.check_timeout diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json index 187c589df..9ec20cb7b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageStage.json @@ -62,7 +62,7 @@ "message": "" } }, - "test_image_upgrade_stage_00070a": { + "test_image_upgrade_stage_00075a": { "TEST_NOTES": [ "RETURN_CODE == 200", "MESSAGE == OK" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json index 2a9c7b9c3..6c311b86d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_ImageValidate.json @@ -29,5 +29,18 @@ ], "message": "" } + }, + "test_image_upgrade_validate_00024a": { + "TEST_NOTES": [], + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json index 2784bd138..aa106070a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/fixtures/image_upgrade_responses_SwitchIssuDetails.json @@ -2000,6 +2000,57 @@ "message": "" } }, + "test_image_upgrade_validate_00024a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/packagemgnt/issu", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "serialNumber": "FDO21120U5D", + "deviceName": "leaf1", + "version": "10.2(5)", + "policy": "KR5M", + "status": "In-Sync", + "reason": "Upgrade", + "imageStaged": "Success", + "validated": "Success", + "upgrade": "Success", + "upgGroups": "null", + "mode": "Normal", + "systemMode": "Normal", + "vpcRole": "null", + "vpcPeer": "null", + "role": "leaf", + "lastUpgAction": "2023-Oct-19 02:20", + "model": "N9K-C93180YC-EX", + "fabric": "easy", + "ipAddress": "172.22.150.102", + "issuAllowed": "", + "statusPercent": 100, + "imageStagedPercent": 100, + "validatedPercent": 100, + "upgradePercent": 0, + "modelType": 0, + "vdcId": 0, + "ethswitchid": 145740, + "platform": "N9K", + "vpc_role": "null", + "ip_address": "172.22.150.102", + "peer": "null", + "vdc_id": -1, + "sys_name": "leaf1", + "id": 1, + "group": "easy", + "fcoEEnabled": "False", + "mds": "False" + } + ], + "message": "" + } + }, "test_image_upgrade_image_policy_action_00003a": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 9a637f5ef..98d3b8e6b 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -415,6 +415,10 @@ def test_image_upgrade_validate_00022(image_validate) -> None: Function - commit + Summary + Verify that instance.commit() returns without calling dcnm_send when + instance.serial_numbers is an empty list. + Test - instance.response is set to {} because dcnm_send was not called - instance.result is set to {} because dcnm_send was not called @@ -436,17 +440,20 @@ def test_image_upgrade_validate_00023(monkeypatch, image_validate) -> None: Function - commit + Summary + Verify that instance.commit() calls fail_json on failure response from + the controller (501). + Test - fail_json is called on 501 response from controller """ + key = "test_image_upgrade_validate_00023a" # Needed only for the 501 return code def mock_rest_send_image_validate(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_upgrade_validate_00023a" return responses_image_validate(key) def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: - key = "test_image_upgrade_validate_00023a" return responses_switch_issu_details(key) monkeypatch.setattr( @@ -466,6 +473,56 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: instance.commit() +def test_image_upgrade_validate_00024(monkeypatch, image_validate) -> None: + """ + Function + - commit + + Summary + Verify that instance.commit() sets instance.diff appropriately on + a successful response from the controller. + + Test + - instance.diff is set to the expected value + - fail_json is not called + """ + key = "test_image_upgrade_validate_00024a" + + def mock_rest_send_image_validate(*args, **kwargs) -> Dict[str, Any]: + return responses_image_validate(key) + + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: + return responses_switch_issu_details(key) + + def mock_wait_for_image_validate_to_complete(*args) -> None: + instance.serial_numbers_done = {"FDO21120U5D"} + + monkeypatch.setattr( + PATCH_IMAGE_VALIDATE_REST_SEND_COMMIT, mock_rest_send_image_validate + ) + monkeypatch.setattr( + PATCH_IMAGE_VALIDATE_REST_SEND_RESULT_CURRENT, + {"success": True, "changed": True}, + ) + monkeypatch.setattr(DCNM_SEND_ISSU_DETAILS, mock_dcnm_send_issu_details) + + with does_not_raise(): + instance = image_validate + instance.unit_test = True + instance.serial_numbers = ["FDO21120U5D"] + monkeypatch.setattr( + instance, + "_wait_for_image_validate_to_complete", + mock_wait_for_image_validate_to_complete, + ) + instance.commit() + + assert instance.diff[0]["action"] == "validate" + assert instance.diff[0]["policy"] == "KR5M" + assert instance.diff[0]["ip_address"] == "172.22.150.102" + assert instance.diff[0]["serial_number"] == "FDO21120U5D" + + MATCH_00030 = "ImageValidate.serial_numbers: " MATCH_00030 += "instance.serial_numbers must be a python list " MATCH_00030 += "of switch serial numbers." From ad6d70eef195c027b218945ec47dedb70864737a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 16:00:57 -1000 Subject: [PATCH 290/300] ImageInstallOptions: 99% unit test coverage InstallOptions.policy_name: minor edit to fail_json message. ImageInstallOptions.policy_name: Add unit test test_image_upgrade_image_install_options.py: - rename unit test test_image_upgrade_install_options_00003 to test_image_upgrade_install_options_00070 Run all other test_image_*.py files through black/isort --- .../image_upgrade/install_options.py | 2 +- ...est_image_upgrade_image_install_options.py | 77 ++++++++++++++----- .../test_image_upgrade_image_upgrade.py | 5 ++ .../test_image_upgrade_image_validate.py | 5 ++ .../test_image_upgrade_switch_details.py | 1 + ...rade_switch_issu_details_by_device_name.py | 2 +- ...grade_switch_issu_details_by_ip_address.py | 2 +- ...de_switch_issu_details_by_serial_number.py | 2 +- 8 files changed, 73 insertions(+), 23 deletions(-) diff --git a/plugins/module_utils/image_upgrade/install_options.py b/plugins/module_utils/image_upgrade/install_options.py index 505832c1e..581941df1 100644 --- a/plugins/module_utils/image_upgrade/install_options.py +++ b/plugins/module_utils/image_upgrade/install_options.py @@ -306,7 +306,7 @@ def policy_name(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, str): msg = f"{self.class_name}.{method_name}: " - msg += f"policy_name must be a string. Got {value}." + msg += f"instance.policy_name must be a string. Got {value}." self.module.fail_json(msg, **self.failed_result) self.properties["policy_name"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index 705ed5eeb..c19a7e97f 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -90,25 +90,6 @@ def test_image_upgrade_install_options_00002(image_install_options) -> None: assert instance.properties.get("unit_test") is False -def test_image_upgrade_install_options_00003(image_install_options) -> None: - """ - Function - - refresh - - serial_number setter - - Test - - fail_json is called because policy_name is not set when refresh is called - - fail_json error message is matched - """ - instance = image_install_options - instance.serial_number = "FOO" - match = "ImageInstallOptions._validate_refresh_parameters: " - match += "instance.policy_name must be set before " - match += r"calling refresh\(\)" - with pytest.raises(AnsibleFailJson, match=match): - instance.refresh() - - def test_image_upgrade_install_options_00004(image_install_options) -> None: """ Function @@ -574,3 +555,61 @@ def test_image_upgrade_install_options_00024(image_install_options) -> None: instance.unit_test = True with pytest.raises(AnsibleFailJson, match=match): instance.package_install = "FOO" + + +def test_image_upgrade_install_options_00070(image_install_options) -> None: + """ + Function + - refresh + - policy_name + + Summary + - refresh() calls fail_json if serial_number if policy_name is not set. + + Test + - fail_json is called because policy_name is not set when refresh is called + - fail_json error message is matched + """ + instance = image_install_options + instance.serial_number = "FOO" + match = "ImageInstallOptions._validate_refresh_parameters: " + match += "instance.policy_name must be set before " + match += r"calling refresh\(\)" + with pytest.raises(AnsibleFailJson, match=match): + instance.refresh() + + +MATCH_00080 = r"ImageInstallOptions\.policy_name: " +MATCH_00080 += r"instance\.policy_name must be a string. Got" + + +@pytest.mark.parametrize( + "value, expected, raise_flag", + [ + ("NR3F", does_not_raise(), False), + (1, pytest.raises(AnsibleFailJson, match=MATCH_00080), True), + (False, pytest.raises(AnsibleFailJson, match=MATCH_00080), True), + ({"foo": "bar"}, pytest.raises(AnsibleFailJson, match=MATCH_00080), True), + ([1, 2], pytest.raises(AnsibleFailJson, match=MATCH_00080), True), + ], +) +def test_image_upgrade_install_options_00080( + image_install_options, value, expected, raise_flag +) -> None: + """ + Function + - ImageInstallOptions.policy_name + + Summary + Verify proper behavior of policy_name property + + Test + - fail_json is called when property_name is not a string + - fail_json is not called when property_name is a string + """ + with does_not_raise(): + instance = image_install_options + with expected: + instance.policy_name = value + if raise_flag is False: + assert instance.policy_name == value diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index ad0347169..9570caa11 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -2174,6 +2174,7 @@ def test_image_upgrade_upgrade_00200( instance.ip_addresses 4. fail_json is not called """ + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00200a" return responses_switch_issu_details(key) @@ -2234,6 +2235,7 @@ def test_image_upgrade_upgrade_00205( 5. (Post analysis) converage tool indicates tha the continue statement is hit. """ + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00205a" return responses_switch_issu_details(key) @@ -2278,6 +2280,7 @@ def test_image_upgrade_upgrade_00210( - fail_json is called due to timeout - fail_json error message is matched """ + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00210a" return responses_switch_issu_details(key) @@ -2389,6 +2392,7 @@ def test_image_upgrade_upgrade_00230( - instance.ipv4_done contains 172.22.150.102, upgrade is "Success" - fail_json is called due to timeout exceeded """ + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00230a" return responses_switch_issu_details(key) @@ -2453,6 +2457,7 @@ def test_image_upgrade_upgrade_00240( - instance.ipv4_done contains 172.22.150.102 and 172.22.150.108 - fail_json is not called """ + def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_upgrade_00240a" return responses_switch_issu_details(key) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 98d3b8e6b..3e3d9b095 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -197,6 +197,7 @@ def test_image_upgrade_validate_00005( In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00005a" return responses_switch_issu_details(key) @@ -239,6 +240,7 @@ def test_image_upgrade_validate_00006( In the case where all serial numbers are "Success", the module returns. In the case where any serial number is "Failed", the module calls fail_json. """ + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00006a" return responses_switch_issu_details(key) @@ -288,6 +290,7 @@ def test_image_upgrade_validate_00007( Description See test_wait_for_image_stage_to_complete for functional details. """ + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00007a" return responses_switch_issu_details(key) @@ -341,6 +344,7 @@ def test_image_upgrade_validate_00008( ["imageStaged", "upgrade", "validated"] """ + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00008a" return responses_switch_issu_details(key) @@ -380,6 +384,7 @@ def test_image_upgrade_validate_00009( Description See test_wait_for_current_actions_to_complete for functional details. """ + def mock_dcnm_send_issu_details(*args) -> Dict[str, Any]: key = "test_image_upgrade_validate_00009a" return responses_switch_issu_details(key) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index 3916fe1a1..a6002f741 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -428,6 +428,7 @@ def test_image_upgrade_switch_details_00030(monkeypatch, switch_details) -> None - platform returns None - fail_json is not called """ + def mock_rest_send_switch_details(*args, **kwargs) -> Dict[str, Any]: key = "test_image_upgrade_switch_details_00030a" return responses_switch_details(key) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index f9a9c53c7..b50c02a5d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -377,7 +377,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_switch_issu_details_by_device_name_00042( - issu_details_by_device_name + issu_details_by_device_name, ) -> None: """ Function diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 29632054a..558b87680 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -391,7 +391,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_switch_issu_details_by_ip_address_00042( - issu_details_by_ip_address + issu_details_by_ip_address, ) -> None: """ Function diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index b1a05068d..3fe693e88 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -388,7 +388,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: def test_image_upgrade_switch_issu_details_by_serial_number_00042( - issu_details_by_serial_number + issu_details_by_serial_number, ) -> None: """ Function From e52497d1a6fb6a13b40a01aaa2bbe7eb9125b983 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 29 Feb 2024 21:04:53 -1000 Subject: [PATCH 291/300] Disable logging --- plugins/modules/dcnm_image_upgrade.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index b71d47844..77f001e18 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1326,9 +1326,9 @@ def main(): # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json log = Log(ansible_module) - collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - log.config = config_file + # collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + # config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + # log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) From 0e68d016e4f6b2c72a8f35768413e5a8c71847bf Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 1 Mar 2024 17:04:02 -1000 Subject: [PATCH 292/300] Update integration tests with new include syntax 'include' is deprecated in Ansible roles and is now 'include_tasks' --- tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml | 2 +- tests/integration/targets/dcnm_image_upgrade/tasks/main.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml b/tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml index 0f3410d0f..e419fc865 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tasks/dcnm.yaml @@ -14,7 +14,7 @@ set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" - name: run test cases (connection=httpapi) - include: "{{ test_case_to_run }}" + include_tasks: "{{ test_case_to_run }}" with_items: "{{ test_items }}" loop_control: loop_var: test_case_to_run diff --git a/tests/integration/targets/dcnm_image_upgrade/tasks/main.yaml b/tests/integration/targets/dcnm_image_upgrade/tasks/main.yaml index 78c5fb834..f3ccb253f 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tasks/main.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tasks/main.yaml @@ -1,2 +1,2 @@ --- -- { include: dcnm.yaml, tags: ['dcnm'] } \ No newline at end of file +- { include_tasks: dcnm.yaml, tags: ['dcnm'] } \ No newline at end of file From 347f84e071ad60a69521061dc6d99d6843a8845d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 4 Mar 2024 20:49:56 -1000 Subject: [PATCH 293/300] Initial check_mode changes (needs more work) --- .../module_utils/common/controller_version.py | 10 +-- plugins/module_utils/common/rest_send.py | 9 +- .../image_upgrade/image_policies.py | 18 ++-- .../image_upgrade/image_policy_action.py | 34 ++++---- .../module_utils/image_upgrade/image_stage.py | 36 ++++---- .../image_upgrade/image_upgrade.py | 86 ++++++++++--------- .../image_upgrade/image_upgrade_common.py | 43 +++++----- .../image_upgrade_task_result.py | 9 +- .../image_upgrade/image_validate.py | 34 ++++---- .../image_upgrade/install_options.py | 22 ++--- .../image_upgrade/switch_details.py | 14 +-- .../image_upgrade/switch_issu_details.py | 42 ++++----- plugins/modules/dcnm_image_upgrade.py | 56 ++++++------ .../unit/module_utils/common/common_utils.py | 2 + .../dcnm_image_upgrade/image_upgrade_utils.py | 2 + ...est_image_upgrade_image_install_options.py | 2 +- .../test_image_upgrade_image_policies.py | 2 +- .../test_image_upgrade_image_stage.py | 2 +- 18 files changed, 221 insertions(+), 202 deletions(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 91f6d8165..3a79bc985 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -66,8 +66,8 @@ class ControllerVersion(ImageUpgradeCommon): } """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -88,19 +88,19 @@ def refresh(self): """ path = self.endpoints.controller_version.get("path") verb = self.endpoints.controller_version.get("verb") - self.properties["response"] = dcnm_send(self.module, verb, path) + self.properties["response"] = dcnm_send(self.ansible_module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) if self.result["success"] is False or self.result["found"] is False: msg = f"{self.class_name}.refresh() failed: {self.result}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) self.properties["response_data"] = self.response.get("DATA") if self.response_data is None: msg = f"{self.class_name}.refresh() failed: response " msg += "does not contain DATA key. Controller response: " msg += f"{self.response}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) def _get(self, item): return self.make_boolean(self.make_none(self.response_data.get(item))) diff --git a/plugins/module_utils/common/rest_send.py b/plugins/module_utils/common/rest_send.py index d6b07fadf..6ecb4e091 100644 --- a/plugins/module_utils/common/rest_send.py +++ b/plugins/module_utils/common/rest_send.py @@ -57,10 +57,13 @@ def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED RestSend()" - self.log.debug(msg) self.ansible_module = ansible_module + self.check_mode = self.ansible_module.check_mode + + msg = f"ENTERED RestSend() check_mode: {self.check_mode}" + self.log.debug(msg) + self.params = ansible_module.params self._valid_verbs = {"GET", "POST", "PUT", "DELETE"} @@ -244,7 +247,7 @@ def failed_result(self): """ Return a result for a failed task with no changes """ - return ImageUpgradeTaskResult(None).failed_result + return ImageUpgradeTaskResult(self.ansible_module).failed_result @property def path(self): diff --git a/plugins/module_utils/image_upgrade/image_policies.py b/plugins/module_utils/image_upgrade/image_policies.py index b78d3d8c9..d2b744caa 100644 --- a/plugins/module_utils/image_upgrade/image_policies.py +++ b/plugins/module_utils/image_upgrade/image_policies.py @@ -53,8 +53,8 @@ class ImagePolicies(ImageUpgradeCommon): /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -82,14 +82,14 @@ def refresh(self): path = self.endpoints.policies_info.get("path") verb = self.endpoints.policies_info.get("verb") - self.properties["response"] = dcnm_send(self.module, verb, path) + self.properties["response"] = dcnm_send(self.ansible_module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) if not self.result["success"]: msg = f"{self.class_name}.{self.method_name}: " msg += "Bad result when retrieving image policy " msg += "information from the controller." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) data = self.response.get("DATA").get("lastOperDataObject") @@ -97,14 +97,14 @@ def refresh(self): msg = f"{self.class_name}.{self.method_name}: " msg += "Bad response when retrieving image policy " msg += "information from the controller." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) # We cannot fail_json here since dcnm_image_policy merged # state will fail if there are no policies defined. # if len(data) == 0: # msg = f"{self.class_name}.{self.method_name}: " # msg += "the controller has no defined image policies." - # self.module.fail_json(msg, **self.failed_result) + # self.ansible_module.fail_json(msg, **self.failed_result) if len(data) == 0: msg = "the controller has no defined image policies." @@ -118,7 +118,7 @@ def refresh(self): if policy_name is None: msg = f"{self.class_name}.{self.method_name}: " msg += "Cannot parse policy information from the controller." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["response_data"][policy_name] = policy @@ -133,7 +133,7 @@ def _get(self, item): msg = f"{self.class_name}.{self.method_name}: " msg += "instance.policy_name must be set before " msg += f"accessing property {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.policy_name not in self.properties["response_data"]: return None @@ -144,7 +144,7 @@ def _get(self, item): if item not in self.properties["response_data"][self.policy_name]: msg = f"{self.class_name}.{self.method_name}: " msg += f"{self.policy_name} does not have a key named {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) return self.make_boolean( self.make_none(self.properties["response_data"][self.policy_name][item]) diff --git a/plugins/module_utils/image_upgrade/image_policy_action.py b/plugins/module_utils/image_upgrade/image_policy_action.py index 0ae8c2754..d0c081511 100644 --- a/plugins/module_utils/image_upgrade/image_policy_action.py +++ b/plugins/module_utils/image_upgrade/image_policy_action.py @@ -62,8 +62,8 @@ class ImagePolicyAction(ImageUpgradeCommon): /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/image-policy/__POLICY_NAME__ """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -71,10 +71,10 @@ def __init__(self, module): self.endpoints = ApiEndpoints() self._init_properties() - self.image_policies = ImagePolicies(self.module) + self.image_policies = ImagePolicies(self.ansible_module) self.path = None self.payloads = [] - self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.module) + self.switch_issu_details = SwitchIssuDetailsBySerialNumber(self.ansible_module) self.valid_actions = {"attach", "detach", "query"} self.verb = None @@ -119,7 +119,7 @@ def build_payload(self): msg += f"{self.switch_issu_details.device_name}. " msg += "Please verify that the switch is managed by " msg += "the controller." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.payloads.append(payload) def validate_request(self): @@ -135,13 +135,13 @@ def validate_request(self): msg = f"{self.class_name}.{method_name}: " msg += "instance.action must be set before " msg += "calling commit()" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.policy_name is None: msg = f"{self.class_name}.{method_name}: " msg += "instance.policy_name must be set before " msg += "calling commit()" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.action == "query": return @@ -150,7 +150,7 @@ def validate_request(self): msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_numbers must be set before " msg += "calling commit()" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.image_policies.refresh() self.switch_issu_details.refresh() @@ -162,7 +162,7 @@ def validate_request(self): msg = f"{self.class_name}.{method_name}: " msg += f"policy {self.policy_name} does not exist on " msg += "the controller." - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) for serial_number in self.serial_numbers: self.switch_issu_details.filter = serial_number @@ -173,7 +173,7 @@ def validate_request(self): msg += f"{self.switch_issu_details.platform}. {self.policy_name} " msg += "supports the following platform(s): " msg += f"{self.image_policies.platform}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) def commit(self): """ @@ -197,7 +197,7 @@ def commit(self): else: msg = f"{self.class_name}.{method_name}: " msg += f"Unknown action {self.action}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) def _attach_policy(self): """ @@ -232,7 +232,7 @@ def _attach_policy(self): msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when attaching policy {self.policy_name} " msg += f"to switch. Payload: {payload}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) for payload in self.payloads: diff: Dict[str, Any] = {} @@ -267,7 +267,7 @@ def _detach_policy(self): msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when detaching policy {self.policy_name} " msg += f"from the following device(s): {','.join(sorted(self.serial_numbers))}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) for serial_number in self.serial_numbers: self.switch_issu_details.filter = serial_number @@ -298,7 +298,7 @@ def _query_policy(self): if not self.result_current["success"]: msg = f"{self.class_name}.{method_name}: " msg += f"Bad result when querying image policy {self.policy_name}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.query_result = self.response_current.get("DATA") self.diff = self.response_current @@ -346,7 +346,7 @@ def action(self, value): msg += "instance.action must be one of " msg += f"{','.join(sorted(self.valid_actions))}. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["action"] = value @property @@ -380,10 +380,10 @@ def serial_numbers(self, value): msg += "instance.serial_numbers must be a " msg += "python list of switch serial numbers. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if len(value) == 0: msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_numbers must contain at least one " msg += "switch serial number." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value diff --git a/plugins/module_utils/image_upgrade/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py index 6e0bc0399..5622db65b 100644 --- a/plugins/module_utils/image_upgrade/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -98,8 +98,8 @@ class ImageStage(ImageUpgradeCommon): ] """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -110,11 +110,11 @@ def __init__(self, module): self.verb = self.endpoints.image_stage.get("verb") self.payload = None - self.rest_send = RestSend(self.module) + self.rest_send = RestSend(self.ansible_module) self._init_properties() self.serial_numbers_done = set() self.controller_version = None - self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) + self.issu_detail = SwitchIssuDetailsBySerialNumber(self.ansible_module) def _init_properties(self): # self.properties is already initialized in the parent class @@ -130,7 +130,7 @@ def _populate_controller_version(self): 1. This cannot go into ImageUpgradeCommon() due to circular imports resulting in RecursionError """ - instance = ControllerVersion(self.module) + instance = ControllerVersion(self.ansible_module) instance.refresh() self.controller_version = instance.version @@ -164,7 +164,7 @@ def validate_serial_numbers(self): msg += f"{self.issu_detail.serial_number}. " msg += "Check the switch connectivity to the controller " msg += "and try again." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) def commit(self) -> None: """ @@ -183,7 +183,7 @@ def commit(self) -> None: msg = f"{self.class_name}.{method_name}: " msg += "call instance.serial_numbers " msg += "before calling commit." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if len(self.serial_numbers) == 0: msg = "No files to stage." @@ -234,7 +234,7 @@ def commit(self) -> None: msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.result_current}. " msg += f"Controller response: {self.response_current}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self._wait_for_image_stage_to_complete() @@ -283,7 +283,7 @@ def _wait_for_current_actions_to_complete(self): msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) def _wait_for_image_stage_to_complete(self): """ @@ -316,7 +316,7 @@ def _wait_for_image_stage_to_complete(self): msg += f"Seconds remaining {timeout}: stage image failed " msg += f"for {device_name}, {serial_number}, {ip_address}. " msg += f"image staged percent: {staged_percent}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if staged_status == "Success": self.serial_numbers_done.add(serial_number) @@ -335,7 +335,7 @@ def _wait_for_image_stage_to_complete(self): msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) @property def serial_numbers(self): @@ -352,7 +352,7 @@ def serial_numbers(self, value): if not isinstance(value, list): msg = f"{self.class_name}.{method_name}: " msg += "must be a python list of switch serial numbers." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value @property @@ -370,11 +370,11 @@ def check_interval(self, value): msg += f"Got value {value} of type {type(value)}." # isinstance(True, int) is True so we need to check for bool first if isinstance(value, bool): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if value < 0: - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @property @@ -392,9 +392,9 @@ def check_timeout(self, value): msg += f"Got value {value} of type {type(value)}." # isinstance(True, int) is True so we need to check for bool first if isinstance(value, bool): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if value < 0: - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_upgrade/image_upgrade.py b/plugins/module_utils/image_upgrade/image_upgrade.py index 737584d5b..b92dbe13d 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade.py +++ b/plugins/module_utils/image_upgrade/image_upgrade.py @@ -131,17 +131,19 @@ class ImageUpgrade(ImageUpgradeCommon): "3" """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ImageUpgrade()") + msg = "ENTERED ImageUpgrade(): " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) self.endpoints = ApiEndpoints() - self.install_options = ImageInstallOptions(self.module) - self.rest_send = RestSend(self.module) - self.issu_detail = SwitchIssuDetailsByIpAddress(self.module) + self.install_options = ImageInstallOptions(self.ansible_module) + self.rest_send = RestSend(self.ansible_module) + self.issu_detail = SwitchIssuDetailsByIpAddress(self.ansible_module) self.ipv4_done = set() self.ipv4_todo = set() self.payload: Dict[str, Any] = {} @@ -205,7 +207,7 @@ def _validate_devices(self) -> None: if self.devices is None: msg = f"{self.class_name}.{method_name}: " msg += "call instance.devices before calling commit." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.issu_detail.refresh() for device in self.devices: @@ -278,7 +280,7 @@ def _build_payload_issu_upgrade(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "upgrade.nxos must be a boolean. " msg += f"Got {nxos_upgrade}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.payload["issuUpgrade"] = nxos_upgrade def _build_payload_issu_options_1(self, device) -> None: @@ -301,7 +303,7 @@ def _build_payload_issu_options_1(self, device) -> None: msg += "options.nxos.mode must be one of " msg += f"{sorted(self.valid_nxos_mode)}. " msg += f"Got {nxos_mode}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) verify_nxos_mode_list = [] if nxos_mode == "non_disruptive": @@ -326,7 +328,7 @@ def _build_payload_issu_options_2(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.nxos.bios_force must be a boolean. " msg += f"Got {bios_force}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.payload["issuUpgradeOptions2"] = {} self.payload["issuUpgradeOptions2"]["biosForce"] = bios_force @@ -343,7 +345,7 @@ def _build_payload_epld(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "upgrade.epld must be a boolean. " msg += f"Got {epld_upgrade}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) epld_module = device.get("options").get("epld").get("module") epld_golden = device.get("options").get("epld").get("golden") @@ -353,7 +355,7 @@ def _build_payload_epld(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.epld.golden must be a boolean. " msg += f"Got {epld_golden}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if epld_golden is True and device.get("upgrade").get("nxos") is True: msg = f"{self.class_name}.{method_name}: " @@ -362,7 +364,7 @@ def _build_payload_epld(self, device) -> None: msg += "If options.epld.golden is True " msg += "all other upgrade options, e.g. upgrade.nxos, " msg += "must be False." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if epld_module != "ALL": try: @@ -371,7 +373,7 @@ def _build_payload_epld(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.epld.module must either be 'ALL' " msg += f"or an integer. Got {epld_module}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.payload["epldUpgrade"] = epld_upgrade self.payload["epldOptions"] = {} @@ -390,7 +392,7 @@ def _build_payload_reboot(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "reboot must be a boolean. " msg += f"Got {reboot}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.payload["reboot"] = reboot def _build_payload_reboot_options(self, device) -> None: @@ -407,14 +409,14 @@ def _build_payload_reboot_options(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.config_reload must be a boolean. " msg += f"Got {config_reload}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) write_erase = self.make_boolean(write_erase) if not isinstance(write_erase, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.reboot.write_erase must be a boolean. " msg += f"Got {write_erase}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.payload["rebootOptions"] = {} self.payload["rebootOptions"]["configReload"] = config_reload @@ -438,14 +440,14 @@ def _build_payload_package(self, device) -> None: msg = f"{self.class_name}.{method_name}: " msg += "options.package.install must be a boolean. " msg += f"Got {package_install}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) package_uninstall = self.make_boolean(package_uninstall) if not isinstance(package_uninstall, bool): msg = f"{self.class_name}.{method_name}: " msg += "options.package.uninstall must be a boolean. " msg += f"Got {package_uninstall}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) # Yes, these keys are misspelled. The controller # wants them to be misspelled. Need to keep an @@ -511,7 +513,7 @@ def commit(self) -> None: msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.result_current}. " msg += f"Controller response: {self.response_current}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(self.payload) @@ -555,7 +557,7 @@ def _wait_for_current_actions_to_complete(self): msg += f"{','.join(sorted(self.ipv4_todo))}. " msg += "check the device(s) to determine the cause " msg += "(e.g. show install all status)." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) def _wait_for_image_upgrade_to_complete(self): """ @@ -595,7 +597,7 @@ def _wait_for_image_upgrade_to_complete(self): msg += "Operations > Image Management > Devices > View Details. " msg += "And/or check the devices " msg += "(e.g. show install all status)." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if upgrade_status == "Success": self.ipv4_done.add(ipv4) @@ -614,7 +616,7 @@ def _wait_for_image_upgrade_to_complete(self): msg += "Operations > Image Management > Devices > View Details. " msg += "And/or check the device(s) " msg += "(e.g. show install all status)." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) # setter properties @property @@ -632,7 +634,7 @@ def bios_force(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.bios_force must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["bios_force"] = value @property @@ -650,7 +652,7 @@ def config_reload(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.config_reload must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["config_reload"] = value @property @@ -675,20 +677,20 @@ def devices(self, value: List[Dict]): msg = f"{self.class_name}.{method_name}: " msg += "instance.devices must be a python list of dict. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) for device in value: if not isinstance(device, dict): msg = f"{self.class_name}.{method_name}: " msg += "instance.devices must be a python list of dict. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if "ip_address" not in device: msg = f"{self.class_name}.{method_name}: " msg += "instance.devices must be a python list of dict, " msg += "where each dict contains the following keys: " msg += "ip_address. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["devices"] = value @property @@ -706,7 +708,7 @@ def disruptive(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.disruptive must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["disruptive"] = value @property @@ -724,7 +726,7 @@ def epld_golden(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.epld_golden must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["epld_golden"] = value @property @@ -742,7 +744,7 @@ def epld_upgrade(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.epld_upgrade must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["epld_upgrade"] = value @property @@ -770,7 +772,7 @@ def epld_module(self, value): if not isinstance(value, int) and value != "ALL": msg = f"{self.class_name}.{method_name}: " msg += "instance.epld_module must be an integer or 'ALL'" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["epld_module"] = value @property @@ -788,7 +790,7 @@ def force_non_disruptive(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.force_non_disruptive must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["force_non_disruptive"] = value @property @@ -806,7 +808,7 @@ def non_disruptive(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.non_disruptive must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["non_disruptive"] = value @property @@ -824,7 +826,7 @@ def package_install(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.package_install must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["package_install"] = value @property @@ -842,7 +844,7 @@ def package_uninstall(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.package_uninstall must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["package_uninstall"] = value @property @@ -860,7 +862,7 @@ def reboot(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.reboot must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["reboot"] = value @property @@ -878,7 +880,7 @@ def write_erase(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += "instance.write_erase must be a boolean." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["write_erase"] = value @property @@ -896,9 +898,9 @@ def check_interval(self, value): # isinstance(False, int) returns True, so we need first # to test for this and fail_json specifically for bool values. if isinstance(value, bool): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @property @@ -916,7 +918,7 @@ def check_timeout(self, value): # isinstance(False, int) returns True, so we need first # to test for this and fail_json specifically for bool values. if isinstance(value, bool): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_upgrade/image_upgrade_common.py b/plugins/module_utils/image_upgrade/image_upgrade_common.py index 3434fcca6..670aa0bcc 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_common.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_common.py @@ -43,15 +43,18 @@ def __init__(self, module): ... """ - def __init__(self, module): + def __init__(self, ansible_module): self.class_name = self.__class__.__name__ + self.ansible_module = ansible_module + self.check_mode = self.ansible_module.check_mode self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED ImageUpgradeCommon()" + msg = "ENTERED ImageUpgradeCommon() " + msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self.module = module - self.params = module.params + + self.params = ansible_module.params self.properties = {} self.properties["changed"] = False @@ -102,7 +105,7 @@ def dcnm_send_with_retry(self, verb: str, path: str, payload=None): if payload is None: msg = f"{caller}: Calling dcnm_send: verb {verb}, path {path}" self.log.debug(msg) - response = self.dcnm_send(self.module, verb, path) + response = self.dcnm_send(self.ansible_module, verb, path) else: msg = ( f"{caller}: Calling dcnm_send: verb {verb}, path {path}, payload: " @@ -110,7 +113,7 @@ def dcnm_send_with_retry(self, verb: str, path: str, payload=None): msg += f"{json.dumps(payload, indent=4, sort_keys=True)}" self.log.debug(msg) response = self.dcnm_send( - self.module, verb, path, data=json.dumps(payload) + self.ansible_module, verb, path, data=json.dumps(payload) ) self.response_current = copy.deepcopy(response) @@ -157,7 +160,7 @@ def _handle_unknown_request_verbs(self, response, verb): msg = f"{self.class_name}.{method_name}: " msg += f"Unknown request verb ({verb}) for response {response}." - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) def _handle_get_response(self, response): """ @@ -249,7 +252,7 @@ def failed_result(self): """ Return a result for a failed task with no changes """ - return ImageUpgradeTaskResult(None).failed_result + return ImageUpgradeTaskResult(self.ansible_module).failed_result @property def changed(self): @@ -264,7 +267,7 @@ def changed(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a bool. Got {value}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) self.properties["changed"] = value @property @@ -280,7 +283,7 @@ def diff(self, value): if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a dict. Got {value}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) self.properties["diff"].append(value) @property @@ -298,7 +301,7 @@ def failed(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a bool. Got {value}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) self.properties["failed"] = value @property @@ -318,7 +321,7 @@ def response_current(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a dict. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["response_current"] = value @property @@ -338,7 +341,7 @@ def response(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a dict. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["response"].append(value) @property @@ -369,7 +372,7 @@ def result(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a dict. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["result"].append(value) @property @@ -389,7 +392,7 @@ def result_current(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a dict. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["result_current"] = value @property @@ -409,9 +412,9 @@ def send_interval(self, value): # isinstance(False, int) returns True, so we need first # to test for this and fail_json specifically for bool values. if isinstance(value, bool): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["send_interval"] = value @property @@ -431,9 +434,9 @@ def timeout(self, value): # isinstance(False, int) returns True, so we need first # to test for this and fail_json specifically for bool values. if isinstance(value, bool): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["timeout"] = value @property @@ -451,5 +454,5 @@ def unit_test(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a bool(). Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["unit_test"] = value diff --git a/plugins/module_utils/image_upgrade/image_upgrade_task_result.py b/plugins/module_utils/image_upgrade/image_upgrade_task_result.py index 0518db6ca..5bef99f8a 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_task_result.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_task_result.py @@ -30,9 +30,13 @@ class ImageUpgradeTaskResult: def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module + self.check_mode = self.ansible_module.check_mode self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ImageUpgradeTaskResult()") + + msg = "ENTERED ImageUpgradeTaskResult(): " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) # Used in did_anything_change() to determine if any diffs have been # appended to the diff lists. @@ -77,6 +81,9 @@ def did_anything_change(self): """ return True if diffs have been appended to any of the diff lists. """ + if self.check_mode is True: + self.log.debug("check_mode is True. No changes made.") + return False for key in self.diff_properties: # skip query state diffs if key == "diff_issu_status": diff --git a/plugins/module_utils/image_upgrade/image_validate.py b/plugins/module_utils/image_upgrade/image_validate.py index 91d2cf668..c0faaa1ff 100644 --- a/plugins/module_utils/image_upgrade/image_validate.py +++ b/plugins/module_utils/image_upgrade/image_validate.py @@ -68,15 +68,15 @@ class ImageValidate(ImageUpgradeCommon): SwitchIssuDetailsBySerialNumber. """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED ImageValidate()") self.endpoints = ApiEndpoints() - self.rest_send = RestSend(self.module) + self.rest_send = RestSend(self.ansible_module) self.path = self.endpoints.image_validate.get("path") self.verb = self.endpoints.image_validate.get("verb") @@ -84,7 +84,7 @@ def __init__(self, module): self.serial_numbers_done: Set[str] = set() self._init_properties() - self.issu_detail = SwitchIssuDetailsBySerialNumber(self.module) + self.issu_detail = SwitchIssuDetailsBySerialNumber(self.ansible_module) def _init_properties(self) -> None: """ @@ -141,7 +141,7 @@ def validate_serial_numbers(self) -> None: msg += f"{self.issu_detail.serial_number}. " msg += "If this persists, check the switch connectivity to " msg += "the controller and try again." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) def build_payload(self) -> None: """ @@ -206,7 +206,7 @@ def commit(self) -> None: msg = f"{self.class_name}.{method_name}: " msg += f"failed: {self.result_current}. " msg += f"Controller response: {self.response_current}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["response_data"] = self.response self._wait_for_image_validate_to_complete() @@ -259,7 +259,7 @@ def _wait_for_current_actions_to_complete(self) -> None: msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) def _wait_for_image_validate_to_complete(self) -> None: """ @@ -299,7 +299,7 @@ def _wait_for_image_validate_to_complete(self) -> None: msg += "check Operations > Image Management > " msg += "Devices > View Details > Validate on the " msg += "controller GUI for more details." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if validated_status == "Success": self.serial_numbers_done.add(serial_number) @@ -317,7 +317,7 @@ def _wait_for_image_validate_to_complete(self) -> None: msg += f"{','.join(sorted(self.serial_numbers_done))}, " msg += "serial_numbers_todo: " msg += f"{','.join(sorted(serial_numbers_todo))}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) @property def serial_numbers(self) -> List[str]: @@ -337,7 +337,7 @@ def serial_numbers(self, value: List[str]): msg += "instance.serial_numbers must be a " msg += "python list of switch serial numbers. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["serial_numbers"] = value @@ -357,7 +357,7 @@ def non_disruptive(self, value): msg = f"{self.class_name}.{self.method_name}: " msg += "instance.non_disruptive must be a boolean. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["non_disruptive"] = value @@ -376,11 +376,11 @@ def check_interval(self, value): msg += f"Got value {value} of type {type(value)}." # isinstance(True, int) is True so we need to check for bool first if isinstance(value, bool): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if value < 0: - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["check_interval"] = value @property @@ -398,9 +398,9 @@ def check_timeout(self, value): msg += f"Got value {value} of type {type(value)}." # isinstance(True, int) is True so we need to check for bool first if isinstance(value, bool): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if not isinstance(value, int): - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if value < 0: - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["check_timeout"] = value diff --git a/plugins/module_utils/image_upgrade/install_options.py b/plugins/module_utils/image_upgrade/install_options.py index 581941df1..b129bac69 100644 --- a/plugins/module_utils/image_upgrade/install_options.py +++ b/plugins/module_utils/image_upgrade/install_options.py @@ -140,8 +140,8 @@ class ImageInstallOptions(ImageUpgradeCommon): } """ - def __init__(self, module) -> None: - super().__init__(module) + def __init__(self, ansible_module) -> None: + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -181,13 +181,13 @@ def _validate_refresh_parameters(self) -> None: msg = f"{self.class_name}.{method_name}: " msg += "instance.policy_name must be set before " msg += "calling refresh()" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.serial_number is None: msg = f"{self.class_name}.{method_name}: " msg += "instance.serial_number must be set before " msg += "calling refresh()" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) def refresh(self) -> None: """ @@ -230,7 +230,7 @@ def refresh(self) -> None: self.log.debug(msg) response = dcnm_send( - self.module, self.verb, self.path, data=json.dumps(self.payload) + self.ansible_module, self.verb, self.path, data=json.dumps(self.payload) ) self.properties["response_data"] = response.get("DATA", {}) @@ -246,14 +246,14 @@ def refresh(self) -> None: msg += "Bad result when retrieving install-options from " msg += f"the controller. Controller response: {self.response_current}. " if self.response_data.get("error", None) is None: - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if "does not have package to continue" in self.response_data.get( "error", "" ): msg += f"Possible cause: Image policy {self.policy_name} does not have " msg += "a package defined, and package_install is set to " msg += f"True in the playbook for device {self.serial_number}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.response = copy.deepcopy(self.response_current) if self.response_data.get("compatibilityStatusList") is None: @@ -307,7 +307,7 @@ def policy_name(self, value): if not isinstance(value, str): msg = f"{self.class_name}.{method_name}: " msg += f"instance.policy_name must be a string. Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["policy_name"] = value @property @@ -341,7 +341,7 @@ def issu(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"issu must be a boolean value. Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["issu"] = value @property @@ -363,7 +363,7 @@ def epld(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"epld must be a boolean value. Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["epld"] = value @property @@ -385,7 +385,7 @@ def package_install(self, value): msg = f"{self.class_name}.{method_name}: " msg += "package_install must be a boolean value. " msg += f"Got {value}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) self.properties["package_install"] = value # Getter properties diff --git a/plugins/module_utils/image_upgrade/switch_details.py b/plugins/module_utils/image_upgrade/switch_details.py index d79dcb312..65e18e681 100644 --- a/plugins/module_utils/image_upgrade/switch_details.py +++ b/plugins/module_utils/image_upgrade/switch_details.py @@ -50,8 +50,8 @@ class SwitchDetails(ImageUpgradeCommon): /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -61,7 +61,7 @@ def __init__(self, module): self.path = self.endpoints.switches_info.get("path") self.verb = self.endpoints.switches_info.get("verb") - self.rest_send = RestSend(self.module) + self.rest_send = RestSend(self.ansible_module) self._init_properties() @@ -100,7 +100,7 @@ def refresh(self): msg = f"{self.class_name}.{method_name}: " msg += "Unable to retrieve switch information from the controller. " msg += f"Got response {self.response_current}" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) data = self.response_current.get("DATA") self.properties["info"] = {} @@ -117,17 +117,17 @@ def _get(self, item): msg = f"{self.class_name}.{method_name}: " msg += "set instance.ip_address before accessing " msg += f"property {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.ip_address not in self.properties["info"]: msg = f"{self.class_name}.{method_name}: " msg += f"{self.ip_address} does not exist on the controller." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if item not in self.properties["info"][self.ip_address]: msg = f"{self.class_name}.{method_name}: " msg += f"{self.ip_address} does not have a key named {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) return self.make_boolean( self.make_none(self.properties["info"][self.ip_address].get(item)) diff --git a/plugins/module_utils/image_upgrade/switch_issu_details.py b/plugins/module_utils/image_upgrade/switch_issu_details.py index 9bc924eb4..95bb6b88f 100644 --- a/plugins/module_utils/image_upgrade/switch_issu_details.py +++ b/plugins/module_utils/image_upgrade/switch_issu_details.py @@ -88,8 +88,8 @@ class SwitchIssuDetails(ImageUpgradeCommon): """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -120,7 +120,7 @@ def refresh_super(self) -> None: msg = f"verb: {verb}, path {path}" self.log.debug(msg) - self.response_current = dcnm_send(self.module, verb, path) + self.response_current = dcnm_send(self.ansible_module, verb, path) self.result_current = self._handle_response(self.response_current, verb) msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" @@ -136,19 +136,19 @@ def refresh_super(self) -> None: msg = f"{self.class_name}.{method_name}: " msg += "Bad result when retriving switch " msg += "information from the controller" - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) data = self.response_current.get("DATA").get("lastOperDataObject") if data is None: msg = f"{self.class_name}.{method_name}: " msg += "The controller has no switch ISSU information." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if len(data) == 0: msg = f"{self.class_name}.{method_name}: " msg += "The controller has no switch ISSU information." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) @property def actions_in_progress(self): @@ -659,8 +659,8 @@ class SwitchIssuDetailsByIpAddress(SwitchIssuDetails): See SwitchIssuDetails for more details. """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -689,17 +689,17 @@ def _get(self, item): msg = f"{self.class_name}.{method_name}: " msg += "set instance.filter to a switch ipAddress " msg += f"before accessing property {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.filter) is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} does not exist on the controller." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.data_subclass[self.filter].get(item) is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} unknown property name: {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) return self.make_none( self.make_boolean(self.data_subclass[self.filter].get(item)) @@ -747,8 +747,8 @@ class SwitchIssuDetailsBySerialNumber(SwitchIssuDetails): """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -778,18 +778,18 @@ def _get(self, item): msg = f"{self.class_name}.{method_name}: " msg += "set instance.filter to a switch serialNumber " msg += f"before accessing property {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.filter) is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} does not exist " msg += "on the controller." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.data_subclass[self.filter].get(item) is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} unknown property name: {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) return self.make_none( self.make_boolean(self.data_subclass[self.filter].get(item)) @@ -836,8 +836,8 @@ class SwitchIssuDetailsByDeviceName(SwitchIssuDetails): """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -866,18 +866,18 @@ def _get(self, item): msg = f"{self.class_name}.{method_name}: " msg += "set instance.filter to a switch deviceName " msg += f"before accessing property {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.data_subclass.get(self.filter) is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} does not exist " msg += "on the controller." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) if self.data_subclass[self.filter].get(item) is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} unknown property name: {item}." - self.module.fail_json(msg, **self.failed_result) + self.ansible_module.fail_json(msg, **self.failed_result) return self.make_none( self.make_boolean(self.data_subclass[self.filter].get(item)) diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 77f001e18..385b78759 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -449,8 +449,8 @@ class ImageUpgradeTask(ImageUpgradeCommon): query: return switch issu details for one or more devices """ - def __init__(self, module): - super().__init__(module) + def __init__(self, ansible_module): + super().__init__(ansible_module) self.class_name = self.__class__.__name__ method_name = inspect.stack()[0][3] @@ -467,13 +467,13 @@ def __init__(self, module): self.path = None self.verb = None - self.config = module.params.get("config", {}) + self.config = ansible_module.params.get("config", {}) if not isinstance(self.config, dict): msg = f"{self.class_name}.{method_name}: " msg += "expected dict type for self.config. " msg += f"got {type(self.config).__name__}" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) self.check_mode = False @@ -481,11 +481,11 @@ def __init__(self, module): self.want = [] self.need = [] - self.task_result = ImageUpgradeTaskResult(self.module) + self.task_result = ImageUpgradeTaskResult(self.ansible_module) self.task_result.changed = False - self.switch_details = SwitchDetails(self.module) - self.image_policies = ImagePolicies(self.module) + self.switch_details = SwitchDetails(self.ansible_module) + self.image_policies = ImagePolicies(self.ansible_module) def get_have(self) -> None: """ @@ -495,7 +495,7 @@ def get_have(self) -> None: """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.have = SwitchIssuDetailsByIpAddress(self.module) + self.have = SwitchIssuDetailsByIpAddress(self.ansible_module) self.have.refresh() def get_want(self) -> None: @@ -525,7 +525,7 @@ def get_want(self) -> None: if len(self.want) == 0: self.task_result.result["changed"] = False - self.module.exit_json(**self.task_result.module_result) + self.ansible_module.exit_json(**self.task_result.module_result) def _build_idempotent_want(self, want) -> None: """ @@ -618,7 +618,7 @@ def _build_idempotent_want(self, want) -> None: # Get relevant install options from the controller # based on the options in our idempotent_want item - instance = ImageInstallOptions(self.module) + instance = ImageInstallOptions(self.ansible_module) instance.policy_name = self.idempotent_want["policy"] instance.serial_number = self.have.serial_number @@ -727,16 +727,16 @@ def get_need_query(self) -> None: def _build_params_spec(self) -> Dict[str, Any]: method_name = inspect.stack()[0][3] - if self.module.params["state"] == "merged": + if self.ansible_module.params["state"] == "merged": return self._build_params_spec_for_merged_state() - if self.module.params["state"] == "deleted": + if self.ansible_module.params["state"] == "deleted": return self._build_params_spec_for_merged_state() - if self.module.params["state"] == "query": + if self.ansible_module.params["state"] == "query": return self._build_params_spec_for_query_state() msg = f"{self.class_name}.{method_name}: " - msg += f"Unsupported state: {self.module.params['state']}" - self.module.fail_json(msg) + msg += f"Unsupported state: {self.ansible_module.params['state']}" + self.ansible_module.fail_json(msg) @staticmethod def _build_params_spec_for_merged_state() -> Dict[str, Any]: @@ -906,7 +906,7 @@ def _merge_global_and_switch_configs(self, config) -> None: if not config.get("switches"): msg = f"{self.class_name}.{method_name}: " msg += "playbook is missing list of switches" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) self.switch_configs = [] merged_configs = [] @@ -923,7 +923,7 @@ def _merge_global_and_switch_configs(self, config) -> None: msg = f"switch PRE_MERGE : {json.dumps(switch, indent=4, sort_keys=True)}" self.log.debug(msg) - merge_dicts = MergeDicts(self.module) + merge_dicts = MergeDicts(self.ansible_module) merge_dicts.dict1 = global_config merge_dicts.dict2 = switch merge_dicts.commit() @@ -942,7 +942,7 @@ def _merge_defaults_to_switch_configs(self) -> None: """ configs_to_merge = copy.copy(self.switch_configs) merged_configs = [] - merge = ParamsMergeDefaults(self.module) + merge = ParamsMergeDefaults(self.ansible_module) merge.params_spec = self._build_params_spec() for switch_config in configs_to_merge: merge.parameters = switch_config @@ -959,7 +959,7 @@ def _validate_switch_configs(self) -> None: Callers: - self.get_want """ - validator = ParamsValidate(self.module) + validator = ParamsValidate(self.ansible_module) validator.params_spec = self._build_params_spec() for switch in self.switch_configs: @@ -998,7 +998,7 @@ def _attach_or_detach_image_policy(self, action=None) -> None: self.switch_details.serial_number ) - instance = ImagePolicyAction(self.module) + instance = ImagePolicyAction(self.ansible_module) if len(serial_numbers_to_update) == 0: msg = f"No policies to {action}" self.log.debug(msg) @@ -1044,7 +1044,7 @@ def _stage_images(self, serial_numbers) -> None: msg = f"serial_numbers: {serial_numbers}" self.log.debug(msg) - instance = ImageStage(self.module) + instance = ImageStage(self.ansible_module) instance.serial_numbers = serial_numbers instance.commit() for diff in instance.diff: @@ -1064,7 +1064,7 @@ def _validate_images(self, serial_numbers) -> None: msg = f"serial_numbers: {serial_numbers}" self.log.debug(msg) - instance = ImageValidate(self.module) + instance = ImageValidate(self.ansible_module) instance.serial_numbers = serial_numbers instance.commit() for diff in instance.diff: @@ -1112,7 +1112,7 @@ def _verify_install_options(self, devices) -> None: if len(devices) == 0: return - install_options = ImageInstallOptions(self.module) + install_options = ImageInstallOptions(self.ansible_module) self.switch_details.refresh() verify_devices = copy.deepcopy(devices) @@ -1143,7 +1143,7 @@ def _verify_install_options(self, devices) -> None: msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " msg += "NX-OS image" - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) msg = f"install_options.epld: {install_options.epld}" self.log.debug(msg) @@ -1160,7 +1160,7 @@ def _verify_install_options(self, devices) -> None: msg += f"{device['ip_address']}, but the image policy " msg += f"{install_options.policy_name} does not contain an " msg += "EPLD image." - self.module.fail_json(msg) + self.ansible_module.fail_json(msg) def needs_epld_upgrade(self, epld_modules) -> bool: """ @@ -1202,7 +1202,7 @@ def _upgrade_images(self, devices) -> None: Callers: - handle_merged_state """ - upgrade = ImageUpgrade(self.module) + upgrade = ImageUpgrade(self.ansible_module) upgrade.devices = devices upgrade.commit() for diff in upgrade.diff: @@ -1276,7 +1276,7 @@ def handle_query_state(self) -> None: Caller: main() """ - instance = SwitchIssuDetailsByIpAddress(self.module) + instance = SwitchIssuDetailsByIpAddress(self.ansible_module) instance.refresh() response_current = copy.deepcopy(instance.response_current) if "DATA" in response_current: @@ -1305,7 +1305,7 @@ def _failure(self, resp) -> None: ) res.update({"DATA": data}) - self.module.fail_json(msg=res) + self.ansible_module.fail_json(msg=res) def main(): diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index de2cb1ac4..b04044962 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -38,11 +38,13 @@ class MockAnsibleModule: """ Mock the AnsibleModule class """ + check_mode = False params = {"config": {"switches": [{"ip_address": "172.22.150.105"}]}} argument_spec = { "config": {"required": True, "type": "dict"}, "state": {"default": "merged", "choices": ["merged", "deleted", "query"]}, + "check_mode": False } supports_check_mode = True diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py index 9ad8f11aa..1bec763b7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py @@ -58,11 +58,13 @@ class MockAnsibleModule: """ Mock the AnsibleModule class """ + check_mode = False params = {"config": {"switches": [{"ip_address": "172.22.150.105"}]}} argument_spec = { "config": {"required": True, "type": "dict"}, "state": {"default": "merged", "choices": ["merged", "deleted", "query"]}, + "check_mode": False } supports_check_mode = True diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index c19a7e97f..a6cb40321 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -54,7 +54,7 @@ def test_image_upgrade_install_options_00001(image_install_options) -> None: """ with does_not_raise(): instance = image_install_options - assert instance.module == MockAnsibleModule + assert instance.ansible_module == MockAnsibleModule assert instance.class_name == "ImageInstallOptions" assert isinstance(instance.endpoints, ApiEndpoints) path = "/appcenter/cisco/ndfc/api/v1/imagemanagement" diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index ca3583cca..e9636ba18 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -53,7 +53,7 @@ def test_image_upgrade_image_policies_00001(image_policies) -> None: """ with does_not_raise(): instance = image_policies - assert instance.module == MockAnsibleModule + assert instance.ansible_module == MockAnsibleModule assert instance.class_name == "ImagePolicies" assert isinstance(instance.endpoints, ApiEndpoints) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index fdb6068c9..fa986ceda 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -72,7 +72,7 @@ def test_image_upgrade_stage_00001(image_stage) -> None: - Class attributes are initialized to expected values """ instance = image_stage - assert instance.module == MockAnsibleModule + assert instance.ansible_module == MockAnsibleModule assert instance.class_name == "ImageStage" assert isinstance(instance.properties, dict) assert isinstance(instance.serial_numbers_done, set) From 08da08daf5b1d5226c2e33167cc0f8fba3a2a986 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 4 Mar 2024 20:53:53 -1000 Subject: [PATCH 294/300] Fix PEP8 too many blank lines --- plugins/module_utils/image_upgrade/image_upgrade_common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/image_upgrade/image_upgrade_common.py b/plugins/module_utils/image_upgrade/image_upgrade_common.py index 670aa0bcc..8dd6298c1 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_common.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_common.py @@ -53,7 +53,6 @@ def __init__(self, ansible_module): msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self.params = ansible_module.params self.properties = {} From 682a20d51b4a98291cc711d38f00042e70d8ac2e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 5 Mar 2024 16:19:08 -1000 Subject: [PATCH 295/300] Add support for check_mode Also: dcnm_image_upgrade.py: Add a boolean, enable_logging, so that users don't have to comment/uncomment lines to disable/enable logging. Rename image_upgrade_utils.py to utils.py to align with the convention in dcnm_image_policy Modify the match regex in several unit test cases to align with the check_mode changes. --- plugins/module_utils/common/rest_send.py | 129 +++++++++++++-- .../image_upgrade/image_policy_action.py | 86 +++++++++- .../module_utils/image_upgrade/image_stage.py | 154 ++++++++++++++++-- .../image_upgrade/image_upgrade.py | 113 ++++++++++++- .../image_upgrade/image_upgrade_common.py | 3 +- .../image_upgrade/image_validate.py | 139 ++++++++++++++-- .../image_upgrade/switch_details.py | 15 +- plugins/modules/dcnm_image_upgrade.py | 43 +++-- ...est_image_upgrade_image_install_options.py | 6 +- .../test_image_upgrade_image_policies.py | 5 +- .../test_image_upgrade_image_policy_action.py | 16 +- .../test_image_upgrade_image_stage.py | 14 +- .../test_image_upgrade_image_upgrade.py | 12 +- ...test_image_upgrade_image_upgrade_common.py | 4 +- .../test_image_upgrade_image_upgrade_task.py | 13 +- ...image_upgrade_image_upgrade_task_result.py | 3 +- .../test_image_upgrade_image_validate.py | 9 +- .../test_image_upgrade_switch_details.py | 4 +- ...rade_switch_issu_details_by_device_name.py | 5 +- ...grade_switch_issu_details_by_ip_address.py | 5 +- ...de_switch_issu_details_by_serial_number.py | 5 +- .../{image_upgrade_utils.py => utils.py} | 3 +- 22 files changed, 665 insertions(+), 121 deletions(-) rename tests/unit/modules/dcnm/dcnm_image_upgrade/{image_upgrade_utils.py => utils.py} (99%) diff --git a/plugins/module_utils/common/rest_send.py b/plugins/module_utils/common/rest_send.py index 6ecb4e091..a78dab6f9 100644 --- a/plugins/module_utils/common/rest_send.py +++ b/plugins/module_utils/common/rest_send.py @@ -59,15 +59,15 @@ def __init__(self, ansible_module): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.ansible_module = ansible_module - self.check_mode = self.ansible_module.check_mode - msg = f"ENTERED RestSend() check_mode: {self.check_mode}" + msg = "ENTERED RestSend(): " self.log.debug(msg) self.params = ansible_module.params self._valid_verbs = {"GET", "POST", "PUT", "DELETE"} self.properties = {} + self.properties["check_mode"] = False self.properties["response"] = [] self.properties["response_current"] = {} self.properties["result"] = [] @@ -90,6 +90,70 @@ def _verify_commit_parameters(self): self.ansible_module.fail_json(msg, **self.failed_result) def commit(self): + if self.check_mode is True: + self.commit_check_mode() + else: + self.commit_normal_mode() + + def commit_check_mode(self): + """ + Simulate a dcnm_send() call for check_mode + + Properties read: + self.verb: HTTP verb e.g. GET, POST, PUT, DELETE + self.path: HTTP path e.g. http://controller_ip/path/to/endpoint + self.payload: Optional HTTP payload + + Properties written: + self.properties["response_current"]: raw simulated response + self.properties["result_current"]: result from self._handle_response() method + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"verb {self.verb}, path {self.path}." + self.log.debug(msg) + + self._verify_commit_parameters() + + self.response_current = {} + self.response_current["RETURN_CODE"] = 200 + self.response_current["METHOD"] = self.verb + self.response_current["REQUEST_PATH"] = self.path + self.response_current["MESSAGE"] = "OK" + self.response_current["DATA"] = "[simulated-check-mode-response:Success] " + self.result_current = self._handle_response(self.response_current) + + self.response = copy.deepcopy(self.response_current) + self.result = copy.deepcopy(self.result_current) + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"self.response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"self.response: " + msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"self.result_current: " + msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"self.result: " + msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def commit_normal_mode(self): """ Call dcnm_send() with retries until successful response or timeout is exceeded. @@ -104,8 +168,14 @@ def commit(self): self.properties["response"]: raw response from the controller self.properties["result"]: result from self._handle_response() method """ + method_name = inspect.stack()[0][3] caller = inspect.stack()[1][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"verb {self.verb}, path {self.path}." + self.log.debug(msg) + self._verify_commit_parameters() try: timeout = self.timeout @@ -136,31 +206,38 @@ def commit(self): self.result_current = self._handle_response(response) success = self.result_current["success"] - if success is False and self.unit_test is False: sleep(self.send_interval) timeout -= self.send_interval - self.response = copy.deepcopy(response) - self.result = copy.deepcopy(self.result_current) - msg = f"{caller}: Exiting dcnm_send_with_retry loop." msg += f"success {success}. verb {self.verb}, path {self.path}." self.log.debug(msg) - msg = f"{caller}: self.response_current " + self.response = copy.deepcopy(self.response_current) + self.result = copy.deepcopy(self.result_current) + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"self.response_current: " msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"{caller}: self.response " + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"self.response: " msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"{caller}: self.result_current " + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"self.result_current: " msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"{caller}: self.result " + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"self.result: " msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) @@ -242,6 +319,38 @@ def _handle_post_put_delete_response(self, response): result["changed"] = True return result + @property + def check_mode(self): + """ + Determines if dcnm_send should be called. + + Default: False + + If False, dcnm_send is called. Real controller responses + are returned by RestSend() + + If True, dcnm_send is not called. Simulated controller responses + are returned by RestSend() + + Discussion: + We don't set check_mode from the value of self.ansible_module.check_mode + because we want to be able to read data from the controller even when + self.ansible_module.check_mode is True. For example, SwitchIssuDetails + is a read-only operation, and we want to be able to read this data + to provide a realistic simulation of stage, validate, and upgrade + tasks. + """ + return self.properties.get("check_mode") + + @check_mode.setter + def check_mode(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be a bool(). Got {value}." + self.ansible_module.fail_json(msg, **self.failed_result) + self.properties["check_mode"] = value + @property def failed_result(self): """ diff --git a/plugins/module_utils/image_upgrade/image_policy_action.py b/plugins/module_utils/image_upgrade/image_policy_action.py index d0c081511..d992a97a8 100644 --- a/plugins/module_utils/image_upgrade/image_policy_action.py +++ b/plugins/module_utils/image_upgrade/image_policy_action.py @@ -200,6 +200,41 @@ def commit(self): self.ansible_module.fail_json(msg, **self.failed_result) def _attach_policy(self): + if self.check_mode is True: + self._attach_policy_check_mode() + else: + self._attach_policy_normal_mode() + + def _attach_policy_check_mode(self): + """ + Simulate _attach_policy() + """ + self.build_payload() + + self.path = self.endpoints.policy_attach.get("path") + self.verb = self.endpoints.policy_attach.get("verb") + + payload: Dict[str, Any] = {} + payload["mappingList"] = self.payloads + + self.response_current = {} + self.response_current["RETURN_CODE"] = 200 + self.response_current["METHOD"] = self.verb + self.response_current["REQUEST_PATH"] = self.path + self.response_current["MESSAGE"] = "OK" + self.response_current["DATA"] = "[simulated-check-mode-response:Success] " + self.result_current = self._handle_response(self.response_current, self.verb) + + for payload in self.payloads: + diff: Dict[str, Any] = {} + diff["action"] = self.action + diff["ip_address"] = payload["ipAddr"] + diff["logical_name"] = payload["hostName"] + diff["policy_name"] = payload["policyName"] + diff["serial_number"] = payload["serialNumber"] + self.diff = copy.deepcopy(diff) + + def _attach_policy_normal_mode(self): """ Attach policy_name to the switch(es) associated with serial_numbers @@ -244,11 +279,60 @@ def _attach_policy(self): self.diff = copy.deepcopy(diff) def _detach_policy(self): + if self.check_mode is True: + self._detach_policy_check_mode() + else: + self._detach_policy_normal_mode() + + def _detach_policy_check_mode(self): + """ + Simulate self._detach_policy_normal_mode() + verb: DELETE + endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy + query_params(example): ?serialNumber=FDO211218GC,FDO21120U5D + """ + method_name = inspect.stack()[0][3] + + msg = "ENTERED" + self.log.debug(msg) + + self.path = self.endpoints.policy_detach.get("path") + self.verb = self.endpoints.policy_detach.get("verb") + + query_params = ",".join(self.serial_numbers) + self.path += f"?serialNumber={query_params}" + + self.response_current = {} + self.response_current["RETURN_CODE"] = 200 + self.response_current["METHOD"] = self.verb + self.response_current["REQUEST_PATH"] = self.path + self.response_current["MESSAGE"] = "OK" + self.response_current["DATA"] = "[simulated-response:Success] " + self.result_current = self._handle_response(self.response_current, self.verb) + + if not self.result_current["success"]: + msg = f"{self.class_name}.{method_name}: " + msg += f"Bad result when detaching policy {self.policy_name} " + msg += f"from the following device(s): {','.join(sorted(self.serial_numbers))}." + self.ansible_module.fail_json(msg, **self.failed_result) + + for serial_number in self.serial_numbers: + self.switch_issu_details.filter = serial_number + diff: Dict[str, Any] = {} + diff["action"] = self.action + diff["ip_address"] = self.switch_issu_details.ip_address + diff["logical_name"] = self.switch_issu_details.device_name + diff["policy_name"] = self.policy_name + diff["serial_number"] = serial_number + self.diff = copy.deepcopy(diff) + self.changed = False + + def _detach_policy_normal_mode(self): """ Detach policy_name from the switch(es) associated with serial_numbers verb: DELETE endpoint: /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy - query_params: ?serialNumber=FDO211218GC,FDO21120U5D + query_params(example): ?serialNumber=FDO211218GC,FDO21120U5D """ method_name = inspect.stack()[0][3] diff --git a/plugins/module_utils/image_upgrade/image_stage.py b/plugins/module_utils/image_upgrade/image_stage.py index 5622db65b..9a1dada87 100644 --- a/plugins/module_utils/image_upgrade/image_stage.py +++ b/plugins/module_utils/image_upgrade/image_stage.py @@ -26,12 +26,12 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ ControllerVersion +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber @@ -103,7 +103,9 @@ def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ImageStage()") + msg = "ENTERED ImageStage() " + msg += f"check_mode {self.check_mode}" + self.log.debug(msg) self.endpoints = ApiEndpoints() self.path = self.endpoints.image_stage.get("path") @@ -125,10 +127,6 @@ def _init_properties(self): def _populate_controller_version(self): """ Populate self.controller_version with the running controller version. - - Notes: - 1. This cannot go into ImageUpgradeCommon() due to circular - imports resulting in RecursionError """ instance = ControllerVersion(self.ansible_module) instance.refresh() @@ -167,6 +165,119 @@ def validate_serial_numbers(self): self.ansible_module.fail_json(msg, **self.failed_result) def commit(self) -> None: + if self.check_mode is True: + self.commit_check_mode() + else: + self.commit_normal_mode() + + def commit_check_mode(self) -> None: + """ + Simulate a commit of the image staging request to the + controller. + """ + method_name = inspect.stack()[0][3] + + msg = f"ENTERED {self.class_name}.{method_name}" + self.log.debug(msg) + + msg = f"self.serial_numbers: {self.serial_numbers}" + self.log.debug(msg) + + if self.serial_numbers is None: + msg = f"{self.class_name}.{method_name}: " + msg += "call instance.serial_numbers " + msg += "before calling commit." + self.ansible_module.fail_json(msg, **self.failed_result) + + if len(self.serial_numbers) == 0: + msg = "No files to stage." + response_current = {"DATA": [{"key": "ALL", "value": msg}]} + self.response_current = response_current + self.response = response_current + self.response_data = response_current.get("DATA", "No Stage DATA") + self.result = {"changed": False, "success": True} + self.result_current = {"changed": False, "success": True} + return + + self.prune_serial_numbers() + self.validate_serial_numbers() + + self.payload = {} + self._populate_controller_version() + + if self.controller_version == "12.1.2e": + # Yes, version 12.1.2e wants serialNum to be misspelled + self.payload["sereialNum"] = self.serial_numbers + else: + self.payload["serialNumbers"] = self.serial_numbers + + self.rest_send.verb = self.verb + self.rest_send.path = self.path + self.rest_send.payload = self.payload + + self.rest_send.check_mode = True + + self.rest_send.commit() + + self.response_current = {} + self.response_current["DATA"] = "[simulated-check-mode-response:Success]" + self.response_current["MESSAGE"] = "OK" + self.response_current["METHOD"] = self.verb + self.response_current["REQUEST_PATH"] = self.path + self.response_current["RETURN_CODE"] = 200 + self.response = copy.deepcopy(self.response_current) + + self.response_data = self.response_current.get("DATA") + + self.result_current = self.rest_send._handle_response(self.response_current) + self.result = copy.deepcopy(self.result_current) + + msg = "payload: " + msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response: " + msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response_data: " + msg += f"{self.response_data}" + self.log.debug(msg) + + msg = "self.result: " + msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.result_current: " + msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.result_current["success"]: + msg = f"{self.class_name}.{method_name}: " + msg += f"failed: {self.result_current}. " + msg += f"Controller response: {self.response_current}" + self.ansible_module.fail_json(msg, **self.failed_result) + + for serial_number in self.serial_numbers: + self.issu_detail.filter = serial_number + diff = {} + diff["action"] = "stage" + diff["ip_address"] = self.issu_detail.ip_address + diff["logical_name"] = self.issu_detail.device_name + diff["policy"] = self.issu_detail.policy + diff["serial_number"] = serial_number + # See image_upgrade_common.py for the definition of self.diff + self.diff = copy.deepcopy(diff) + + msg = "self.diff: " + msg += f"{json.dumps(self.diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def commit_normal_mode(self) -> None: """ Commit the image staging request to the controller and wait for the images to be staged. @@ -212,6 +323,8 @@ def commit(self) -> None: self.rest_send.path = self.path self.rest_send.payload = self.payload + self.rest_send.check_mode = False + self.rest_send.commit() self.response = self.rest_send.response_current @@ -221,13 +334,28 @@ def commit(self) -> None: self.result = self.rest_send.result_current self.result_current = self.rest_send.result_current - msg = f"payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" + msg = "payload: " + msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response: " + msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"response: {json.dumps(self.response, indent=4, sort_keys=True)}" + + msg = "self.response_data: " + msg += f"{self.response_data}" self.log.debug(msg) - msg = f"result: {json.dumps(self.result, indent=4, sort_keys=True)}" + + msg = "self.result: " + msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.response_data: {self.response_data}" + + msg = "self.result_current: " + msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) if not self.result_current["success"]: @@ -249,6 +377,10 @@ def commit(self) -> None: # See image_upgrade_common.py for the definition of self.diff self.diff = copy.deepcopy(diff) + msg = "self.diff: " + msg += f"{json.dumps(self.diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + def _wait_for_current_actions_to_complete(self): """ The controller will not stage an image if there are any actions in diff --git a/plugins/module_utils/image_upgrade/image_upgrade.py b/plugins/module_utils/image_upgrade/image_upgrade.py index b92dbe13d..65de2f9ee 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade.py +++ b/plugins/module_utils/image_upgrade/image_upgrade.py @@ -25,14 +25,14 @@ from time import sleep from typing import Any, Dict, List, Set +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.install_options import \ ImageInstallOptions -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsByIpAddress @@ -456,6 +456,89 @@ def _build_payload_package(self, device) -> None: self.payload["pacakgeUnInstall"] = package_uninstall def commit(self) -> None: + if self.check_mode is True: + self.commit_check_mode() + else: + self.commit_normal_mode() + + def commit_check_mode(self) -> None: + """ + Simulate a commit of the image upgrade request to + the controller. + """ + method_name = inspect.stack()[0][3] + + self._validate_devices() + + self.rest_send.verb = self.verb + self.rest_send.path = self.path + + self.rest_send.check_mode = True + + for device in self.devices: + msg = f"device: {json.dumps(device, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self._build_payload(device) + + msg = "Calling rest_send.commit(): " + msg += f"verb {self.verb}, path: {self.path} " + msg += f"payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.rest_send.payload = self.payload + self.rest_send.commit() + + msg = "DONE rest_send.commit()" + self.log.debug(msg) + + self.response_current = {} + self.response_current["DATA"] = "[simulated-check-mode-response:Success]" + self.response_current["MESSAGE"] = "OK" + self.response_current["METHOD"] = self.verb + self.response_current["REQUEST_PATH"] = self.path + self.response_current["RETURN_CODE"] = 200 + self.response = copy.deepcopy(self.response_current) + + self.response_data = self.response_current.get("DATA") + + self.result_current = self.rest_send._handle_response(self.response_current) + self.result = copy.deepcopy(self.result_current) + + msg = "payload: " + msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response: " + msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response_data: " + msg += f"{self.response_data}" + self.log.debug(msg) + + msg = "self.result: " + msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.result_current: " + msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.result_current["success"]: + msg = f"{self.class_name}.{method_name}: " + msg += f"failed: {self.result_current}. " + msg += f"Controller response: {self.response_current}" + self.ansible_module.fail_json(msg, **self.failed_result) + + # See image_upgrade_common.py for the definition of self.diff + self.diff = copy.deepcopy(self.payload) + + def commit_normal_mode(self) -> None: """ Commit the image upgrade request to the controller and wait for the images to be upgraded. @@ -468,6 +551,11 @@ def commit(self) -> None: self.rest_send.verb = self.verb self.rest_send.path = self.path + if self.check_mode is True: + self.rest_send.check_mode = True + else: + self.rest_send.check_mode = False + for device in self.devices: msg = f"device: {json.dumps(device, indent=4, sort_keys=True)}" self.log.debug(msg) @@ -492,21 +580,28 @@ def commit(self) -> None: self.result = self.rest_send.result_current self.result_current = self.rest_send.result_current - msg = ( - f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" - ) + msg = "payload: " + msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response: " + msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.response_current: {json.dumps(self.response_current, indent=4, sort_keys=True)}" + msg = "self.response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.response_data: {self.response_data}" + msg = "self.response_data: " + msg += f"{self.response_data}" self.log.debug(msg) - msg = f"self.result: {json.dumps(self.result, indent=4, sort_keys=True)}" + msg = "self.result: " + msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}" + msg = "self.result_current: " + msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) if not self.result_current["success"]: diff --git a/plugins/module_utils/image_upgrade/image_upgrade_common.py b/plugins/module_utils/image_upgrade/image_upgrade_common.py index 8dd6298c1..d306bace7 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_common.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_common.py @@ -46,9 +46,10 @@ def __init__(self, module): def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.ansible_module = ansible_module - self.check_mode = self.ansible_module.check_mode + self.check_mode = ansible_module.check_mode self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED ImageUpgradeCommon() " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) diff --git a/plugins/module_utils/image_upgrade/image_validate.py b/plugins/module_utils/image_upgrade/image_validate.py index c0faaa1ff..908d6a2ac 100644 --- a/plugins/module_utils/image_upgrade/image_validate.py +++ b/plugins/module_utils/image_upgrade/image_validate.py @@ -25,12 +25,12 @@ from time import sleep from typing import List, Set +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber @@ -73,7 +73,9 @@ def __init__(self, ansible_module): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ImageValidate()") + msg = "ENTERED ImageValidate() " + msg += f"check_mode {self.check_mode}" + self.log.debug(msg) self.endpoints = ApiEndpoints() self.rest_send = RestSend(self.ansible_module) @@ -154,6 +156,101 @@ def build_payload(self) -> None: self.payload["nonDisruptive"] = self.non_disruptive def commit(self) -> None: + if self.check_mode is True: + self.commit_check_mode() + else: + self.commit_normal_mode() + + def commit_check_mode(self) -> None: + """ + Simulate a commit of the image validation request to the + controller. + """ + method_name = inspect.stack()[0][3] + + msg = f"ENTERED: self.serial_numbers: {self.serial_numbers}" + self.log.debug(msg) + + if len(self.serial_numbers) == 0: + msg = "No serial numbers to validate." + self.response_current = {"response": msg} + self.result_current = {"success": True} + self.response_data = {"response": msg} + self.response = self.response_current + self.result = self.result_current + return + + self.prune_serial_numbers() + self.validate_serial_numbers() + + self.build_payload() + self.rest_send.verb = self.verb + self.rest_send.path = self.path + self.rest_send.payload = self.payload + + self.rest_send.check_mode = True + + self.rest_send.commit() + + self.response_current = {} + self.response_current["DATA"] = "[simulated-check-mode-response:Success]" + self.response_current["MESSAGE"] = "OK" + self.response_current["METHOD"] = self.verb + self.response_current["REQUEST_PATH"] = self.path + self.response_current["RETURN_CODE"] = 200 + self.response = copy.deepcopy(self.response_current) + + self.response_data = self.response_current.get("DATA") + + self.result_current = self.rest_send._handle_response(self.response_current) + self.result = copy.deepcopy(self.result_current) + + msg = "self.payload: " + msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response: " + msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.response_data: " + msg += f"{self.response_data}" + self.log.debug(msg) + + msg = "self.result: " + msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.result_current: " + msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if not self.result_current["success"]: + msg = f"{self.class_name}.{method_name}: " + msg += f"failed: {self.result_current}. " + msg += f"Controller response: {self.response_current}" + self.ansible_module.fail_json(msg, **self.failed_result) + + for serial_number in self.serial_numbers: + self.issu_detail.filter = serial_number + diff = {} + diff["action"] = "validate" + diff["ip_address"] = self.issu_detail.ip_address + diff["logical_name"] = self.issu_detail.device_name + diff["policy"] = self.issu_detail.policy + diff["serial_number"] = serial_number + # See image_upgrade_common.py for the definition of self.diff + self.diff = copy.deepcopy(diff) + + msg = "self.diff: " + msg += f"{json.dumps(self.diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def commit_normal_mode(self) -> None: """ Commit the image validation request to the controller and wait for the images to be validated. @@ -181,25 +278,45 @@ def commit(self) -> None: self.rest_send.path = self.path self.rest_send.payload = self.payload + if self.check_mode is True: + self.rest_send.check_mode = True + else: + self.rest_send.check_mode = False + self.rest_send.commit() msg = f"self.rest_send.response_current: {self.rest_send.response_current}" self.log.debug(msg) - self.response = self.rest_send.response_current - self.response_current = self.rest_send.response_current + self.response_current = copy.deepcopy(self.rest_send.response_current) + self.response = copy.deepcopy(self.rest_send.response_current) self.response_data = self.response_current.get("DATA", "No Stage DATA") - self.result = self.rest_send.result_current - self.result_current = self.rest_send.result_current + self.result_current = copy.deepcopy(self.rest_send.result_current) + self.result = copy.deepcopy(self.rest_send.result_current) - msg = f"self.payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" + msg = "self.payload: " + msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}" + + msg = "self.response: " + msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.result: {json.dumps(self.result, indent=4, sort_keys=True)}" + + msg = "self.response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" self.log.debug(msg) - msg = f"self.response_data: {self.response_data}" + + msg = "self.response_data: " + msg += f"{self.response_data}" + self.log.debug(msg) + + msg = "self.result: " + msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = "self.result_current: " + msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) if not self.result_current["success"]: diff --git a/plugins/module_utils/image_upgrade/switch_details.py b/plugins/module_utils/image_upgrade/switch_details.py index 65e18e681..44f947068 100644 --- a/plugins/module_utils/image_upgrade/switch_details.py +++ b/plugins/module_utils/image_upgrade/switch_details.py @@ -22,12 +22,12 @@ import json import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend class SwitchDetails(ImageUpgradeCommon): @@ -83,10 +83,14 @@ def refresh(self): self.rest_send.path = self.path self.rest_send.commit() - msg = f"self.rest_send.response_current: {self.rest_send.response_current}" + msg = "self.rest_send.response_current: " + msg += ( + f"{json.dumps(self.rest_send.response_current, indent=4, sort_keys=True)}" + ) self.log.debug(msg) - msg = f"self.rest_send.result_current: {self.rest_send.result_current}" + msg = "self.rest_send.result_current: " + msg += f"{json.dumps(self.rest_send.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) self.response = self.rest_send.response_current @@ -107,7 +111,8 @@ def refresh(self): for switch in data: self.properties["info"][switch["ipAddress"]] = switch - msg = f"self.properties[info]: {json.dumps(self.properties['info'], indent=4, sort_keys=True)}" + msg = "self.properties[info]: " + msg += f"{json.dumps(self.properties['info'], indent=4, sort_keys=True)}" self.log.debug(msg) def _get(self, item): diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 385b78759..42c9a6b5b 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1048,10 +1048,14 @@ def _stage_images(self, serial_numbers) -> None: instance.serial_numbers = serial_numbers instance.commit() for diff in instance.diff: + msg = "adding diff to task_result.diff_stage: " + msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) self.task_result.diff_stage = copy.deepcopy(diff) for response in instance.response: - if "DATA" in response: - response.pop("DATA") + msg = "adding response to task_result.response_stage: " + msg += f"{json.dumps(response, indent=4, sort_keys=True)}" + self.log.debug(msg) self.task_result.response_stage = copy.deepcopy(response) def _validate_images(self, serial_numbers) -> None: @@ -1068,10 +1072,14 @@ def _validate_images(self, serial_numbers) -> None: instance.serial_numbers = serial_numbers instance.commit() for diff in instance.diff: + msg = "adding diff to task_result.diff_validate: " + msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) self.task_result.diff_validate = copy.deepcopy(diff) for response in instance.response: - if "DATA" in response: - response.pop("DATA") + msg = "adding response to task_result.response_validate: " + msg += f"{json.dumps(response, indent=4, sort_keys=True)}" + self.log.debug(msg) self.task_result.response_validate = copy.deepcopy(response) def _verify_install_options(self, devices) -> None: @@ -1206,11 +1214,15 @@ def _upgrade_images(self, devices) -> None: upgrade.devices = devices upgrade.commit() for diff in upgrade.diff: - self.task_result.diff_upgrade = diff + msg = "adding diff to diff_upgrade: " + msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" + self.log.debug(msg) + self.task_result.diff_upgrade = copy.deepcopy(diff) for response in upgrade.response: - msg = f"adding response to response_upgrade: {json.dumps(response, indent=4, sort_keys=True)}" + msg = "adding response to response_upgrade: " + msg += f"{json.dumps(response, indent=4, sort_keys=True)}" self.log.debug(msg) - self.task_result.response_upgrade = response + self.task_result.response_upgrade = copy.deepcopy(response) def handle_merged_state(self) -> None: """ @@ -1319,16 +1331,22 @@ def main(): ansible_module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) # Create the base/parent logger for the dcnm collection. - # To disable logging, comment out log.config = below + # To enable logging, set enable_logging to True. # log.config can be either a dictionary, or a path to a JSON file # Both dictionary and JSON file formats must be conformant with # logging.config.dictConfig and must not log to the console. # For an example configuration, see: # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json + enable_logging = False log = Log(ansible_module) - # collection_path = "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - # config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - # log.config = config_file + if enable_logging is True: + collection_path = ( + "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" + ) + config_file = ( + f"{collection_path}/plugins/module_utils/common/logging_config.json" + ) + log.config = config_file log.commit() task_module = ImageUpgradeTask(ansible_module) @@ -1347,9 +1365,6 @@ def main(): if len(task_module.need) == 0: ansible_module.exit_json(**task_module.task_result.module_result) - if ansible_module.check_mode: - ansible_module.exit_json(**task_module.task_result.module_result) - if ansible_module.params["state"] in ["merged", "deleted"]: task_module.task_result.changed = True diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py index a6cb40321..d2fa7afc3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_install_options.py @@ -34,9 +34,9 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, - image_install_options_fixture, - responses_image_install_options) +from .utils import (MockAnsibleModule, does_not_raise, + image_install_options_fixture, + responses_image_install_options) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py index e9636ba18..037b0dba0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policies.py @@ -34,9 +34,8 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ ApiEndpoints -from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, - image_policies_fixture, - responses_image_policies) +from .utils import (MockAnsibleModule, does_not_raise, image_policies_fixture, + responses_image_policies) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py index 72cde8064..2e1b94de6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_policy_action.py @@ -39,13 +39,11 @@ SwitchIssuDetailsBySerialNumber from .fixture import load_fixture -from .image_upgrade_utils import (does_not_raise, image_policies_fixture, - image_policy_action_fixture, - issu_details_by_serial_number_fixture, - responses_image_policies, - responses_image_policy_action, - responses_switch_details, - responses_switch_issu_details) +from .utils import (does_not_raise, image_policies_fixture, + image_policy_action_fixture, + issu_details_by_serial_number_fixture, + responses_image_policies, responses_image_policy_action, + responses_switch_details, responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." @@ -527,7 +525,7 @@ def mock_dcnm_send_image_upgrade_common(*args) -> Dict[str, Any]: instance.unit_test = True monkeypatch.setattr(instance, "dcnm_send", mock_dcnm_send_image_upgrade_common) - match = r"ImagePolicyAction\._detach_policy: " + match = r"ImagePolicyAction\._detach_policy_normal_mode: " match += r"Bad result when detaching policy KR5M " match += r"from the following device\(s\):" @@ -635,7 +633,7 @@ def mock_dcnm_send_image_upgrade_common(*args, **kwargs) -> Dict[str, Any]: instance.action = "attach" instance.unit_test = True - match = r"ImagePolicyAction\._attach_policy: " + match = r"ImagePolicyAction\._attach_policy_normal_mode: " match += r"Bad result when attaching policy KR5M to switch\. Payload:" with pytest.raises(AnsibleFailJson, match=match): instance.commit() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py index fa986ceda..5c9172fc2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_stage.py @@ -40,12 +40,10 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber -from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, - image_stage_fixture, - issu_details_by_serial_number_fixture, - responses_controller_version, - responses_image_stage, - responses_switch_issu_details) +from .utils import (MockAnsibleModule, does_not_raise, image_stage_fixture, + issu_details_by_serial_number_fixture, + responses_controller_version, responses_image_stage, + responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." @@ -583,7 +581,7 @@ def test_image_upgrade_stage_00060(image_stage, arg, value, context) -> None: assert instance.serial_numbers == value -MATCH_00070 = "ImageStage.commit: call instance.serial_numbers " +MATCH_00070 = "ImageStage.commit_normal_mode: call instance.serial_numbers " MATCH_00070 += "before calling commit." @@ -761,7 +759,7 @@ def mock_rest_send_image_stage(*args, **kwargs) -> Dict[str, Any]: instance = image_stage instance.serial_numbers = ["FDO21120U5D"] - match = "ImageStage.commit: failed" + match = "ImageStage.commit_normal_mode: failed" with pytest.raises(AnsibleFailJson, match=match): instance.commit() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py index 9570caa11..4b1db4f42 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade.py @@ -37,12 +37,10 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade import \ ImageUpgrade -from .image_upgrade_utils import (does_not_raise, image_upgrade_fixture, - issu_details_by_ip_address_fixture, - payloads_image_upgrade, - responses_image_install_options, - responses_image_upgrade, - responses_switch_issu_details) +from .utils import (does_not_raise, image_upgrade_fixture, + issu_details_by_ip_address_fixture, payloads_image_upgrade, + responses_image_install_options, responses_image_upgrade, + responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." @@ -1289,7 +1287,7 @@ def mock_rest_send_image_upgrade(*args, **kwargs) -> Dict[str, Any]: } ] - match = "ImageUpgrade.commit: failed: " + match = "ImageUpgrade.commit_normal_mode: failed: " match += r"\{'success': False, 'changed': False\}. " match += r"Controller response: \{'DATA': 123, " match += "'MESSAGE': 'Internal Server Error', 'METHOD': 'POST', " diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py index 62d7e7ff8..d6c9c0b19 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_common.py @@ -37,8 +37,8 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log -from .image_upgrade_utils import (does_not_raise, image_upgrade_common_fixture, - responses_image_upgrade_common) +from .utils import (does_not_raise, image_upgrade_common_fixture, + responses_image_upgrade_common) def test_image_upgrade_image_upgrade_common_00001(image_upgrade_common) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index c585b43bd..fa72109e4 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -40,14 +40,11 @@ from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ ImageUpgradeTask -from .image_upgrade_utils import (MockAnsibleModule, does_not_raise, - image_upgrade_fixture, - image_upgrade_task_fixture, - issu_details_by_ip_address_fixture, - load_playbook_config, payloads_image_upgrade, - responses_image_install_options, - responses_image_upgrade, - responses_switch_issu_details) +from .utils import (MockAnsibleModule, does_not_raise, image_upgrade_fixture, + image_upgrade_task_fixture, + issu_details_by_ip_address_fixture, load_playbook_config, + payloads_image_upgrade, responses_image_install_options, + responses_image_upgrade, responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py index cc95369fe..be64f5d93 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py @@ -36,8 +36,7 @@ from ansible_collections.cisco.dcnm.plugins.modules.dcnm_image_upgrade import \ ImageUpgradeTaskResult -from .image_upgrade_utils import (does_not_raise, - image_upgrade_task_result_fixture) +from .utils import does_not_raise, image_upgrade_task_result_fixture def test_image_upgrade_upgrade_task_result_00010(image_upgrade_task_result) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py index 3e3d9b095..9f6a9ee80 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_validate.py @@ -38,10 +38,9 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_issu_details import \ SwitchIssuDetailsBySerialNumber -from .image_upgrade_utils import (does_not_raise, image_validate_fixture, - issu_details_by_serial_number_fixture, - responses_image_validate, - responses_switch_issu_details) +from .utils import (does_not_raise, image_validate_fixture, + issu_details_by_serial_number_fixture, + responses_image_validate, responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." @@ -473,7 +472,7 @@ def mock_dcnm_send_issu_details(*args, **kwargs) -> Dict[str, Any]: with does_not_raise(): instance = image_validate instance.serial_numbers = ["FDO21120U5D"] - MATCH = "ImageValidate.commit: failed: " + MATCH = "ImageValidate.commit_normal_mode: failed: " with pytest.raises(AnsibleFailJson, match=MATCH): instance.commit() diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py index a6002f741..8953ac221 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_details.py @@ -36,8 +36,8 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.switch_details import \ SwitchDetails -from .image_upgrade_utils import (does_not_raise, responses_switch_details, - switch_details_fixture) +from .utils import (does_not_raise, responses_switch_details, + switch_details_fixture) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py index b50c02a5d..f77cce289 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_device_name.py @@ -32,9 +32,8 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from .image_upgrade_utils import (does_not_raise, - issu_details_by_device_name_fixture, - responses_switch_issu_details) +from .utils import (does_not_raise, issu_details_by_device_name_fixture, + responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py index 558b87680..a870ad180 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_ip_address.py @@ -32,9 +32,8 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from .image_upgrade_utils import (does_not_raise, - issu_details_by_ip_address_fixture, - responses_switch_issu_details) +from .utils import (does_not_raise, issu_details_by_ip_address_fixture, + responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py index 3fe693e88..f0ac2dd95 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_switch_issu_details_by_serial_number.py @@ -33,9 +33,8 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from .image_upgrade_utils import (does_not_raise, - issu_details_by_serial_number_fixture, - responses_switch_issu_details) +from .utils import (does_not_raise, issu_details_by_serial_number_fixture, + responses_switch_issu_details) PATCH_MODULE_UTILS = "ansible_collections.cisco.dcnm.plugins.module_utils." PATCH_IMAGE_UPGRADE = PATCH_MODULE_UTILS + "image_upgrade." diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/utils.py similarity index 99% rename from tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py rename to tests/unit/modules/dcnm/dcnm_image_upgrade/utils.py index 1bec763b7..357990c8a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/image_upgrade_utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/utils.py @@ -58,13 +58,14 @@ class MockAnsibleModule: """ Mock the AnsibleModule class """ + check_mode = False params = {"config": {"switches": [{"ip_address": "172.22.150.105"}]}} argument_spec = { "config": {"required": True, "type": "dict"}, "state": {"default": "merged", "choices": ["merged", "deleted", "query"]}, - "check_mode": False + "check_mode": False, } supports_check_mode = True From 285127caf97a25acc810cde75836ea27fb48116e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 5 Mar 2024 16:24:32 -1000 Subject: [PATCH 296/300] Fix pylint f-string interpolation errors. --- plugins/module_utils/common/rest_send.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/common/rest_send.py b/plugins/module_utils/common/rest_send.py index a78dab6f9..b33175c0e 100644 --- a/plugins/module_utils/common/rest_send.py +++ b/plugins/module_utils/common/rest_send.py @@ -131,25 +131,25 @@ def commit_check_mode(self): msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " - msg += f"self.response_current: " + msg += "self.response_current: " msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" self.log.debug(msg) msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " - msg += f"self.response: " + msg += "self.response: " msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" self.log.debug(msg) msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " - msg += f"self.result_current: " + msg += "self.result_current: " msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " - msg += f"self.result: " + msg += "self.result: " msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) @@ -219,25 +219,25 @@ def commit_normal_mode(self): msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " - msg += f"self.response_current: " + msg += "self.response_current: " msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" self.log.debug(msg) msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " - msg += f"self.response: " + msg += "self.response: " msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" self.log.debug(msg) msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " - msg += f"self.result_current: " + msg += "self.result_current: " msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" self.log.debug(msg) msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " - msg += f"self.result: " + msg += "self.result: " msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" self.log.debug(msg) From ce359cf6c31289b3b71f905472af7858fc073a8d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 21 Mar 2024 08:28:49 -1000 Subject: [PATCH 297/300] Pull in changes from dcnm_image_policy Since dcnm_image_upgrade is the single source of truth (prior to commiting these new modules) for everything in module_utils/common, we need to update a couple files that were enhanced during development of dcnm_image_policy. This commit removes the dependency that rest_send.py had on module_utils/image_upgrade/image_upgrade_task_result, since this file will go away in a future version of this repo. rest_send.py now imports failed_result from module_utils/common/results.py Also: module_utils/params_validate.py : fix a sanity test error with type hinting --- .../module_utils/common/params_validate.py | 4 +- plugins/module_utils/common/rest_send.py | 129 ++-- plugins/module_utils/common/results.py | 596 ++++++++++++++++++ 3 files changed, 656 insertions(+), 73 deletions(-) create mode 100644 plugins/module_utils/common/results.py diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index cf6dca2a2..9da3489f6 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -427,7 +427,7 @@ def _verify_preferred_type_param_spec_is_present( def _verify_preferred_type_for_standard_types( self, preferred_type: str, value: Any - ) -> (bool, Any): + ) -> tuple: """ If preferred_type is one of the standard python types we use isinstance() to check if we are able to convert @@ -448,7 +448,7 @@ def _verify_preferred_type_for_standard_types( def _verify_preferred_type_for_ipaddress_types( self, preferred_type: str, value: Any - ) -> (bool, Any): + ) -> tuple: """ We can't use isinstance() to verify ipaddress types. Hence, we check these types separately. diff --git a/plugins/module_utils/common/rest_send.py b/plugins/module_utils/common/rest_send.py index b33175c0e..9fd433e9e 100644 --- a/plugins/module_utils/common/rest_send.py +++ b/plugins/module_utils/common/rest_send.py @@ -22,11 +22,12 @@ import inspect import json import logging +import re from time import sleep # Using only for its failed_result property -from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_task_result import \ - ImageUpgradeTaskResult +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ dcnm_send @@ -55,13 +56,9 @@ class RestSend: def __init__(self, ansible_module): self.class_name = self.__class__.__name__ - - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.ansible_module = ansible_module - msg = "ENTERED RestSend(): " - self.log.debug(msg) + self.log = logging.getLogger(f"dcnm.{self.class_name}") self.params = ansible_module.params @@ -79,6 +76,14 @@ def __init__(self, ansible_module): self.properties["path"] = None self.properties["payload"] = None + self.check_mode = self.ansible_module.check_mode + self.state = self.params.get("state") + + msg = "ENTERED RestSend(): " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + def _verify_commit_parameters(self): if self.verb is None: msg = f"{self.class_name}._verify_commit_parameters: " @@ -90,6 +95,12 @@ def _verify_commit_parameters(self): self.ansible_module.fail_json(msg, **self.failed_result) def commit(self): + """ + Send the REST request to the controller + """ + msg = f"{self.class_name}.commit: " + msg += f"check_mode: {self.check_mode}." + self.log.debug(msg) if self.check_mode is True: self.commit_check_mode() else: @@ -123,36 +134,15 @@ def commit_check_mode(self): self.response_current["METHOD"] = self.verb self.response_current["REQUEST_PATH"] = self.path self.response_current["MESSAGE"] = "OK" - self.response_current["DATA"] = "[simulated-check-mode-response:Success] " - self.result_current = self._handle_response(self.response_current) + self.response_current["CHECK_MODE"] = True + self.response_current["DATA"] = "[simulated-check-mode-response:Success]" + self.result_current = self._handle_response( + copy.deepcopy(self.response_current) + ) self.response = copy.deepcopy(self.response_current) self.result = copy.deepcopy(self.result_current) - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += "self.response_current: " - msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += "self.response: " - msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += "self.result_current: " - msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += "self.result: " - msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" - self.log.debug(msg) - def commit_normal_mode(self): """ Call dcnm_send() with retries until successful response or timeout is exceeded. @@ -171,11 +161,6 @@ def commit_normal_mode(self): method_name = inspect.stack()[0][3] caller = inspect.stack()[1][3] - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += f"verb {self.verb}, path {self.path}." - self.log.debug(msg) - self._verify_commit_parameters() try: timeout = self.timeout @@ -184,62 +169,64 @@ def commit_normal_mode(self): success = False msg = f"{caller}: Entering commit loop. " + msg += f"timeout: {timeout}, unit_test: {self.unit_test}." self.log.debug(msg) while timeout > 0 and success is False: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Calling dcnm_send: verb {self.verb}, path {self.path}" if self.payload is None: - msg = f"{caller}: Calling dcnm_send: verb {self.verb}, path {self.path}" self.log.debug(msg) - response = dcnm_send(self.ansible_module, self.verb, self.path) + self.response_current = dcnm_send( + self.ansible_module, self.verb, self.path + ) else: - msg = f"{caller}: Calling dcnm_send: verb {self.verb}, path {self.path}, payload: " + msg += ", payload: " msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - response = dcnm_send( + self.response_current = dcnm_send( self.ansible_module, self.verb, self.path, data=json.dumps(self.payload), ) + self.result_current = self._handle_response(self.response_current) - self.response_current = copy.deepcopy(response) - self.result_current = self._handle_response(response) + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}." + self.log.debug(msg) success = self.result_current["success"] if success is False and self.unit_test is False: sleep(self.send_interval) timeout -= self.send_interval - msg = f"{caller}: Exiting dcnm_send_with_retry loop." - msg += f"success {success}. verb {self.verb}, path {self.path}." - self.log.debug(msg) + self.response_current = self._strip_invalid_json_from_response_data( + self.response_current + ) + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}." + self.log.debug(msg) self.response = copy.deepcopy(self.response_current) self.result = copy.deepcopy(self.result_current) - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += "self.response_current: " - msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += "self.response: " - msg += f"{json.dumps(self.response, indent=4, sort_keys=True)}" - self.log.debug(msg) - - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += "self.result_current: " - msg += f"{json.dumps(self.result_current, indent=4, sort_keys=True)}" - self.log.debug(msg) + def _strip_invalid_json_from_response_data(self, response): + """ + Strip "Invalid JSON response:" from response["DATA"] if present - msg = f"{self.class_name}.{method_name}: " - msg += f"caller: {caller}. " - msg += "self.result: " - msg += f"{json.dumps(self.result, indent=4, sort_keys=True)}" - self.log.debug(msg) + This just clutters up the output and is not useful to the user. + """ + if "DATA" not in response: + return response + if not isinstance(response["DATA"], str): + return response + response["DATA"] = re.sub(r"Invalid JSON response:\s*", "", response["DATA"]) + return response def _handle_response(self, response): """ @@ -256,7 +243,7 @@ def _handle_unknown_request_verbs(self, response): msg = f"{self.class_name}.{method_name}: " msg += f"Unknown request verb ({self.verb}) for response {response}." - self.ansible_module.fail_json(msg) + self.ansible_module.fail_json(msg, **self.failed_result) def _handle_get_response(self, response): """ @@ -356,7 +343,7 @@ def failed_result(self): """ Return a result for a failed task with no changes """ - return ImageUpgradeTaskResult(self.ansible_module).failed_result + return Results().failed_result @property def path(self): diff --git a/plugins/module_utils/common/results.py b/plugins/module_utils/common/results.py new file mode 100644 index 000000000..da7c989a3 --- /dev/null +++ b/plugins/module_utils/common/results.py @@ -0,0 +1,596 @@ +# Copyright (c) 2024 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 +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect +import json +import logging +from typing import Any, Dict + + +class Results: + """ + Collect results across tasks. + + Provides a mechanism to collect results across tasks. The task classes + must support this Results class. Specifically, they must implement the + following: + + 1. Accept an instantiation of Results + - Typically a class property is used for this + 2. Populate the Results instance with the results of the task + - Typically done by transferring RestSend's responses to the + Results instance + 3. Register the results of the task with Results, using: + - Results.register_task_results() + - Typically done after the task is complete + + Results should be instantiated in the main Ansible Task class and passed + to all other task classes. The task classes should populate the Results + instance with the results of the task and then register the results with + Results.register_task_results(). This may be done within a separate class + (as in the example below, where FabricDelete() class is called from the + TaskDelete() class. The Results instance can then be used to build the + final result, by calling Results.build_final_result(). + + Example Usage: + + We assume an Ansible module structure as follows: + + TaskCommon() : Common methods used by the various ansible state classes. + TaskDelete(TaskCommon) : Implements the delete state + TaskMerge(TaskCommon) : Implements the merge state + TaskQuery(TaskCommon) : Implements the query state + etc... + + In TaskCommon, Results is instantiated and, hence, is inherited by all + state classes.: + + class TaskCommon: + def __init__(self): + self.results = Results() + + @property + def results(self): + ''' + An instance of the Results class. + ''' + return self.properties["results"] + + @results.setter + def results(self, value): + self.properties["results"] = value + + + In each of the state classes (TaskDelete, TaskMerge, TaskQuery, etc...) + a class is instantiated (in the example below, FabricDelete) that + supports collecting results for the Results instance: + + class TaskDelete(TaskCommon): + def __init__(self, ansible_module): + super().__init__(ansible_module) + self.fabric_delete = FabricDelete(self.ansible_module) + + def commit(self): + ''' + delete the fabric + ''' + ... + self.fabric_delete.fabric_names = ["FABRIC_1", "FABRIC_2"] + self.fabric_delete.results = self.results + # results.register_task_results() is called within the + # commit() method of the FabricDelete class. + self.fabric_delete.commit() + + + Finally, within the main() method of the Ansible module, the final result + is built by calling Results.build_final_result(): + + if ansible_module.params["state"] == "deleted": + task = TaskDelete(ansible_module) + task.commit() + elif ansible_module.params["state"] == "merged": + task = TaskDelete(ansible_module) + task.commit() + etc... + + # Build the final result + task.results.build_final_result() + + # Call fail_json() or exit_json() based on the final result + if True in task.results.failed: + ansible_module.fail_json(**task.results.final_result) + ansible_module.exit_json(**task.results.final_result) + + + # results.final_result will be a dict with the following structure + + { + "changed": True, # or False + "failed": True, # or False + "diff": { + [], + } + "response": { + [], + } + "result": { + [], + } + "metadata": { + [], + } + } + + diff, response, and result dicts are per the Ansible DCNM Collection standard output. + + An example of a result dict would be (sequence_number is added by Results): + + { + "found": true, + "sequence_number": 0, + "success": true + } + + An examplke of a metadata dict would be (sequence_number is added by Results): + + { + "action": "merge", + "check_mode": false, + "state": "merged", + "sequence_number": 0 + } + + sequence_number indicates the order in which the task was registered with Results. + It provides a way to correlate the diff, response, result, and metadata across all + tasks. + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + msg = "ENTERED Results():" + self.log.debug(msg) + + self.diff_keys = ["deleted", "merged", "query"] + self.response_keys = ["deleted", "merged", "query"] + + self.diff_ok = [] + self.response_ok = [] + self.result_ok = [] + self.diff_nok = [] + self.response_nok = [] + self.result_nok = [] + + # Assign a unique sequence number to each registered task + self.task_sequence_number = 0 + + self.final_result = {} + self._build_properties() + + def _build_properties(self): + self.properties: Dict[str, Any] = {} + self.properties["action"] = None + self.properties["changed"] = set() + self.properties["check_mode"] = False + self.properties["diff"] = [] + self.properties["diff_current"] = {} + self.properties["failed"] = set() + self.properties["metadata"] = [] + self.properties["response"] = [] + self.properties["response_current"] = {} + self.properties["response_data"] = [] + self.properties["result"] = [] + self.properties["result_current"] = {} + self.properties["state"] = None + + def increment_task_sequence_number(self) -> None: + """ + Increment a unique task sequence number. + """ + self.task_sequence_number += 1 + msg = f"self.task_sequence_number: {self.task_sequence_number}" + self.log.debug(msg) + + def did_anything_change(self) -> bool: + """ + Return True if there were any changes + Otherwise, return False + """ + msg = f"{self.class_name}.did_anything_change(): ENTERED: " + msg += f"self.action: {self.action}, " + msg += f"self.result_current: {self.result_current}, " + msg += f"self.diff: {self.diff}" + self.log.debug(msg) + if self.check_mode is True: + return False + if self.action == "query": + msg = f"{self.class_name}.did_anything_change(): " + msg += f"self.action: {self.action}" + self.log.debug(msg) + return False + if self.result_current.get("changed", None) is True: + return True + if self.result_current.get("changed", None) is False: + return False + if len(self.diff) != 0: + return True + return False + + def register_task_result(self): + """ + Register a task's result. + + 1. Append result_current, response_current, diff_current and + metadata_current their respective lists (result, response, diff, + and metadata) + 2. Set self.changed based on current_diff. + If current_diff is empty, it is assumed that no changes were made + and self.changed is set to False. Else, self.changed is set to True. + 3. Set self.failed based on current_result. If current_result["success"] + is True, self.failed is set to False. Else, self.failed is set to True. + 4. Set self.metadata based on current_metadata. + + - self.response : list of controller responses + - self.result : list of results returned by the handler + - self.diff : list of diffs + - self.metadata : list of metadata + """ + method_name = inspect.stack()[0][3] + + msg = f"{self.class_name}.{method_name}: " + msg += f"ENTERED: self.action: {self.action}, " + msg += f"self.result_current: {self.result_current}" + self.log.debug(msg) + + self.increment_task_sequence_number() + self.metadata = self.metadata_current + self.response = self.response_current + self.result = self.result_current + self.diff = self.diff_current + + if self.did_anything_change() is False: + self.changed = False + else: + self.changed = True + + if self.result_current.get("success") is True: + self.failed = False + else: + self.failed = True + + msg = f"{self.class_name}.{method_name}: " + msg += f"self.diff: {json.dumps(self.diff, indent=4, sort_keys=True)}, " + self.log.debug(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += f"self.metadata: {json.dumps(self.metadata, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += f"self.response: {json.dumps(self.response, indent=4, sort_keys=True)}, " + self.log.debug(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += f"self.result: {json.dumps(self.result, indent=4, sort_keys=True)}, " + self.log.debug(msg) + + def build_final_result(self): + """ + Build the final result + """ + msg = f"self.changed: {self.changed}, " + msg = f"self.failed: {self.failed}, " + self.log.debug(msg) + + if True in self.failed: # pylint: disable=unsupported-membership-test + self.final_result["failed"] = True + else: + self.final_result["failed"] = False + + if True in self.changed: # pylint: disable=unsupported-membership-test + self.final_result["changed"] = True + else: + self.final_result["changed"] = False + self.final_result["diff"] = self.diff + self.final_result["response"] = self.response + self.final_result["result"] = self.result + self.final_result["metadata"] = self.metadata + + @property + def failed_result(self) -> Dict[str, Any]: + """ + return a result for a failed task with no changes + """ + result = {} + result["changed"] = False + result["failed"] = True + result["diff"] = [{}] + result["response"] = [{}] + result["result"] = [{}] + return result + + @property + def action(self): + """ + Added to results to indicate the action that was taken + """ + return self.properties["action"] + + @action.setter + def action(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.{method_name} must be a string. " + msg += f"Got {value}." + raise ValueError(msg) + msg = f"{self.class_name}.{method_name}: " + msg += f"value: {value}" + self.log.debug(msg) + self.properties["action"] = value + + @property + def changed(self) -> set: + """ + bool = whether we changed anything + + raise ValueError if value is not a bool + """ + return self.properties["changed"] + + @changed.setter + def changed(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.changed must be a bool. Got {value}" + raise ValueError(msg) + self.properties["changed"].add(value) + + @property + def check_mode(self): + """ + check_mode + """ + return self.properties["check_mode"] + + @check_mode.setter + def check_mode(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.{method_name} must be a bool. " + msg += f"Got {value}." + raise ValueError(msg) + self.properties["check_mode"] = value + + @property + def diff(self): + """ + List of dicts representing the changes made + + raise ValueError if value is not a dict + """ + return self.properties["diff"] + + @diff.setter + def diff(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.diff must be a dict. Got {value}" + raise ValueError(msg) + value["sequence_number"] = self.task_sequence_number + self.properties["diff"].append(copy.deepcopy(value)) + + @property + def diff_current(self): + """ + Return the current diff + + This is a dict of the current diff set by the handler. + """ + value = self.properties.get("diff_current") + value["sequence_number"] = self.task_sequence_number + return value + + @diff_current.setter + def diff_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.diff_current must be a dict. " + msg += f"Got {value}." + raise ValueError(msg) + self.properties["diff_current"] = value + + @property + def failed(self) -> set: + """ + A set() of Boolean values indicating whether any tasks failed + + If the set contains True, at least one task failed + If the set contains only False all tasks succeeded + + raise ValueError if value is not a bool + """ + return self.properties["failed"] + + @failed.setter + def failed(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + # Setting failed, itself failed(!) + # Add True to failed to indicate this. + self.properties["failed"].add(True) + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.failed must be a bool. Got {value}" + raise ValueError(msg) + self.properties["failed"].add(value) + + @property + def metadata(self): + """ + List of dicts representing the metadata (if any) + for each diff. + + raise ValueError if value is not a dict + """ + return self.properties["metadata"] + + @metadata.setter + def metadata(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.metadata must be a dict. Got {value}" + raise ValueError(msg) + value["sequence_number"] = self.task_sequence_number + self.properties["metadata"].append(copy.deepcopy(value)) + + @property + def metadata_current(self): + """ + Return the current metadata which is comprised of the + properties action, check_mode, and state. + """ + value = {} + value["action"] = self.action + value["check_mode"] = self.check_mode + value["state"] = self.state + value["sequence_number"] = self.task_sequence_number + return value + + @property + def response_current(self): + """ + Return the current POST response from the controller + instance.commit() must be called first. + + This is a dict of the current response from the controller. + """ + value = self.properties.get("response_current") + value["sequence_number"] = self.task_sequence_number + return value + + @response_current.setter + def response_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response_current must be a dict. " + msg += f"Got {value}." + raise ValueError(msg) + self.properties["response_current"] = value + + @property + def response(self): + """ + Return the aggregated POST response from the controller + instance.commit() must be called first. + + This is a list of responses from the controller. + """ + return self.properties.get("response") + + @response.setter + def response(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.response must be a dict. " + msg += f"Got {value}." + raise ValueError(msg) + value["sequence_number"] = self.task_sequence_number + self.properties["response"].append(copy.deepcopy(value)) + + @property + def response_data(self): + """ + Return the contents of the DATA key within current_response. + """ + return self.properties.get("response_data") + + @response_data.setter + def response_data(self, value): + self.properties["response_data"].append(value) + + @property + def result(self): + """ + Return the aggregated result from the controller + instance.commit() must be called first. + + This is a list of results from the controller. + """ + return self.properties.get("result") + + @result.setter + def result(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.result must be a dict. " + msg += f"Got {value}." + raise ValueError(msg) + value["sequence_number"] = self.task_sequence_number + self.properties["result"].append(copy.deepcopy(value)) + + @property + def result_current(self): + """ + Return the current result from the controller + instance.commit() must be called first. + + This is a dict containing the current result. + """ + value = self.properties.get("result_current") + value["sequence_number"] = self.task_sequence_number + return value + + @result_current.setter + def result_current(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "instance.result_current must be a dict. " + msg += f"Got {value}." + raise ValueError(msg) + self.properties["result_current"] = value + + @property + def state(self): + """ + Ansible state + """ + return self.properties["state"] + + @state.setter + def state(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.{method_name} must be a string. " + msg += f"Got {value}." + raise ValueError(msg) + self.properties["state"] = value From 46be48e3896217dc0c30abcb1d300f06fa8c2ed8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 21 Mar 2024 14:06:42 -1000 Subject: [PATCH 298/300] Apply changes to results.py from dcnm_image_policy This adds an "ok_result" property to results.py Results() class. --- plugins/module_utils/common/results.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/module_utils/common/results.py b/plugins/module_utils/common/results.py index da7c989a3..6d174be44 100644 --- a/plugins/module_utils/common/results.py +++ b/plugins/module_utils/common/results.py @@ -329,6 +329,19 @@ def failed_result(self) -> Dict[str, Any]: result["result"] = [{}] return result + @property + def ok_result(self) -> Dict[str, Any]: + """ + return a result for a successful task with no changes + """ + result = {} + result["changed"] = False + result["failed"] = False + result["diff"] = [{}] + result["response"] = [{}] + result["result"] = [{}] + return result + @property def action(self): """ From bf7c41ce52ed0f7dd45c365e32149a522de32f88 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 22 Mar 2024 16:33:07 -1000 Subject: [PATCH 299/300] Align result output with existing DCNM collection modules This aligns the result output with existing modules. However, it doesn't leverage the Result() class. For expediency's sake, I'll save those changes to a later version of this module since it'll be a larger effort that would require changes to many unit tests. --- .../image_upgrade_task_result.py | 52 ++- .../image_upgrade/switch_details.py | 3 + plugins/modules/dcnm_image_upgrade.py | 18 + .../dcnm_image_upgrade/tests/deleted.yaml | 360 ++++++++++++++++-- .../tests/merged_global_config.yaml | 291 ++++++++++++-- .../tests/merged_override_global_config.yaml | 284 ++++++++++++-- .../dcnm_image_upgrade/tests/query.yaml | 171 ++++----- .../test_image_upgrade_image_upgrade_task.py | 62 ++- ...image_upgrade_image_upgrade_task_result.py | 163 ++++---- 9 files changed, 1108 insertions(+), 296 deletions(-) diff --git a/plugins/module_utils/image_upgrade/image_upgrade_task_result.py b/plugins/module_utils/image_upgrade/image_upgrade_task_result.py index 5bef99f8a..f4ea1da32 100644 --- a/plugins/module_utils/image_upgrade/image_upgrade_task_result.py +++ b/plugins/module_utils/image_upgrade/image_upgrade_task_result.py @@ -18,6 +18,7 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import copy import inspect import logging @@ -63,6 +64,7 @@ def _build_properties(self): Build the properties dict() with default values """ self.properties = {} + self.properties["diff"] = [] self.properties["diff_attach_policy"] = [] self.properties["diff_detach_policy"] = [] self.properties["diff_issu_status"] = [] @@ -70,6 +72,7 @@ def _build_properties(self): self.properties["diff_upgrade"] = [] self.properties["diff_validate"] = [] + self.properties["response"] = [] self.properties["response_attach_policy"] = [] self.properties["response_issu_status"] = [] self.properties["response_detach_policy"] = [] @@ -118,7 +121,7 @@ def failed_result(self): return result @property - def module_result(self): + def module_result_orig(self): """ return a result that AnsibleModule can use """ @@ -132,7 +135,37 @@ def module_result(self): result["response"][response_key] = self.properties[key] return result + @property + def module_result(self): + """ + return a result that AnsibleModule can use + """ + result = {} + result["changed"] = self.did_anything_change() + result["diff"] = {} + result["response"] = {} + result["diff"] = copy.deepcopy(self.diff) + result["response"] = copy.deepcopy(self.response) + return result + # diff properties + @property + def diff(self): + """ + Getter for diff property + + Used for all diffs + """ + return self.properties["diff"] + + @diff.setter + def diff(self, value): + """ + Setter for diff property + """ + self._verify_is_dict(value) + self.properties["diff"].append(value) + @property def diff_attach_policy(self): """ @@ -232,6 +265,23 @@ def diff_validate(self, value): self.properties["diff_validate"].append(value) # response properties + @property + def response(self): + """ + Getter for response property + + Used for all responses + """ + return self.properties["response"] + + @response.setter + def response(self, value): + """ + Setter for response_attach_policy property + """ + self._verify_is_dict(value) + self.properties["response"].append(value) + @property def response_attach_policy(self): """ diff --git a/plugins/module_utils/image_upgrade/switch_details.py b/plugins/module_utils/image_upgrade/switch_details.py index 44f947068..40ce33373 100644 --- a/plugins/module_utils/image_upgrade/switch_details.py +++ b/plugins/module_utils/image_upgrade/switch_details.py @@ -79,6 +79,9 @@ def refresh(self): """ method_name = inspect.stack()[0][3] + # Regardless of ansible_module.check_mode, we need to get the switch details + # So, set check_mode to False + self.rest_send.check_mode = False self.rest_send.verb = self.verb self.rest_send.path = self.path self.rest_send.commit() diff --git a/plugins/modules/dcnm_image_upgrade.py b/plugins/modules/dcnm_image_upgrade.py index 42c9a6b5b..7bd6de004 100644 --- a/plugins/modules/dcnm_image_upgrade.py +++ b/plugins/modules/dcnm_image_upgrade.py @@ -1005,8 +1005,10 @@ def _attach_or_detach_image_policy(self, action=None) -> None: if action == "attach": self.task_result.diff_attach_policy = instance.diff_null + self.task_result.diff = instance.diff_null if action == "detach": self.task_result.diff_detach_policy = instance.diff_null + self.task_result.diff = instance.diff_null return for key, value in serial_numbers_to_update.items(): @@ -1018,10 +1020,16 @@ def _attach_or_detach_image_policy(self, action=None) -> None: self.task_result.response_attach_policy = copy.deepcopy( instance.response_current ) + self.task_result.response = copy.deepcopy( + instance.response_current + ) if action == "detach": self.task_result.response_detach_policy = copy.deepcopy( instance.response_current ) + self.task_result.response = copy.deepcopy( + instance.response_current + ) for diff in instance.diff: msg = ( @@ -1030,8 +1038,10 @@ def _attach_or_detach_image_policy(self, action=None) -> None: self.log.debug(msg) if action == "attach": self.task_result.diff_attach_policy = copy.deepcopy(diff) + self.task_result.diff = copy.deepcopy(diff) elif action == "detach": self.task_result.diff_detach_policy = copy.deepcopy(diff) + self.task_result.diff = copy.deepcopy(diff) def _stage_images(self, serial_numbers) -> None: """ @@ -1052,11 +1062,13 @@ def _stage_images(self, serial_numbers) -> None: msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" self.log.debug(msg) self.task_result.diff_stage = copy.deepcopy(diff) + self.task_result.diff = copy.deepcopy(diff) for response in instance.response: msg = "adding response to task_result.response_stage: " msg += f"{json.dumps(response, indent=4, sort_keys=True)}" self.log.debug(msg) self.task_result.response_stage = copy.deepcopy(response) + self.task_result.response = copy.deepcopy(response) def _validate_images(self, serial_numbers) -> None: """ @@ -1076,11 +1088,13 @@ def _validate_images(self, serial_numbers) -> None: msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" self.log.debug(msg) self.task_result.diff_validate = copy.deepcopy(diff) + self.task_result.diff = copy.deepcopy(diff) for response in instance.response: msg = "adding response to task_result.response_validate: " msg += f"{json.dumps(response, indent=4, sort_keys=True)}" self.log.debug(msg) self.task_result.response_validate = copy.deepcopy(response) + self.task_result.response = copy.deepcopy(response) def _verify_install_options(self, devices) -> None: """ @@ -1218,11 +1232,13 @@ def _upgrade_images(self, devices) -> None: msg += f"{json.dumps(diff, indent=4, sort_keys=True)}" self.log.debug(msg) self.task_result.diff_upgrade = copy.deepcopy(diff) + self.task_result.diff = copy.deepcopy(diff) for response in upgrade.response: msg = "adding response to response_upgrade: " msg += f"{json.dumps(response, indent=4, sort_keys=True)}" self.log.debug(msg) self.task_result.response_upgrade = copy.deepcopy(response) + self.task_result.response = copy.deepcopy(response) def handle_merged_state(self) -> None: """ @@ -1294,6 +1310,7 @@ def handle_query_state(self) -> None: if "DATA" in response_current: response_current.pop("DATA") self.task_result.response_issu_status = copy.deepcopy(response_current) + self.task_result.response = copy.deepcopy(response_current) for switch in self.need: instance.filter = switch.get("ip_address") msg = f"SwitchIssuDetailsByIpAddress.filter: {instance.filter}, " @@ -1302,6 +1319,7 @@ def handle_query_state(self) -> None: if instance.filtered_data is None: continue self.task_result.diff_issu_status = instance.filtered_data + self.task_result.diff = instance.filtered_data def _failure(self, resp) -> None: """ diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml index b785691a1..259d88713 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml @@ -126,7 +126,223 @@ ################################################################################ # DELETED - SETUP - Upgrade all switches using global_config ################################################################################ - +# Expected result +# ok: [dcnm] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "action": "attach", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy_name": "KR5M", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "attach", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy_name": "KR5M", +# "serial_number": "FDO211218AX" +# }, +# { +# "action": "attach", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy_name": "KR5M", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy": "KR5M", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy": "KR5M", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy": "KR5M", +# "serial_number": "FDO211218AX" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy": "KR5M", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy": "KR5M", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy": "KR5M", +# "serial_number": "FDO211218AX" +# }, +# { +# "devices": [ +# { +# "policyName": "KR5M", +# "serialNumber": "FDO211218HB" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# }, +# { +# "devices": [ +# { +# "policyName": "KR5M", +# "serialNumber": "FDO211218AX" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# }, +# { +# "devices": [ +# { +# "policyName": "KR5M", +# "serialNumber": "FOX2109PHDD" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# } +# ], +# "failed": false, +# "response": [ +# { +# "DATA": "[cvd-2311-leaf:Success] [cvd-2312-leaf:Success] [cvd-2211-spine:Success] ", +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": [ +# { +# "key": "FDO211218AX", +# "value": "No files to stage" +# }, +# { +# "key": "FDO211218HB", +# "value": "No files to stage" +# }, +# { +# "key": "FOX2109PHDD", +# "value": "No files to stage" +# } +# ], +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": "[StageResponse [key=success, value=]]", +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 63, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 64, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 65, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# } +# ] +# } +# } +################################################################################ - name: DELETED - SETUP - Upgrade all switches using global config cisco.dcnm.dcnm_image_upgrade: &global_config state: merged @@ -164,21 +380,32 @@ that: - result.changed == true - result.failed == false - - result.diff.attach_policy[0].policy_name == image_policy_1 - - result.diff.attach_policy[1].policy_name == image_policy_1 - - result.diff.attach_policy[2].policy_name == image_policy_1 - - result.diff.upgrade[0].devices[0].policyName == image_policy_1 - - result.diff.upgrade[1].devices[0].policyName == image_policy_1 - - result.diff.upgrade[2].devices[0].policyName == image_policy_1 - - result.diff.validate[0].policy == image_policy_1 - - result.diff.validate[1].policy == image_policy_1 - - result.diff.validate[2].policy == image_policy_1 - - result.response.attach_policy[0].RETURN_CODE == 200 - - result.response.stage[0].RETURN_CODE == 200 - - result.response.upgrade[0].RETURN_CODE == 200 - - result.response.upgrade[1].RETURN_CODE == 200 - - result.response.upgrade[2].RETURN_CODE == 200 - - result.response.validate[0].RETURN_CODE == 200 + - result.diff[0].action == "attach" + - result.diff[1].action == "attach" + - result.diff[2].action == "attach" + - result.diff[0].policy_name == image_policy_1 + - result.diff[1].policy_name == image_policy_1 + - result.diff[2].policy_name == image_policy_1 + - result.diff[3].action == "stage" + - result.diff[4].action == "stage" + - result.diff[5].action == "stage" + - result.diff[3].policy == image_policy_1 + - result.diff[4].policy == image_policy_1 + - result.diff[5].policy == image_policy_1 + - result.diff[6].action == "validate" + - result.diff[7].action == "validate" + - result.diff[8].action == "validate" + - result.diff[6].policy == image_policy_1 + - result.diff[7].policy == image_policy_1 + - result.diff[8].policy == image_policy_1 + - result.diff[9].devices[0].policyName == image_policy_1 + - result.diff[10].devices[0].policyName == image_policy_1 + - result.diff[11].devices[0].policyName == image_policy_1 + - result.response[0].RETURN_CODE == 200 + - result.response[1].RETURN_CODE == 200 + - result.response[3].RETURN_CODE == 200 + - result.response[4].RETURN_CODE == 200 + - result.response[5].RETURN_CODE == 200 - name: DELETED - SETUP - Wait for controller response for all three switches cisco.dcnm.dcnm_image_upgrade: @@ -190,9 +417,9 @@ - ip_address: "{{ ansible_switch_3 }}" register: result until: - - result.diff.issu_status[0].ipAddress == ansible_switch_1 - - result.diff.issu_status[1].ipAddress == ansible_switch_2 - - result.diff.issu_status[2].ipAddress == ansible_switch_3 + - result.diff[0].ipAddress == ansible_switch_1 + - result.diff[1].ipAddress == ansible_switch_2 + - result.diff[2].ipAddress == ansible_switch_3 retries: 60 delay: 5 ignore_errors: yes @@ -200,7 +427,39 @@ ################################################################################ # DELETED - TEST - Detach policies from two switches and verify ################################################################################ - +# Expected result +# ok: [dcnm] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "action": "detach", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy_name": "KR5M", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "detach", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy_name": "KR5M", +# "serial_number": "FDO211218AX" +# } +# ], +# "failed": false, +# "response": [ +# { +# "DATA": "Successfully detach the policy from device.", +# "MESSAGE": "OK", +# "METHOD": "DELETE", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy?serialNumber=FDO211218HB,FDO211218AX", +# "RETURN_CODE": 200 +# } +# ] +# } +# } +################################################################################ - name: DELETED - TEST - Detach policies from two switches cisco.dcnm.dcnm_image_upgrade: state: deleted @@ -218,22 +477,43 @@ that: - result.changed == true - result.failed == false - - (result.diff.attach_policy | length) == 0 - - (result.diff.detach_policy | length) == 2 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.attach_policy | length) == 0 - - (result.response.detach_policy | length) == 1 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 - + - (result.diff | length) == 2 + - (result.response | length) == 1 + - result.diff[0]["action"] == "detach" + - result.diff[1]["action"] == "detach" + - response[0].RETURN_CODE == 200 + - response[0].DATA == "Successfully detach the policy from device." + - response[0].METHOD == "DELETE" ################################################################################ # DELETED - TEST - Detach policies from remaining switch and verify ################################################################################ - +# Expected result +# ok: [dcnm] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "action": "detach", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy_name": "KR5M", +# "serial_number": "FOX2109PHDD" +# } +# ], +# "failed": false, +# "response": [ +# { +# "DATA": "Successfully detach the policy from device.", +# "MESSAGE": "OK", +# "METHOD": "DELETE", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy?serialNumber=FOX2109PHDD", +# "RETURN_CODE": 200 +# } +# ] +# } +# } +################################################################################ - name: DELETED - TEST - Detach policy from remaining switch cisco.dcnm.dcnm_image_upgrade: state: deleted @@ -250,16 +530,11 @@ that: - result.changed == true - result.failed == false - - (result.diff.attach_policy | length) == 0 - - (result.diff.detach_policy | length) == 1 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.attach_policy | length) == 0 - - (result.response.detach_policy | length) == 1 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 + - (result.diff | length) == 1 + - (result.response | length) == 1 + - result.diff[0]["action"] == "detach" + - result.diff[0]["policy_name"] == image_policy_1 + - response[0].RETURN_CODE == 200 ################################################################################ # CLEAN-UP @@ -269,3 +544,4 @@ cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: deleted + diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml index e42cce4f4..bd016b213 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_global_config.yaml @@ -141,12 +141,228 @@ ################################################################################ # MERGED - TEST - Upgrade all switches using global config ################################################################################ - -- name: MERGED - TEST - Upgrade all switches using global config - cisco.dcnm.dcnm_image_upgrade: +# Expected result +# ok: [dcnm] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "action": "attach", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy_name": "KR5M", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "attach", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy_name": "KR5M", +# "serial_number": "FDO211218AX" +# }, +# { +# "action": "attach", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy_name": "KR5M", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy": "KR5M", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy": "KR5M", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy": "KR5M", +# "serial_number": "FDO211218AX" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy": "KR5M", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy": "KR5M", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy": "KR5M", +# "serial_number": "FDO211218AX" +# }, +# { +# "devices": [ +# { +# "policyName": "KR5M", +# "serialNumber": "FDO211218HB" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# }, +# { +# "devices": [ +# { +# "policyName": "KR5M", +# "serialNumber": "FDO211218AX" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# }, +# { +# "devices": [ +# { +# "policyName": "KR5M", +# "serialNumber": "FOX2109PHDD" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# } +# ], +# "failed": false, +# "response": [ +# { +# "DATA": "[cvd-2311-leaf:Success] [cvd-2312-leaf:Success] [cvd-2211-spine:Success] ", +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": [ +# { +# "key": "FDO211218AX", +# "value": "No files to stage" +# }, +# { +# "key": "FDO211218HB", +# "value": "No files to stage" +# }, +# { +# "key": "FOX2109PHDD", +# "value": "No files to stage" +# } +# ], +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": "[StageResponse [key=success, value=]]", +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 63, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 64, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 65, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# } +# ] +# } +# } +################################################################################ +- name: MERGED - SETUP - Upgrade all switches using global config + cisco.dcnm.dcnm_image_upgrade: &global_config state: merged config: - policy: "{{ image_policy_1}}" + policy: "{{ image_policy_1 }}" reboot: false stage: true validate: true @@ -179,21 +395,32 @@ that: - result.changed == true - result.failed == false - - result.diff.attach_policy[0].policy_name == image_policy_1 - - result.diff.attach_policy[1].policy_name == image_policy_1 - - result.diff.attach_policy[2].policy_name == image_policy_1 - - result.diff.upgrade[0].devices[0].policyName == image_policy_1 - - result.diff.upgrade[1].devices[0].policyName == image_policy_1 - - result.diff.upgrade[2].devices[0].policyName == image_policy_1 - - result.diff.validate[0].policy == image_policy_1 - - result.diff.validate[1].policy == image_policy_1 - - result.diff.validate[2].policy == image_policy_1 - - result.response.attach_policy[0].RETURN_CODE == 200 - - result.response.stage[0].RETURN_CODE == 200 - - result.response.upgrade[0].RETURN_CODE == 200 - - result.response.upgrade[1].RETURN_CODE == 200 - - result.response.upgrade[2].RETURN_CODE == 200 - - result.response.validate[0].RETURN_CODE == 200 + - result.diff[0].action == "attach" + - result.diff[1].action == "attach" + - result.diff[2].action == "attach" + - result.diff[0].policy_name == image_policy_1 + - result.diff[1].policy_name == image_policy_1 + - result.diff[2].policy_name == image_policy_1 + - result.diff[3].action == "stage" + - result.diff[4].action == "stage" + - result.diff[5].action == "stage" + - result.diff[3].policy == image_policy_1 + - result.diff[4].policy == image_policy_1 + - result.diff[5].policy == image_policy_1 + - result.diff[6].action == "validate" + - result.diff[7].action == "validate" + - result.diff[8].action == "validate" + - result.diff[6].policy == image_policy_1 + - result.diff[7].policy == image_policy_1 + - result.diff[8].policy == image_policy_1 + - result.diff[9].devices[0].policyName == image_policy_1 + - result.diff[10].devices[0].policyName == image_policy_1 + - result.diff[11].devices[0].policyName == image_policy_1 + - result.response[0].RETURN_CODE == 200 + - result.response[1].RETURN_CODE == 200 + - result.response[3].RETURN_CODE == 200 + - result.response[4].RETURN_CODE == 200 + - result.response[5].RETURN_CODE == 200 - name: MERGED - TEST - Wait for controller response for all three switches cisco.dcnm.dcnm_image_upgrade: @@ -205,9 +432,9 @@ - ip_address: "{{ ansible_switch_3 }}" register: result until: - - result.diff.issu_status[0].ipAddress == ansible_switch_1 - - result.diff.issu_status[1].ipAddress == ansible_switch_2 - - result.diff.issu_status[2].ipAddress == ansible_switch_3 + - result.diff[0].ipAddress == ansible_switch_1 + - result.diff[1].ipAddress == ansible_switch_2 + - result.diff[2].ipAddress == ansible_switch_3 retries: 60 delay: 5 ignore_errors: yes @@ -215,6 +442,16 @@ ################################################################################ # MERGED - TEST - global_config - IDEMPOTENCE ################################################################################ +# Expected result +# ok: [dcnm] => { +# "result": { +# "changed": false, +# "diff": [], +# "failed": false, +# "response": [] +# } +# } +################################################################################ - name: MERGED - TEST - global_config - IDEMPOTENCE cisco.dcnm.dcnm_image_upgrade: @@ -253,14 +490,8 @@ that: - result.changed == false - result.failed == false - - (result.diff.attach_policy | length) == 0 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.attach_policy | length) == 0 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 + - (result.diff | length) == 0 + - (result.response | length) == 0 ################################################################################ # CLEAN-UP diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml index 0312fa071..3113bea7c 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/merged_override_global_config.yaml @@ -138,7 +138,222 @@ ################################################################################ # MERGED - TEST - Override global image policy in switch configs ################################################################################ - +# Expected result +# ok: [dcnm] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "action": "attach", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy_name": "NR3F", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "attach", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy_name": "NR3F", +# "serial_number": "FDO211218AX" +# }, +# { +# "action": "attach", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy_name": "NR3F", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy": "NR3F", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy": "NR3F", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "stage", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy": "NR3F", +# "serial_number": "FDO211218AX" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.106", +# "logical_name": "cvd-2311-leaf", +# "policy": "NR3F", +# "serial_number": "FDO211218HB" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.114", +# "logical_name": "cvd-2211-spine", +# "policy": "NR3F", +# "serial_number": "FOX2109PHDD" +# }, +# { +# "action": "validate", +# "ip_address": "172.22.150.107", +# "logical_name": "cvd-2312-leaf", +# "policy": "NR3F", +# "serial_number": "FDO211218AX" +# }, +# { +# "devices": [ +# { +# "policyName": "NR3F", +# "serialNumber": "FDO211218HB" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# }, +# { +# "devices": [ +# { +# "policyName": "NR3F", +# "serialNumber": "FDO211218AX" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# }, +# { +# "devices": [ +# { +# "policyName": "NR3F", +# "serialNumber": "FOX2109PHDD" +# } +# ], +# "epldOptions": { +# "golden": false, +# "moduleNumber": "ALL" +# }, +# "epldUpgrade": false, +# "issuUpgrade": true, +# "issuUpgradeOptions1": { +# "disruptive": true, +# "forceNonDisruptive": false, +# "nonDisruptive": false +# }, +# "issuUpgradeOptions2": { +# "biosForce": false +# }, +# "pacakgeInstall": false, +# "pacakgeUnInstall": false, +# "reboot": false, +# "rebootOptions": { +# "configReload": false, +# "writeErase": false +# } +# } +# ], +# "failed": false, +# "response": [ +# { +# "DATA": "[cvd-2311-leaf:Success] [cvd-2312-leaf:Success] [cvd-2211-spine:Success] ", +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": [ +# { +# "key": "FDO211218AX", +# "value": "No files to stage" +# }, +# { +# "key": "FDO211218HB", +# "value": "No files to stage" +# }, +# { +# "key": "FOX2109PHDD", +# "value": "No files to stage" +# } +# ], +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/stage-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": "[StageResponse [key=success, value=]]", +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement/validate-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 71, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 72, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# }, +# { +# "DATA": 73, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade/upgrade-image", +# "RETURN_CODE": 200 +# } +# ] +# } +# } - name: MERGED - TEST - Upgrade all switches using global config. Override policy in switch configs. cisco.dcnm.dcnm_image_upgrade: state: merged @@ -179,21 +394,32 @@ that: - result.changed == true - result.failed == false - - result.diff.attach_policy[0].policy_name == image_policy_2 - - result.diff.attach_policy[1].policy_name == image_policy_2 - - result.diff.attach_policy[2].policy_name == image_policy_2 - - result.diff.upgrade[0].devices[0].policyName == image_policy_2 - - result.diff.upgrade[1].devices[0].policyName == image_policy_2 - - result.diff.upgrade[2].devices[0].policyName == image_policy_2 - - result.diff.validate[0].policy == image_policy_2 - - result.diff.validate[1].policy == image_policy_2 - - result.diff.validate[2].policy == image_policy_2 - - result.response.attach_policy[0].RETURN_CODE == 200 - - result.response.stage[0].RETURN_CODE == 200 - - result.response.upgrade[0].RETURN_CODE == 200 - - result.response.upgrade[1].RETURN_CODE == 200 - - result.response.upgrade[2].RETURN_CODE == 200 - - result.response.validate[0].RETURN_CODE == 200 + - result.diff[0].action == "attach" + - result.diff[1].action == "attach" + - result.diff[2].action == "attach" + - result.diff[0].policy_name == image_policy_2 + - result.diff[1].policy_name == image_policy_2 + - result.diff[2].policy_name == image_policy_2 + - result.diff[3].action == "stage" + - result.diff[4].action == "stage" + - result.diff[5].action == "stage" + - result.diff[3].policy == image_policy_2 + - result.diff[4].policy == image_policy_2 + - result.diff[5].policy == image_policy_2 + - result.diff[6].action == "validate" + - result.diff[7].action == "validate" + - result.diff[8].action == "validate" + - result.diff[6].policy == image_policy_2 + - result.diff[7].policy == image_policy_2 + - result.diff[8].policy == image_policy_2 + - result.diff[9].devices[0].policyName == image_policy_2 + - result.diff[10].devices[0].policyName == image_policy_2 + - result.diff[11].devices[0].policyName == image_policy_2 + - result.response[0].RETURN_CODE == 200 + - result.response[1].RETURN_CODE == 200 + - result.response[3].RETURN_CODE == 200 + - result.response[4].RETURN_CODE == 200 + - result.response[5].RETURN_CODE == 200 - name: MERGED - TEST - Wait for controller response for all three switches cisco.dcnm.dcnm_image_upgrade: @@ -205,9 +431,9 @@ - ip_address: "{{ ansible_switch_3 }}" register: result until: - - result.diff.issu_status[0].ipAddress == ansible_switch_1 - - result.diff.issu_status[1].ipAddress == ansible_switch_2 - - result.diff.issu_status[2].ipAddress == ansible_switch_3 + - result.diff[0].ipAddress == ansible_switch_1 + - result.diff[1].ipAddress == ansible_switch_2 + - result.diff[2].ipAddress == ansible_switch_3 retries: 60 delay: 5 ignore_errors: yes @@ -217,6 +443,16 @@ # # Anchor and Alias didn't work for this. I copied the entire config from above ################################################################################ +# Expected result +# ok: [dcnm] => { +# "result": { +# "changed": false, +# "diff": [], +# "failed": false, +# "response": [] +# } +# } +################################################################################ - name: MERGED - TEST - switch_config - Idempotence cisco.dcnm.dcnm_image_upgrade: @@ -255,14 +491,8 @@ that: - result.changed == false - result.failed == false - - (result.diff.attach_policy | length) == 0 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.attach_policy | length) == 0 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 + - (result.diff | length) == 0 + - (result.response | length) == 0 ################################################################################ # CLEAN-UP diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml index 1c047f311..2e20ede77 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/query.yaml @@ -165,21 +165,32 @@ that: - result.changed == true - result.failed == false - - result.diff.attach_policy[0].policy_name == image_policy_1 - - result.diff.attach_policy[1].policy_name == image_policy_1 - - result.diff.attach_policy[2].policy_name == image_policy_1 - - result.diff.upgrade[0].devices[0].policyName == image_policy_1 - - result.diff.upgrade[1].devices[0].policyName == image_policy_1 - - result.diff.upgrade[2].devices[0].policyName == image_policy_1 - - result.diff.validate[0].policy == image_policy_1 - - result.diff.validate[1].policy == image_policy_1 - - result.diff.validate[2].policy == image_policy_1 - - result.response.attach_policy[0].RETURN_CODE == 200 - - result.response.stage[0].RETURN_CODE == 200 - - result.response.upgrade[0].RETURN_CODE == 200 - - result.response.upgrade[1].RETURN_CODE == 200 - - result.response.upgrade[2].RETURN_CODE == 200 - - result.response.validate[0].RETURN_CODE == 200 + - result.diff[0].action == "attach" + - result.diff[1].action == "attach" + - result.diff[2].action == "attach" + - result.diff[0].policy_name == image_policy_1 + - result.diff[1].policy_name == image_policy_1 + - result.diff[2].policy_name == image_policy_1 + - result.diff[3].action == "stage" + - result.diff[4].action == "stage" + - result.diff[5].action == "stage" + - result.diff[3].policy == image_policy_1 + - result.diff[4].policy == image_policy_1 + - result.diff[5].policy == image_policy_1 + - result.diff[6].action == "validate" + - result.diff[7].action == "validate" + - result.diff[8].action == "validate" + - result.diff[6].policy == image_policy_1 + - result.diff[7].policy == image_policy_1 + - result.diff[8].policy == image_policy_1 + - result.diff[9].devices[0].policyName == image_policy_1 + - result.diff[10].devices[0].policyName == image_policy_1 + - result.diff[11].devices[0].policyName == image_policy_1 + - result.response[0].RETURN_CODE == 200 + - result.response[1].RETURN_CODE == 200 + - result.response[3].RETURN_CODE == 200 + - result.response[4].RETURN_CODE == 200 + - result.response[5].RETURN_CODE == 200 - name: QUERY - SETUP - Wait for controller response for all three switches cisco.dcnm.dcnm_image_upgrade: @@ -191,9 +202,9 @@ - ip_address: "{{ ansible_switch_3 }}" register: result until: - - result.diff.issu_status[0].ipAddress == ansible_switch_1 - - result.diff.issu_status[1].ipAddress == ansible_switch_2 - - result.diff.issu_status[2].ipAddress == ansible_switch_3 + - result.diff[0].ipAddress == ansible_switch_1 + - result.diff[1].ipAddress == ansible_switch_2 + - result.diff[2].ipAddress == ansible_switch_3 retries: 60 delay: 5 ignore_errors: yes @@ -219,32 +230,23 @@ that: - result.changed == false - result.failed == false - - (result.diff.issu_status | length) == 3 - - (result.diff.detach_policy | length) == 0 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.issu_status | length) == 1 - - (result.response.attach_policy | length) == 0 - - (result.response.detach_policy | length) == 0 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 - - (result.diff.issu_status[0].ipAddress) == ansible_switch_1 - - (result.diff.issu_status[1].ipAddress) == ansible_switch_2 - - (result.diff.issu_status[2].ipAddress) == ansible_switch_3 - - (result.diff.issu_status[0].policy) == image_policy_1 - - (result.diff.issu_status[1].policy) == image_policy_1 - - (result.diff.issu_status[2].policy) == image_policy_1 - - (result.diff.issu_status[0].statusPercent) == 100 - - (result.diff.issu_status[1].statusPercent) == 100 - - (result.diff.issu_status[2].statusPercent) == 100 + - (result.diff | length) == 3 + - (result.response | length) == 1 + - (result.diff[0].ipAddress) == ansible_switch_1 + - (result.diff[1].ipAddress) == ansible_switch_2 + - (result.diff[2].ipAddress) == ansible_switch_3 + - (result.diff[0].policy) == image_policy_1 + - (result.diff[1].policy) == image_policy_1 + - (result.diff[2].policy) == image_policy_1 + - (result.diff[0].statusPercent) == 100 + - (result.diff[1].statusPercent) == 100 + - (result.diff[2].statusPercent) == 100 ################################################################################ # QUERY - TEST - Detach policies from two switches and verify ################################################################################ -- name: QUERY - TEST - Detach policies from two switches +- name: QUERY - TEST - Detach policies from two switches and verify cisco.dcnm.dcnm_image_upgrade: state: deleted config: @@ -261,15 +263,14 @@ that: - result.changed == true - result.failed == false - - (result.diff.detach_policy | length) == 2 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.attach_policy | length) == 0 - - (result.response.detach_policy | length) == 1 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 + - (result.diff | length) == 2 + - (result.response | length) == 1 + - result.diff[0]["action"] == "detach" + - result.diff[1]["action"] == "detach" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA == "Successfully detach the policy from device." + - result.response[0].METHOD == "DELETE" + ################################################################################ @@ -293,26 +294,17 @@ that: - result.changed == false - result.failed == false - - (result.diff.issu_status | length) == 3 - - (result.diff.detach_policy | length) == 0 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.issu_status | length) == 1 - - (result.response.attach_policy | length) == 0 - - (result.response.detach_policy | length) == 0 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 - - (result.diff.issu_status[0].ipAddress) == ansible_switch_1 - - (result.diff.issu_status[1].ipAddress) == ansible_switch_2 - - (result.diff.issu_status[2].ipAddress) == ansible_switch_3 - - (result.diff.issu_status[0].policy) == "None" - - (result.diff.issu_status[1].policy) == "None" - - (result.diff.issu_status[2].policy) == image_policy_1 - - (result.diff.issu_status[0].statusPercent) == 0 - - (result.diff.issu_status[1].statusPercent) == 0 - - (result.diff.issu_status[2].statusPercent) == 100 + - (result.diff | length) == 3 + - (result.response | length) == 1 + - (result.diff[0].ipAddress) == ansible_switch_1 + - (result.diff[1].ipAddress) == ansible_switch_2 + - (result.diff[2].ipAddress) == ansible_switch_3 + - (result.diff[0].policy) == "None" + - (result.diff[1].policy) == "None" + - (result.diff[2].policy) == image_policy_1 + - (result.diff[0].statusPercent) == 0 + - (result.diff[1].statusPercent) == 0 + - (result.diff[2].statusPercent) == 100 ################################################################################ # QUERY - TEST - Detach policies from remaining switch and verify @@ -334,16 +326,8 @@ that: - result.changed == true - result.failed == false - - (result.diff.attach_policy | length) == 0 - - (result.diff.detach_policy | length) == 1 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.attach_policy | length) == 0 - - (result.response.detach_policy | length) == 1 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 + - (result.diff | length) == 1 + - (result.response | length) == 1 ################################################################################ # QUERY - TEST - Verify image_policy_1 removed from all switches @@ -366,26 +350,17 @@ that: - result.changed == false - result.failed == false - - (result.diff.issu_status | length) == 3 - - (result.diff.detach_policy | length) == 0 - - (result.diff.stage | length) == 0 - - (result.diff.upgrade | length) == 0 - - (result.diff.validate | length) == 0 - - (result.response.issu_status | length) == 1 - - (result.response.attach_policy | length) == 0 - - (result.response.detach_policy | length) == 0 - - (result.response.stage | length) == 0 - - (result.response.upgrade | length) == 0 - - (result.response.validate | length) == 0 - - (result.diff.issu_status[0].ipAddress) == ansible_switch_1 - - (result.diff.issu_status[1].ipAddress) == ansible_switch_2 - - (result.diff.issu_status[2].ipAddress) == ansible_switch_3 - - (result.diff.issu_status[0].policy) == "None" - - (result.diff.issu_status[1].policy) == "None" - - (result.diff.issu_status[2].policy) == "None" - - (result.diff.issu_status[0].statusPercent) == 0 - - (result.diff.issu_status[1].statusPercent) == 0 - - (result.diff.issu_status[2].statusPercent) == 0 + - (result.diff | length) == 3 + - (result.response | length) == 1 + - (result.diff[0].ipAddress) == ansible_switch_1 + - (result.diff[1].ipAddress) == ansible_switch_2 + - (result.diff[2].ipAddress) == ansible_switch_3 + - (result.diff[0].policy) == "None" + - (result.diff[1].policy) == "None" + - (result.diff[2].policy) == "None" + - (result.diff[0].statusPercent) == 0 + - (result.diff[1].statusPercent) == 0 + - (result.diff[2].statusPercent) == 0 ################################################################################ # CLEAN-UP diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py index fa72109e4..c58ddea42 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task.py @@ -65,6 +65,50 @@ def image_upgrade_task_bare_fixture(): return ImageUpgradeTask +# def test_image_upgrade_upgrade_task_00001(image_upgrade_task_bare) -> None: +# """ +# Function +# - __init__ + +# Test +# - Class attributes are initialized to expected values +# """ +# instance = image_upgrade_task_bare(MockAnsibleModule) +# assert isinstance(instance, ImageUpgradeTask) +# assert instance.class_name == "ImageUpgradeTask" +# assert instance.have is None +# assert instance.idempotent_want is None +# assert instance.switch_configs == [] +# assert instance.path is None +# assert instance.verb is None +# assert instance.config == {"switches": [{"ip_address": "172.22.150.105"}]} +# assert instance.check_mode is False +# assert instance.validated == {} +# assert instance.want == [] +# assert instance.need == [] +# assert instance.task_result.module_result == { +# "changed": False, +# "diff": { +# "attach_policy": [], +# "detach_policy": [], +# "issu_status": [], +# "stage": [], +# "upgrade": [], +# "validate": [], +# }, +# "response": { +# "attach_policy": [], +# "detach_policy": [], +# "issu_status": [], +# "stage": [], +# "upgrade": [], +# "validate": [], +# }, +# } +# assert isinstance(instance.switch_details, SwitchDetails) +# assert isinstance(instance.image_policies, ImagePolicies) + + def test_image_upgrade_upgrade_task_00001(image_upgrade_task_bare) -> None: """ Function @@ -88,22 +132,8 @@ def test_image_upgrade_upgrade_task_00001(image_upgrade_task_bare) -> None: assert instance.need == [] assert instance.task_result.module_result == { "changed": False, - "diff": { - "attach_policy": [], - "detach_policy": [], - "issu_status": [], - "stage": [], - "upgrade": [], - "validate": [], - }, - "response": { - "attach_policy": [], - "detach_policy": [], - "issu_status": [], - "stage": [], - "upgrade": [], - "validate": [], - }, + "diff": [], + "response": [], } assert isinstance(instance.switch_details, SwitchDetails) assert isinstance(instance.image_policies, ImagePolicies) diff --git a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py index be64f5d93..95da97055 100644 --- a/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py +++ b/tests/unit/modules/dcnm/dcnm_image_upgrade/test_image_upgrade_image_upgrade_task_result.py @@ -200,88 +200,87 @@ def test_image_upgrade_upgrade_task_result_00050(image_upgrade_task_result) -> N result = instance.module_result assert isinstance(result, dict) assert result["changed"] is False - for key in test_keys: - assert result["diff"][key] == [] - for key in test_keys: - assert result["response"][key] == [] - - -@pytest.mark.parametrize( - "state, changed", - [ - ("attach_policy", True), - ("detach_policy", True), - ("issu_status", False), - ("stage", True), - ("upgrade", True), - ("validate", True), - ], -) -def test_image_upgrade_upgrade_task_result_00051( - image_upgrade_task_result, state, changed -) -> None: - """ - Function - - ImageUpgradeTaskResult.module_result - - ImageUpgradeTaskResult.did_anything_change - - ImageUpgradeTaskResult.diff_* - - ImageUpgradeTaskResult.response_* - - Summary - Verify that module_result returns a dict with expected values when - changes have been made to each of the supported states. - - Test - - For non-query-state properties, "changed" should be True - - The diff should be a list containing the dict passed to - the state's diff property (e.g. diff_stage, diff_issu_status, etc) - - The response should be a list containing the dict passed to - the state's response property (e.g. response_stage, response_issu_status, etc) - - All other diffs should be empty lists - - All other responses should be empty lists - """ - test_key = state - test_keys = [ - "attach_policy", - "detach_policy", - "issu_status", - "stage", - "upgrade", - "validate", - ] - diff = {"diff": "diff"} - response = {"response": "response"} - - with does_not_raise(): - instance = image_upgrade_task_result - if state == "attach_policy": - instance.diff_attach_policy = diff - instance.response_attach_policy = response - elif state == "detach_policy": - instance.diff_detach_policy = diff - instance.response_detach_policy = response - elif state == "issu_status": - instance.diff_issu_status = diff - instance.response_issu_status = response - elif state == "stage": - instance.diff_stage = diff - instance.response_stage = response - elif state == "upgrade": - instance.diff_upgrade = diff - instance.response_upgrade = response - elif state == "validate": - instance.diff_validate = diff - instance.response_validate = response - result = instance.module_result - assert isinstance(result, dict) - assert result["changed"] == changed - for key in test_keys: - if key == test_key: - assert result["diff"][key] == [diff] - assert result["response"][key] == [response] - else: - assert result["diff"][key] == [] - assert result["response"][key] == [] + assert result["diff"] == [] + assert result["response"] == [] + + +# REMOVING DUE TO CHANGES IN RESULT STRUCTURE +# @pytest.mark.parametrize( +# "state, changed", +# [ +# ("attach_policy", True), +# ("detach_policy", True), +# ("issu_status", False), +# ("stage", True), +# ("upgrade", True), +# ("validate", True), +# ], +# ) +# def test_image_upgrade_upgrade_task_result_00051( +# image_upgrade_task_result, state, changed +# ) -> None: +# """ +# Function +# - ImageUpgradeTaskResult.module_result +# - ImageUpgradeTaskResult.did_anything_change +# - ImageUpgradeTaskResult.diff_* +# - ImageUpgradeTaskResult.response_* + +# Summary +# Verify that module_result returns a dict with expected values when +# changes have been made to each of the supported states. + +# Test +# - For non-query-state properties, "changed" should be True +# - The diff should be a list containing the dict passed to +# the state's diff property (e.g. diff_stage, diff_issu_status, etc) +# - The response should be a list containing the dict passed to +# the state's response property (e.g. response_stage, response_issu_status, etc) +# - All other diffs should be empty lists +# - All other responses should be empty lists +# """ +# test_key = state +# test_keys = [ +# "attach_policy", +# "detach_policy", +# "issu_status", +# "stage", +# "upgrade", +# "validate", +# ] +# diff = {"diff": "diff"} +# response = {"response": "response"} + +# with does_not_raise(): +# instance = image_upgrade_task_result +# if state == "attach_policy": +# instance.diff_attach_policy = diff +# instance.response_attach_policy = response +# elif state == "detach_policy": +# instance.diff_detach_policy = diff +# instance.response_detach_policy = response +# elif state == "issu_status": +# instance.diff_issu_status = diff +# instance.response_issu_status = response +# elif state == "stage": +# instance.diff_stage = diff +# instance.response_stage = response +# elif state == "upgrade": +# instance.diff_upgrade = diff +# instance.response_upgrade = response +# elif state == "validate": +# instance.diff_validate = diff +# instance.response_validate = response +# result = instance.module_result +# assert isinstance(result, dict) +# assert result["changed"] == changed +# for key in test_keys: +# if key == test_key: +# assert result["diff"][key] == [diff] +# assert result["response"][key] == [response] +# else: +# assert result["diff"][key] == [] +# assert result["response"][key] == [] @pytest.mark.parametrize( From 37bdb8fbb6cb1b05447dda62d8f36197f1e292d6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 22 Mar 2024 16:37:30 -1000 Subject: [PATCH 300/300] Fix yamllint "too many blank lines" --- tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml index 259d88713..ef6973d17 100644 --- a/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml +++ b/tests/integration/targets/dcnm_image_upgrade/tests/deleted.yaml @@ -544,4 +544,3 @@ cisco.dcnm.dcnm_inventory: fabric: "{{ fabric_name }}" state: deleted -