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)