-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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()
- Loading branch information
1 parent
8fe798e
commit a258131
Showing
10 changed files
with
1,028 additions
and
237 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.